[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:
snt 2026-02-12 19:23:29 +01:00
parent fd83d31188
commit 55811d54b1
28 changed files with 1569 additions and 327 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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