[ADD] product_sale_price_from_pricelist: módulo para calcular precio de venta desde tarifa
This commit is contained in:
parent
123aabb775
commit
1bcc31b810
9 changed files with 712 additions and 16 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Product Sale Price from Pricelist",
|
"name": "Product Sale Price from Pricelist",
|
||||||
"version": "16.0.1.0.0",
|
"version": "18.0.1.0.0",
|
||||||
"category": "product",
|
"category": "product",
|
||||||
"summary": "Set sale price from pricelist based on last purchase price",
|
"summary": "Set sale price from pricelist based on last purchase price",
|
||||||
"description": """
|
"description": """
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"depends": [
|
"depends": [
|
||||||
"product_get_price_helper",
|
"product_get_price_helper",
|
||||||
|
"account_invoice_triple_discount_readonly",
|
||||||
"sale",
|
"sale",
|
||||||
"purchase",
|
"purchase",
|
||||||
"account",
|
"account",
|
||||||
"stock_account",
|
"stock_account",
|
||||||
"product_template_tags",
|
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"views/actions.xml",
|
"views/actions.xml",
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@ class ProductPricelist(models.Model):
|
||||||
ProductProduct = self.env["product.product"]
|
ProductProduct = self.env["product.product"]
|
||||||
res = super()._compute_price_rule(
|
res = super()._compute_price_rule(
|
||||||
products,
|
products,
|
||||||
qty=1,
|
qty=qty,
|
||||||
uom=uom,
|
uom=uom,
|
||||||
date=date,
|
date=date,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
new_res = res.copy()
|
new_res = res.copy()
|
||||||
item_id = []
|
item_id = []
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ class ProductPricelistItem(models.Model):
|
||||||
ondelete={"last_purchase_price": "set default"},
|
ondelete={"last_purchase_price": "set default"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _compute_price(self, product, quantity, uom, date, currency=None):
|
def _compute_price(self, product, qty, uom, date, currency=None):
|
||||||
result = super()._compute_price(product, quantity, uom, date, currency)
|
result = super()._compute_price(product, qty, uom, date, currency)
|
||||||
if self.compute_price == "formula" and self.base == "last_purchase_price":
|
if self.compute_price == "formula" and self.base == "last_purchase_price":
|
||||||
result = product.sudo().last_purchase_price_received
|
result = product.sudo().last_purchase_price_received
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
4
product_sale_price_from_pricelist/tests/__init__.py
Normal file
4
product_sale_price_from_pricelist/tests/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from . import test_product_template
|
||||||
|
from . import test_stock_move
|
||||||
|
from . import test_pricelist
|
||||||
|
from . import test_res_config
|
||||||
212
product_sale_price_from_pricelist/tests/test_pricelist.py
Normal file
212
product_sale_price_from_pricelist/tests/test_pricelist.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestPricelist(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Create tax
|
||||||
|
cls.tax = cls.env["account.tax"].create({
|
||||||
|
"name": "Test Tax 10%",
|
||||||
|
"amount": 10.0,
|
||||||
|
"amount_type": "percent",
|
||||||
|
"type_tax_use": "sale",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create product
|
||||||
|
cls.product = cls.env["product.product"].create({
|
||||||
|
"name": "Test Product Pricelist",
|
||||||
|
"type": "product",
|
||||||
|
"list_price": 100.0,
|
||||||
|
"standard_price": 50.0,
|
||||||
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
|
"last_purchase_price_received": 50.0,
|
||||||
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create pricelist with last_purchase_price base
|
||||||
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
|
"name": "Test Pricelist Last Purchase",
|
||||||
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_pricelist_item_base_last_purchase_price(self):
|
||||||
|
"""Test pricelist item with last_purchase_price base"""
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 0,
|
||||||
|
"price_surcharge": 10.0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Compute price using pricelist
|
||||||
|
price = self.pricelist._compute_price_rule(
|
||||||
|
self.product, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Price should be based on last_purchase_price
|
||||||
|
self.assertIn(self.product.id, price)
|
||||||
|
# The exact price depends on the formula calculation
|
||||||
|
|
||||||
|
def test_pricelist_item_with_discount(self):
|
||||||
|
"""Test pricelist item with discount on last_purchase_price"""
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 20.0, # 20% discount
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
price = self.pricelist._compute_price_rule(
|
||||||
|
self.product, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expected: 50.0 - (50.0 * 0.20) = 40.0
|
||||||
|
self.assertIn(self.product.id, price)
|
||||||
|
self.assertEqual(price[self.product.id][0], 40.0)
|
||||||
|
|
||||||
|
def test_pricelist_item_with_markup(self):
|
||||||
|
"""Test pricelist item with markup on last_purchase_price"""
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_markup": 100.0, # 100% markup (double the price)
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
# _compute_price should return the base price (last_purchase_price_received)
|
||||||
|
result = pricelist_item._compute_price(
|
||||||
|
self.product,
|
||||||
|
qty=1,
|
||||||
|
uom=self.product.uom_id,
|
||||||
|
date=False,
|
||||||
|
currency=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return the last purchase price as base
|
||||||
|
self.assertEqual(result, 50.0)
|
||||||
|
|
||||||
|
def test_pricelist_item_compute_price_method(self):
|
||||||
|
"""Test _compute_price method of pricelist item"""
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_markup": 50.0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
result = pricelist_item._compute_price(
|
||||||
|
self.product,
|
||||||
|
qty=1,
|
||||||
|
uom=self.product.uom_id,
|
||||||
|
date=False,
|
||||||
|
currency=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return last_purchase_price_received
|
||||||
|
self.assertEqual(result, self.product.last_purchase_price_received)
|
||||||
|
|
||||||
|
def test_pricelist_item_with_zero_last_purchase_price(self):
|
||||||
|
"""Test pricelist behavior when last_purchase_price is zero"""
|
||||||
|
product_zero = self.env["product.product"].create({
|
||||||
|
"name": "Product Zero Price",
|
||||||
|
"type": "product",
|
||||||
|
"list_price": 100.0,
|
||||||
|
"last_purchase_price_received": 0.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 10.0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
price = self.pricelist._compute_price_rule(
|
||||||
|
product_zero, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should handle zero price gracefully
|
||||||
|
self.assertIn(product_zero.id, price)
|
||||||
|
self.assertEqual(price[product_zero.id][0], 0.0)
|
||||||
|
|
||||||
|
def test_pricelist_multiple_products(self):
|
||||||
|
"""Test pricelist calculation with multiple products"""
|
||||||
|
product2 = self.env["product.product"].create({
|
||||||
|
"name": "Test Product 2",
|
||||||
|
"type": "product",
|
||||||
|
"list_price": 200.0,
|
||||||
|
"last_purchase_price_received": 100.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": self.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 10.0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
products = self.product | product2
|
||||||
|
|
||||||
|
# Test with both products
|
||||||
|
for product in products:
|
||||||
|
price = self.pricelist._compute_price_rule(
|
||||||
|
product, qty=1, date=False
|
||||||
|
)
|
||||||
|
self.assertIn(product.id, price)
|
||||||
|
|
||||||
|
def test_pricelist_item_selection_add(self):
|
||||||
|
"""Test that last_purchase_price is added to base selection"""
|
||||||
|
pricelist_item_model = self.env["product.pricelist.item"]
|
||||||
|
base_field = pricelist_item_model._fields["base"]
|
||||||
|
|
||||||
|
# Check that last_purchase_price is in the selection
|
||||||
|
selection_values = [item[0] for item in base_field.selection]
|
||||||
|
self.assertIn("last_purchase_price", selection_values)
|
||||||
|
|
||||||
|
def test_product_price_compute_fallback(self):
|
||||||
|
"""Test price_compute method fallback for last_purchase_price"""
|
||||||
|
result = self.product.price_compute("last_purchase_price")
|
||||||
|
|
||||||
|
# Should return dummy value 1.0 for all products
|
||||||
|
self.assertEqual(result[self.product.id], 1.0)
|
||||||
|
|
||||||
|
def test_pricelist_with_different_currencies(self):
|
||||||
|
"""Test pricelist with different currency (if applicable)"""
|
||||||
|
# This test is optional and depends on multi-currency setup
|
||||||
|
eur = self.env.ref("base.EUR", raise_if_not_found=False)
|
||||||
|
if not eur:
|
||||||
|
self.skipTest("EUR currency not available")
|
||||||
|
|
||||||
|
pricelist_eur = self.env["product.pricelist"].create({
|
||||||
|
"name": "Test Pricelist EUR",
|
||||||
|
"currency_id": eur.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
pricelist_item = self.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": pricelist_eur.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Price calculation should work with different currency
|
||||||
|
price = pricelist_eur._compute_price_rule(
|
||||||
|
self.product, qty=1, date=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(self.product.id, price)
|
||||||
163
product_sale_price_from_pricelist/tests/test_product_template.py
Normal file
163
product_sale_price_from_pricelist/tests/test_product_template.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestProductTemplate(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Create a tax for the product
|
||||||
|
cls.tax = cls.env["account.tax"].create({
|
||||||
|
"name": "Test Tax 21%",
|
||||||
|
"amount": 21.0,
|
||||||
|
"amount_type": "percent",
|
||||||
|
"type_tax_use": "sale",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a pricelist with formula based on cost
|
||||||
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
|
"name": "Test Pricelist Automatic",
|
||||||
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.pricelist_item = cls.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": cls.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_discount": 0,
|
||||||
|
"price_surcharge": 0,
|
||||||
|
"price_markup": 50.0, # 50% markup
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set the pricelist in configuration
|
||||||
|
cls.env["ir.config_parameter"].sudo().set_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
|
str(cls.pricelist.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a product category
|
||||||
|
cls.category = cls.env["product.category"].create({
|
||||||
|
"name": "Test Category",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a product
|
||||||
|
cls.product = cls.env["product.template"].create({
|
||||||
|
"name": "Test Product",
|
||||||
|
"type": "product",
|
||||||
|
"categ_id": cls.category.id,
|
||||||
|
"list_price": 10.0,
|
||||||
|
"standard_price": 5.0,
|
||||||
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
|
"last_purchase_price_received": 5.0,
|
||||||
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_compute_theoritical_price_without_tax_error(self):
|
||||||
|
"""Test that computing theoretical price without taxes raises an error"""
|
||||||
|
product_no_tax = self.env["product.template"].create({
|
||||||
|
"name": "Product No Tax",
|
||||||
|
"type": "product",
|
||||||
|
"list_price": 10.0,
|
||||||
|
"standard_price": 5.0,
|
||||||
|
"last_purchase_price_received": 5.0,
|
||||||
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
product_no_tax._compute_theoritical_price()
|
||||||
|
|
||||||
|
def test_compute_theoritical_price_success(self):
|
||||||
|
"""Test successful computation of theoretical price"""
|
||||||
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
|
# Verify that theoretical price was calculated
|
||||||
|
self.assertGreater(self.product.list_price_theoritical, 0)
|
||||||
|
|
||||||
|
# Verify that the price includes markup and tax
|
||||||
|
# With 50% markup on 5.0 = 7.5, plus 21% tax = 9.075, rounded to 9.10
|
||||||
|
self.assertAlmostEqual(self.product.list_price_theoritical, 9.10, places=2)
|
||||||
|
|
||||||
|
def test_compute_theoritical_price_with_rounding(self):
|
||||||
|
"""Test that prices are rounded to 0.05"""
|
||||||
|
self.product.last_purchase_price_received = 4.0
|
||||||
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
|
# Price should be rounded to nearest 0.05
|
||||||
|
price = self.product.list_price_theoritical
|
||||||
|
self.assertEqual(round(price % 0.05, 2), 0.0)
|
||||||
|
|
||||||
|
def test_last_purchase_price_updated_flag(self):
|
||||||
|
"""Test that the updated flag is set when prices differ"""
|
||||||
|
initial_list_price = self.product.list_price
|
||||||
|
self.product.last_purchase_price_received = 10.0
|
||||||
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
|
if self.product.list_price_theoritical != initial_list_price:
|
||||||
|
self.assertTrue(self.product.last_purchase_price_updated)
|
||||||
|
|
||||||
|
def test_action_update_list_price(self):
|
||||||
|
"""Test updating list price from theoretical price"""
|
||||||
|
self.product.last_purchase_price_received = 8.0
|
||||||
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
|
theoretical_price = self.product.list_price_theoritical
|
||||||
|
self.product.action_update_list_price()
|
||||||
|
|
||||||
|
# Verify that list price was updated
|
||||||
|
self.assertEqual(self.product.list_price, theoretical_price)
|
||||||
|
self.assertFalse(self.product.last_purchase_price_updated)
|
||||||
|
|
||||||
|
def test_manual_update_type_skips_automatic(self):
|
||||||
|
"""Test that manual update type prevents automatic price calculation"""
|
||||||
|
self.product.last_purchase_price_compute_type = "manual_update"
|
||||||
|
initial_list_price = self.product.list_price
|
||||||
|
|
||||||
|
self.product.action_update_list_price()
|
||||||
|
|
||||||
|
# List price should not change for manual update type
|
||||||
|
self.assertEqual(self.product.list_price, initial_list_price)
|
||||||
|
|
||||||
|
def test_price_compute_with_last_purchase_price(self):
|
||||||
|
"""Test price_compute method with last_purchase_price type"""
|
||||||
|
result = self.product.price_compute("last_purchase_price")
|
||||||
|
|
||||||
|
# Should return dummy prices (1.0) for all product ids
|
||||||
|
for product_id in result:
|
||||||
|
self.assertEqual(result[product_id], 1.0)
|
||||||
|
|
||||||
|
def test_compute_theoritical_price_without_pricelist(self):
|
||||||
|
"""Test that missing pricelist raises an error"""
|
||||||
|
# Remove pricelist configuration
|
||||||
|
self.env["ir.config_parameter"].sudo().set_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
self.product._compute_theoritical_price()
|
||||||
|
|
||||||
|
# Restore pricelist configuration for other tests
|
||||||
|
self.env["ir.config_parameter"].sudo().set_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
|
str(self.pricelist.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_company_dependent_fields(self):
|
||||||
|
"""Test that price fields are company dependent"""
|
||||||
|
# Verify field properties
|
||||||
|
field_last_purchase = self.product._fields["last_purchase_price_received"]
|
||||||
|
field_theoritical = self.product._fields["list_price_theoritical"]
|
||||||
|
field_updated = self.product._fields["last_purchase_price_updated"]
|
||||||
|
field_compute_type = self.product._fields["last_purchase_price_compute_type"]
|
||||||
|
|
||||||
|
self.assertTrue(field_last_purchase.company_dependent)
|
||||||
|
self.assertTrue(field_theoritical.company_dependent)
|
||||||
|
self.assertTrue(field_updated.company_dependent)
|
||||||
|
self.assertTrue(field_compute_type.company_dependent)
|
||||||
78
product_sale_price_from_pricelist/tests/test_res_config.py
Normal file
78
product_sale_price_from_pricelist/tests/test_res_config.py
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestResConfigSettings(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
|
"name": "Test Config Pricelist",
|
||||||
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_config_parameter_set_and_get(self):
|
||||||
|
"""Test setting and getting pricelist configuration"""
|
||||||
|
config = self.env["res.config.settings"].create({
|
||||||
|
"product_pricelist_automatic": self.pricelist.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
config.execute()
|
||||||
|
|
||||||
|
# Verify parameter was saved
|
||||||
|
saved_id = self.env["ir.config_parameter"].sudo().get_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(int(saved_id), self.pricelist.id)
|
||||||
|
|
||||||
|
def test_config_load_from_parameter(self):
|
||||||
|
"""Test loading pricelist from config parameter"""
|
||||||
|
# Set parameter directly
|
||||||
|
self.env["ir.config_parameter"].sudo().set_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
|
str(self.pricelist.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create config and check if value is loaded
|
||||||
|
config = self.env["res.config.settings"].create({})
|
||||||
|
|
||||||
|
self.assertEqual(config.product_pricelist_automatic.id, self.pricelist.id)
|
||||||
|
|
||||||
|
def test_config_update_pricelist(self):
|
||||||
|
"""Test updating pricelist configuration"""
|
||||||
|
# Set initial pricelist
|
||||||
|
config = self.env["res.config.settings"].create({
|
||||||
|
"product_pricelist_automatic": self.pricelist.id,
|
||||||
|
})
|
||||||
|
config.execute()
|
||||||
|
|
||||||
|
# Create new pricelist and update
|
||||||
|
new_pricelist = self.env["product.pricelist"].create({
|
||||||
|
"name": "New Config Pricelist",
|
||||||
|
"currency_id": self.env.company.currency_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
config2 = self.env["res.config.settings"].create({
|
||||||
|
"product_pricelist_automatic": new_pricelist.id,
|
||||||
|
})
|
||||||
|
config2.execute()
|
||||||
|
|
||||||
|
# Verify new value
|
||||||
|
saved_id = self.env["ir.config_parameter"].sudo().get_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(int(saved_id), new_pricelist.id)
|
||||||
|
|
||||||
|
def test_config_without_pricelist(self):
|
||||||
|
"""Test configuration can be saved without pricelist"""
|
||||||
|
config = self.env["res.config.settings"].create({})
|
||||||
|
|
||||||
|
# Should not raise error
|
||||||
|
config.execute()
|
||||||
244
product_sale_price_from_pricelist/tests/test_stock_move.py
Normal file
244
product_sale_price_from_pricelist/tests/test_stock_move.py
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestStockMove(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Create tax
|
||||||
|
cls.tax = cls.env["account.tax"].create({
|
||||||
|
"name": "Test Tax 21%",
|
||||||
|
"amount": 21.0,
|
||||||
|
"amount_type": "percent",
|
||||||
|
"type_tax_use": "sale",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create pricelist
|
||||||
|
cls.pricelist = cls.env["product.pricelist"].create({
|
||||||
|
"name": "Test Pricelist",
|
||||||
|
"currency_id": cls.env.company.currency_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.pricelist_item = cls.env["product.pricelist.item"].create({
|
||||||
|
"pricelist_id": cls.pricelist.id,
|
||||||
|
"compute_price": "formula",
|
||||||
|
"base": "last_purchase_price",
|
||||||
|
"price_markup": 50.0,
|
||||||
|
"applied_on": "3_global",
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.env["ir.config_parameter"].sudo().set_param(
|
||||||
|
"product_sale_price_from_pricelist.product_pricelist_automatic",
|
||||||
|
str(cls.pricelist.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create supplier
|
||||||
|
cls.supplier = cls.env["res.partner"].create({
|
||||||
|
"name": "Test Supplier",
|
||||||
|
"supplier_rank": 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create product with UoM
|
||||||
|
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
||||||
|
cls.uom_dozen = cls.env.ref("uom.product_uom_dozen")
|
||||||
|
|
||||||
|
cls.product = cls.env["product.product"].create({
|
||||||
|
"name": "Test Product Stock",
|
||||||
|
"type": "product",
|
||||||
|
"uom_id": cls.uom_unit.id,
|
||||||
|
"uom_po_id": cls.uom_unit.id,
|
||||||
|
"list_price": 10.0,
|
||||||
|
"standard_price": 5.0,
|
||||||
|
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||||
|
"last_purchase_price_received": 5.0,
|
||||||
|
"last_purchase_price_compute_type": "without_discounts",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create locations
|
||||||
|
cls.supplier_location = cls.env.ref("stock.stock_location_suppliers")
|
||||||
|
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||||
|
|
||||||
|
def _create_purchase_order(self, product, qty, price, discount1=0, discount2=0, discount3=0):
|
||||||
|
"""Helper to create a purchase order"""
|
||||||
|
purchase_order = self.env["purchase.order"].create({
|
||||||
|
"partner_id": self.supplier.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
po_line = self.env["purchase.order.line"].create({
|
||||||
|
"order_id": purchase_order.id,
|
||||||
|
"product_id": product.id,
|
||||||
|
"product_qty": qty,
|
||||||
|
"price_unit": price,
|
||||||
|
"product_uom": product.uom_po_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add discounts if module supports it
|
||||||
|
if hasattr(po_line, "discount1"):
|
||||||
|
po_line.discount1 = discount1
|
||||||
|
if hasattr(po_line, "discount2"):
|
||||||
|
po_line.discount2 = discount2
|
||||||
|
if hasattr(po_line, "discount3"):
|
||||||
|
po_line.discount3 = discount3
|
||||||
|
|
||||||
|
return purchase_order
|
||||||
|
|
||||||
|
def test_update_price_without_discounts(self):
|
||||||
|
"""Test price update without discounts"""
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=8.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
# Get the picking and process it
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Verify price was updated
|
||||||
|
self.assertEqual(self.product.last_purchase_price_received, 8.0)
|
||||||
|
|
||||||
|
def test_update_price_with_first_discount(self):
|
||||||
|
"""Test price update with first discount only"""
|
||||||
|
if not hasattr(self.env["purchase.order.line"], "discount1"):
|
||||||
|
self.skipTest("Purchase discount module not installed")
|
||||||
|
|
||||||
|
self.product.last_purchase_price_compute_type = "with_discount"
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=10.0, discount1=20.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Expected: 10.0 * (1 - 0.20) = 8.0
|
||||||
|
self.assertEqual(self.product.last_purchase_price_received, 8.0)
|
||||||
|
|
||||||
|
def test_update_price_with_two_discounts(self):
|
||||||
|
"""Test price update with two discounts"""
|
||||||
|
if not hasattr(self.env["purchase.order.line"], "discount2"):
|
||||||
|
self.skipTest("Purchase double discount module not installed")
|
||||||
|
|
||||||
|
self.product.last_purchase_price_compute_type = "with_two_discounts"
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Expected: 10.0 * (1 - 0.20) * (1 - 0.10) = 7.2
|
||||||
|
self.assertEqual(self.product.last_purchase_price_received, 7.2)
|
||||||
|
|
||||||
|
def test_update_price_with_three_discounts(self):
|
||||||
|
"""Test price update with three discounts"""
|
||||||
|
if not hasattr(self.env["purchase.order.line"], "discount3"):
|
||||||
|
self.skipTest("Purchase triple discount module not installed")
|
||||||
|
|
||||||
|
self.product.last_purchase_price_compute_type = "with_three_discounts"
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=10.0, discount1=20.0, discount2=10.0, discount3=5.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Price should be calculated from subtotal / qty
|
||||||
|
# Subtotal with all discounts applied
|
||||||
|
|
||||||
|
def test_update_price_with_uom_conversion(self):
|
||||||
|
"""Test price update with different purchase UoM"""
|
||||||
|
# Create product with different purchase UoM
|
||||||
|
product_dozen = self.product.copy({
|
||||||
|
"name": "Test Product Dozen",
|
||||||
|
"uom_po_id": self.uom_dozen.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
product_dozen, qty=2, price=120.0 # 2 dozens at 120.0 per dozen
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# Price should be converted to base UoM (unit)
|
||||||
|
# 120.0 per dozen = 10.0 per unit
|
||||||
|
self.assertEqual(product_dozen.last_purchase_price_received, 10.0)
|
||||||
|
|
||||||
|
def test_no_update_with_zero_quantity(self):
|
||||||
|
"""Test that price is not updated with zero quantity done"""
|
||||||
|
initial_price = self.product.last_purchase_price_received
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=15.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
# Set quantity done to 0
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = 0
|
||||||
|
|
||||||
|
# This should not update the price
|
||||||
|
# Price should remain unchanged
|
||||||
|
self.assertEqual(self.product.last_purchase_price_received, initial_price)
|
||||||
|
|
||||||
|
def test_manual_update_type_no_automatic_update(self):
|
||||||
|
"""Test that manual update type prevents automatic price updates"""
|
||||||
|
self.product.last_purchase_price_compute_type = "manual_update"
|
||||||
|
initial_price = self.product.last_purchase_price_received
|
||||||
|
|
||||||
|
purchase_order = self._create_purchase_order(
|
||||||
|
self.product, qty=10, price=15.0
|
||||||
|
)
|
||||||
|
purchase_order.button_confirm()
|
||||||
|
|
||||||
|
picking = purchase_order.picking_ids[0]
|
||||||
|
picking.action_assign()
|
||||||
|
|
||||||
|
for move in picking.move_ids:
|
||||||
|
move.quantity = move.product_uom_qty
|
||||||
|
|
||||||
|
picking.button_validate()
|
||||||
|
|
||||||
|
# For manual update, the standard Odoo behavior applies
|
||||||
|
# which may or may not update standard_price, but our custom
|
||||||
|
# last_purchase_price_received logic still runs
|
||||||
|
|
@ -6,15 +6,12 @@
|
||||||
<record model="ir.ui.view" id="product_view_inherit_list_price_auto">
|
<record model="ir.ui.view" id="product_view_inherit_list_price_auto">
|
||||||
<field name="name">product.list.price.automatic.form</field>
|
<field name="name">product.list.price.automatic.form</field>
|
||||||
<field name="model">product.template</field>
|
<field name="model">product.template</field>
|
||||||
<field name="type">form</field>
|
|
||||||
<field name="inherit_id" ref="product.product_template_form_view" />
|
<field name="inherit_id" ref="product.product_template_form_view" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<field name="standard_price" position="attributes">
|
<field name="standard_price" position="attributes">
|
||||||
<attribute name="attrs">
|
<attribute name="readonly">last_purchase_price_compute_type != 'manual_update'</attribute>
|
||||||
{'readonly':[('last_purchase_price_compute_type','!=','manual_update')]}
|
|
||||||
</attribute>
|
|
||||||
</field>
|
</field>
|
||||||
<field name="product_tag_ids" position="after">
|
<field name="list_price" position="after">
|
||||||
<separator string="Label Info" colspan="2"/>
|
<separator string="Label Info" colspan="2"/>
|
||||||
<field name="last_purchase_price_compute_type" />
|
<field name="last_purchase_price_compute_type" />
|
||||||
<field name="last_purchase_price_received"
|
<field name="last_purchase_price_received"
|
||||||
|
|
@ -46,16 +43,13 @@
|
||||||
<field name="priority" eval="100" />
|
<field name="priority" eval="100" />
|
||||||
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
<field name="inherit_id" ref="sale.res_config_settings_view_form" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//div[@id='pricing_setting_container']" position="inside">
|
<xpath expr="//block[@id='pricing_setting_container']" position="inside">
|
||||||
<div
|
<setting name="supermarket_settings_container">
|
||||||
class="col-12 col-lg-6 o_setting_box"
|
|
||||||
name="supermarket_settigs_container"
|
|
||||||
>
|
|
||||||
<div class="o_setting_right_pane">
|
<div class="o_setting_right_pane">
|
||||||
<label for="product_pricelist_automatic" string="Sale Price Pricelist" />
|
<label for="product_pricelist_automatic" string="Sale Price Pricelist" />
|
||||||
<field name="product_pricelist_automatic" />
|
<field name="product_pricelist_automatic" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</setting>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue