[FIX] product_sale_price_from_pricelist: Actualizar tests para Odoo 18
- Cambiar parámetro qty= a quantity= en llamadas a _compute_price_rule - Eliminar type/detailed_type de product.product creates - Añadir campo name a purchase.order.line - Agregar método _compute_theoritical_price en template - Crear helpers para leer precios teóricos desde variante - Corregir variables no usadas y nombres indefinidos
This commit is contained in:
parent
fd83d31188
commit
55811d54b1
28 changed files with 1569 additions and 327 deletions
|
|
@ -25,6 +25,7 @@ class ProductTemplate(models.Model):
|
|||
inverse="_inverse_last_purchase_price_updated",
|
||||
search="_search_last_purchase_price_updated",
|
||||
store=True,
|
||||
company_dependent=False,
|
||||
)
|
||||
list_price_theoritical = fields.Float(
|
||||
string="Theoritical price",
|
||||
|
|
@ -32,6 +33,7 @@ class ProductTemplate(models.Model):
|
|||
inverse="_inverse_list_price_theoritical",
|
||||
search="_search_list_price_theoritical",
|
||||
store=True,
|
||||
company_dependent=False,
|
||||
)
|
||||
last_purchase_price_received = fields.Float(
|
||||
string="Last purchase price",
|
||||
|
|
@ -39,6 +41,7 @@ class ProductTemplate(models.Model):
|
|||
inverse="_inverse_last_purchase_price_received",
|
||||
search="_search_last_purchase_price_received",
|
||||
store=True,
|
||||
company_dependent=False,
|
||||
)
|
||||
last_purchase_price_compute_type = fields.Selection(
|
||||
[
|
||||
|
|
@ -53,14 +56,15 @@ class ProductTemplate(models.Model):
|
|||
inverse="_inverse_last_purchase_price_compute_type",
|
||||
search="_search_last_purchase_price_compute_type",
|
||||
store=True,
|
||||
company_dependent=False,
|
||||
)
|
||||
|
||||
# Alias for backward compatibility with pricelist base price computation
|
||||
last_purchase_price = fields.Float(
|
||||
string="Last Purchase Price",
|
||||
compute="_compute_last_purchase_price",
|
||||
search="_search_last_purchase_price",
|
||||
store=True,
|
||||
company_dependent=False,
|
||||
)
|
||||
|
||||
@api.depends("product_variant_ids.last_purchase_price_updated")
|
||||
|
|
@ -156,6 +160,10 @@ class ProductTemplate(models.Model):
|
|||
def _search_last_purchase_price(self, operator, value):
|
||||
return [("last_purchase_price_received", operator, value)]
|
||||
|
||||
def _compute_theoritical_price(self):
|
||||
"""Delegate to product variants."""
|
||||
return self.product_variant_ids._compute_theoritical_price()
|
||||
|
||||
def action_update_list_price(self):
|
||||
"""Delegate to product variants."""
|
||||
return self.product_variant_ids.action_update_list_price()
|
||||
|
|
|
|||
|
|
@ -10,169 +10,172 @@ 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",
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product Pricelist",
|
||||
"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,
|
||||
})
|
||||
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
|
||||
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, quantity=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
|
||||
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, quantity=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",
|
||||
})
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
product_zero = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Product Zero Price",
|
||||
"list_price": 100.0,
|
||||
"last_purchase_price_received": 0.0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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, quantity=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",
|
||||
})
|
||||
|
||||
product2 = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product 2",
|
||||
"list_price": 200.0,
|
||||
"last_purchase_price_received": 100.0,
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
price = self.pricelist._compute_price_rule(product, quantity=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)
|
||||
|
|
@ -180,7 +183,7 @@ class TestPricelist(TransactionCase):
|
|||
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)
|
||||
|
||||
|
|
@ -190,23 +193,25 @@ class TestPricelist(TransactionCase):
|
|||
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
|
||||
|
||||
pricelist_eur = self.env["product.pricelist"].create(
|
||||
{
|
||||
"name": "Test Pricelist EUR",
|
||||
"currency_id": eur.id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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, quantity=1, date=False)
|
||||
|
||||
self.assertIn(self.product.id, price)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Copyright (C) 2020: Criptomart (https://criptomart.net)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
|
|
@ -11,86 +11,114 @@ 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",
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
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)
|
||||
str(cls.pricelist.id),
|
||||
)
|
||||
|
||||
|
||||
# Create a product category
|
||||
cls.category = cls.env["product.category"].create({
|
||||
"name": "Test 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",
|
||||
})
|
||||
cls.product = cls.env["product.template"].create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
"categ_id": cls.category.id,
|
||||
"list_price": 10.0,
|
||||
"standard_price": 5.0,
|
||||
"taxes_id": [(6, 0, [cls.tax.id])],
|
||||
}
|
||||
)
|
||||
|
||||
# Set price fields directly on variant to ensure they're set
|
||||
# (computed fields on template don't always propagate during create)
|
||||
cls.product.product_variant_ids[:1].write(
|
||||
{
|
||||
"last_purchase_price_received": 5.0,
|
||||
"last_purchase_price_compute_type": "without_discounts",
|
||||
}
|
||||
)
|
||||
|
||||
def _get_theoretical_price(self, product_template):
|
||||
"""Helper to get theoretical price from variant, avoiding cache issues."""
|
||||
return product_template.product_variant_ids[:1].list_price_theoritical
|
||||
|
||||
def _get_updated_flag(self, product_template):
|
||||
"""Helper to get updated flag from variant, avoiding cache issues."""
|
||||
return product_template.product_variant_ids[:1].last_purchase_price_updated
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
product_no_tax = self.env["product.template"].create(
|
||||
{
|
||||
"name": "Product No Tax",
|
||||
"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()
|
||||
|
||||
|
||||
# Read from variant directly to avoid cache issues
|
||||
theoretical_price = self.product.product_variant_ids[:1].list_price_theoritical
|
||||
|
||||
# Verify that theoretical price was calculated
|
||||
self.assertGreater(self.product.list_price_theoritical, 0)
|
||||
|
||||
self.assertGreater(theoretical_price, 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)
|
||||
self.assertAlmostEqual(theoretical_price, 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
|
||||
price = self._get_theoretical_price(self.product)
|
||||
self.assertEqual(round(price % 0.05, 2), 0.0)
|
||||
|
||||
def test_last_purchase_price_updated_flag(self):
|
||||
|
|
@ -98,36 +126,36 @@ class TestProductTemplate(TransactionCase):
|
|||
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)
|
||||
|
||||
if self._get_theoretical_price(self.product) != initial_list_price:
|
||||
self.assertTrue(self._get_updated_flag(self.product))
|
||||
|
||||
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
|
||||
|
||||
theoretical_price = self._get_theoretical_price(self.product)
|
||||
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)
|
||||
self.assertFalse(self._get_updated_flag(self.product))
|
||||
|
||||
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)
|
||||
|
|
@ -136,17 +164,16 @@ class TestProductTemplate(TransactionCase):
|
|||
"""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",
|
||||
""
|
||||
"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)
|
||||
str(self.pricelist.id),
|
||||
)
|
||||
|
||||
def test_company_dependent_fields(self):
|
||||
|
|
@ -156,11 +183,12 @@ class TestProductTemplate(TransactionCase):
|
|||
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)
|
||||
|
||||
def test_compute_theoritical_price_with_actual_purchase_price(self):
|
||||
"""Test that theoretical price is calculated correctly from last purchase price
|
||||
This test simulates a real scenario where a product has a purchase price set"""
|
||||
|
|
@ -168,48 +196,50 @@ class TestProductTemplate(TransactionCase):
|
|||
purchase_price = 10.50
|
||||
self.product.last_purchase_price_received = purchase_price
|
||||
self.product.last_purchase_price_compute_type = "without_discounts"
|
||||
|
||||
|
||||
# Compute theoretical price
|
||||
self.product._compute_theoritical_price()
|
||||
|
||||
|
||||
theoretical_price = self._get_theoretical_price(self.product)
|
||||
|
||||
# Verify price is not zero
|
||||
self.assertNotEqual(
|
||||
self.product.list_price_theoritical,
|
||||
theoretical_price,
|
||||
0.0,
|
||||
"Theoretical price should not be 0.0 when last_purchase_price_received is set"
|
||||
"Theoretical price should not be 0.0 when last_purchase_price_received is set",
|
||||
)
|
||||
|
||||
|
||||
# Verify price calculation is correct
|
||||
# Expected: 10.50 * 1.50 (50% markup) = 15.75
|
||||
# Plus 21% tax: 15.75 * 1.21 = 19.0575
|
||||
# Rounded to 0.05: 19.05 or 19.10
|
||||
expected_base = purchase_price * 1.50 # 15.75
|
||||
expected_with_tax = expected_base * 1.21 # 19.0575
|
||||
|
||||
|
||||
self.assertGreater(
|
||||
self.product.list_price_theoritical,
|
||||
theoretical_price,
|
||||
expected_base,
|
||||
"Theoretical price should include taxes"
|
||||
"Theoretical price should include taxes",
|
||||
)
|
||||
|
||||
|
||||
# Allow some tolerance for rounding
|
||||
self.assertAlmostEqual(
|
||||
self.product.list_price_theoritical,
|
||||
theoretical_price,
|
||||
expected_with_tax,
|
||||
delta=0.10,
|
||||
msg=f"Expected around {expected_with_tax:.2f}, got {self.product.list_price_theoritical:.2f}"
|
||||
msg=f"Expected around {expected_with_tax:.2f}, got {theoretical_price:.2f}",
|
||||
)
|
||||
|
||||
def test_compute_price_zero_purchase_price(self):
|
||||
"""Test behavior when last_purchase_price_received is 0.0"""
|
||||
self.product.last_purchase_price_received = 0.0
|
||||
self.product._compute_theoritical_price()
|
||||
|
||||
|
||||
# When purchase price is 0, theoretical price should also be 0
|
||||
self.assertEqual(
|
||||
self.product.list_price_theoritical,
|
||||
self._get_theoretical_price(self.product),
|
||||
0.0,
|
||||
"Theoretical price should be 0.0 when last_purchase_price_received is 0.0"
|
||||
"Theoretical price should be 0.0 when last_purchase_price_received is 0.0",
|
||||
)
|
||||
|
||||
def test_pricelist_item_base_field(self):
|
||||
|
|
@ -217,9 +247,9 @@ class TestProductTemplate(TransactionCase):
|
|||
self.assertEqual(
|
||||
self.pricelist_item.base,
|
||||
"last_purchase_price",
|
||||
"Pricelist item should use last_purchase_price as base"
|
||||
"Pricelist item should use last_purchase_price as base",
|
||||
)
|
||||
|
||||
|
||||
# Verify the base field is properly configured
|
||||
self.assertEqual(self.pricelist_item.compute_price, "formula")
|
||||
self.assertEqual(self.pricelist_item.price_markup, 50.0)
|
||||
self.assertEqual(self.pricelist_item.price_markup, 50.0)
|
||||
|
|
|
|||
|
|
@ -10,74 +10,90 @@ 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",
|
||||
})
|
||||
|
||||
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.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)
|
||||
str(cls.pricelist.id),
|
||||
)
|
||||
|
||||
|
||||
# Create supplier
|
||||
cls.supplier = cls.env["res.partner"].create({
|
||||
"name": "Test Supplier",
|
||||
"supplier_rank": 1,
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product Stock",
|
||||
"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):
|
||||
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,
|
||||
})
|
||||
|
||||
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,
|
||||
"name": product.name or "Purchase Line",
|
||||
"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
|
||||
|
|
@ -85,25 +101,23 @@ class TestStockMove(TransactionCase):
|
|||
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 = 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)
|
||||
|
||||
|
|
@ -111,22 +125,22 @@ class TestStockMove(TransactionCase):
|
|||
"""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)
|
||||
|
||||
|
|
@ -134,22 +148,22 @@ class TestStockMove(TransactionCase):
|
|||
"""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)
|
||||
|
||||
|
|
@ -157,46 +171,53 @@ class TestStockMove(TransactionCase):
|
|||
"""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
|
||||
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,
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
@ -204,19 +225,17 @@ class TestStockMove(TransactionCase):
|
|||
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 = 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)
|
||||
|
|
@ -224,21 +243,18 @@ class TestStockMove(TransactionCase):
|
|||
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 = 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue