oca-ocb-sale/odoo-bringout-oca-ocb-product/product/tests/test_pricelist.py
Ernad Husremovic 73afc09215 19.0 vanilla
2026-03-09 09:32:12 +01:00

466 lines
21 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from itertools import pairwise
from unittest.mock import patch
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command, Domain
from odoo.tests import Form, tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
@tagged('post_install', '-at_install')
class TestPricelist(ProductVariantsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.datacard = cls.env['product.product'].create({'name': 'Office Lamp'})
cls.usb_adapter = cls.env['product.product'].create({'name': 'Office Chair'})
cls.sale_pricelist_id, cls.pricelist_eu = cls.env['product.pricelist'].create([{
'name': 'Sale pricelist',
'item_ids': [
Command.create({
'compute_price': 'formula',
'base': 'list_price', # based on public price
'price_discount': 10,
'product_id': cls.usb_adapter.id,
'applied_on': '0_product_variant',
}),
Command.create({
'compute_price': 'formula',
'base': 'list_price', # based on public price
'price_surcharge': -0.5,
'product_id': cls.datacard.id,
'applied_on': '0_product_variant',
}),
Command.create({
'compute_price': 'formula',
'base': 'standard_price', # based on cost
'price_markup': 99.99,
'applied_on': '3_global',
}),
],
}, {
'name': "EU Pricelist",
'country_group_ids': cls.env.ref('base.europe').ids,
}])
# Enable pricelist feature
cls.env.user.group_ids += cls.env.ref('product.group_product_pricelist')
cls.uom_ton = cls.env.ref('uom.product_uom_ton')
def test_10_discount(self):
# Make sure the price using a pricelist is the same than without after
# applying the computation manually
self.assertEqual(
self.pricelist._get_product_price(self.usb_adapter, 1.0)*0.9,
self.sale_pricelist_id._get_product_price(self.usb_adapter, 1.0))
self.assertEqual(
self.pricelist._get_product_price(self.datacard, 1.0)-0.5,
self.sale_pricelist_id._get_product_price(self.datacard, 1.0))
self.assertAlmostEqual(
self.sale_pricelist_id._get_product_price(self.usb_adapter, 1.0, uom=self.uom_unit)*12,
self.sale_pricelist_id._get_product_price(self.usb_adapter, 1.0, uom=self.uom_dozen))
# price_surcharge applies to product default UoM, here "Units", so surcharge will be multiplied
self.assertAlmostEqual(
self.sale_pricelist_id._get_product_price(self.datacard, 1.0, uom=self.uom_unit)*12,
self.sale_pricelist_id._get_product_price(self.datacard, 1.0, uom=self.uom_dozen))
def test_11_markup(self):
"""Ensure `price_markup` always equals negative `price_discount`."""
# Check create values
for item in self.sale_pricelist_id.item_ids:
self.assertEqual(item.price_markup, -item.price_discount)
# Overwrite create values, and check again
self.sale_pricelist_id.item_ids[0].price_discount = 0
self.sale_pricelist_id.item_ids[1].price_discount = -20.02
self.sale_pricelist_id.item_ids[2].price_markup = -0.5
for item in self.sale_pricelist_id.item_ids:
self.assertEqual(item.price_markup, -item.price_discount)
def test_20_pricelist_uom(self):
# Verify that the pricelist rules are correctly using the product's default UoM
# as reference, and return a result according to the target UoM (as specific in the context)
tonne_price = 100
# setup product stored in 'tonnes', with a discounted pricelist for qty > 3 tonnes
spam = self.env['product.product'].create({
'name': '1 tonne of spam',
'uom_id': self.uom_ton.id,
'list_price': tonne_price,
'type': 'consu'
})
self.env['product.pricelist.item'].create({
'pricelist_id': self.pricelist.id,
'applied_on': '0_product_variant',
'compute_price': 'formula',
'base': 'list_price', # based on public price
'min_quantity': 3, # min = 3 tonnes
'price_surcharge': -10, # -10 EUR / tonne
'product_id': spam.id
})
def test_unit_price(qty, uom_id, expected_unit_price):
uom = self.env['uom.uom'].browse(uom_id)
unit_price = self.pricelist._get_product_price(spam, qty, uom=uom)
self.assertAlmostEqual(unit_price, expected_unit_price, msg='Computed unit price is wrong')
# Test prices - they are *per unit*, the quantity is only here to match the pricelist rules!
test_unit_price(2, self.uom_kgm.id, tonne_price / 1000.0)
test_unit_price(2000, self.uom_kgm.id, tonne_price / 1000.0)
test_unit_price(3500, self.uom_kgm.id, (tonne_price - 10) / 1000.0)
test_unit_price(2, self.uom_ton.id, tonne_price)
test_unit_price(3, self.uom_ton.id, tonne_price - 10)
def test_30_pricelists_order(self):
# Verify the order of pricelists after creation
ProductPricelist = self.env['product.pricelist']
res_partner = self.env['res.partner'].create({'name': 'Ready Corner'})
ProductPricelist.search([]).active = False
pl_first = ProductPricelist.create({'name': 'First Pricelist'})
res_partner.invalidate_recordset(['property_product_pricelist'])
self.assertEqual(res_partner.property_product_pricelist, pl_first)
ProductPricelist.create({'name': 'Second Pricelist'})
res_partner.invalidate_recordset(['property_product_pricelist'])
self.assertEqual(res_partner.property_product_pricelist, pl_first)
def test_40_specific_property_product_pricelist(self):
"""Ensure that that ``specific_property_product_pricelist`` value only gets set
when changing ``property_product_pricelist`` to a non-default value for the partner.
"""
pricelist_1, pricelist_2 = self.pricelist, self.sale_pricelist_id
self.env['product.pricelist'].search([
('id', 'not in', [pricelist_1.id, pricelist_2.id, self.pricelist_eu.id]),
]).active = False
# Set country to BE -> property defaults to EU pricelist
with Form(self.partner) as partner_form:
partner_form.country_id = self.env.ref('base.be')
self.assertEqual(self.partner.property_product_pricelist, self.pricelist_eu)
self.assertFalse(self.partner.specific_property_product_pricelist)
# Set country to KI -> property defaults to highest sequence pricelist
with Form(self.partner) as partner_form:
partner_form.country_id = self.env.ref('base.ki')
self.assertEqual(self.partner.property_product_pricelist, pricelist_1)
self.assertFalse(self.partner.specific_property_product_pricelist)
# Setting non-default pricelist as property should update specific property
with Form(self.partner) as partner_form:
partner_form.property_product_pricelist = pricelist_2
self.assertEqual(self.partner.property_product_pricelist, pricelist_2)
self.assertEqual(self.partner.specific_property_product_pricelist, pricelist_2)
# Changing partner country shouldn't update (specific) pricelist property
with Form(self.partner) as partner_form:
partner_form.country_id = self.env.ref('base.be')
self.assertEqual(self.partner.property_product_pricelist, pricelist_2)
self.assertEqual(self.partner.specific_property_product_pricelist, pricelist_2)
def test_45_property_product_pricelist_config_parameter(self):
"""Check that the ``ir.config_parameter`` gets utilized as fallback to both
``property_product_pricelist`` & ``specific_property_product_pricelist``.
"""
pricelist_1, pricelist_2 = self.pricelist, self.sale_pricelist_id
self.env['product.pricelist'].search([
('id', 'not in', [pricelist_1.id, pricelist_2.id]),
]).active = False
self.assertEqual(self.partner.property_product_pricelist, pricelist_1)
self.partner.invalidate_recordset(['property_product_pricelist'])
ICP = self.env['ir.config_parameter'].sudo()
ICP.set_param('res.partner.property_product_pricelist', pricelist_2.id)
with patch.object(
self.pricelist.__class__,
'_get_partner_pricelist_multi_search_domain_hook',
return_value=Domain.FALSE, # ensures pricelist falls back on ICP
):
with Form(self.partner) as partner_form:
self.assertEqual(partner_form.property_product_pricelist, pricelist_2)
partner_form.property_product_pricelist = pricelist_1
self.assertEqual(self.partner.property_product_pricelist, pricelist_1)
self.assertEqual(self.partner.specific_property_product_pricelist, pricelist_1)
def test_pricelists_multi_comp_checks(self):
first_company = self.env.company
second_company = self.env['res.company'].create({'name': 'Test Company'})
shared_pricelist = self.env['product.pricelist'].create({
'name': 'Test Multi-comp pricelist',
'company_id': False,
})
second_pricelist = self.env['product.pricelist'].create({
'name': f'Second test pricelist{first_company.name}',
})
self.assertEqual(self.pricelist.company_id, first_company)
self.assertFalse(shared_pricelist.company_id)
self.assertEqual(second_pricelist.company_id, first_company)
with self.assertRaises(UserError):
shared_pricelist.item_ids = [
Command.create({
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': self.pricelist.id,
})
]
self.pricelist.item_ids = [
Command.create({
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': shared_pricelist.id,
}),
Command.create({
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': second_pricelist.id,
})
]
with self.assertRaises(UserError):
# Should raise because the pricelist would have a rule based on a pricelist
# from another company
self.pricelist.company_id = second_company
def test_pricelists_res_partner_form(self):
pricelist_europe = self.pricelist_eu
default_pricelist = self.env['product.pricelist'].search([('name', 'ilike', ' ')], limit=1)
with Form(self.env['res.partner']) as partner_form:
partner_form.name = "test"
self.assertEqual(partner_form.property_product_pricelist, default_pricelist)
partner_form.country_id = self.env.ref('base.be')
self.assertEqual(partner_form.property_product_pricelist, pricelist_europe)
partner_form.property_product_pricelist = self.sale_pricelist_id
self.assertEqual(partner_form.property_product_pricelist, self.sale_pricelist_id)
partner = partner_form.save()
with Form(partner) as partner_form:
self.assertEqual(partner_form.property_product_pricelist, self.sale_pricelist_id)
def test_pricelist_change_to_formula_and_back(self):
pricelist_2 = self.env['product.pricelist'].create({
'name': 'Sale pricelist 2',
'item_ids': [
Command.create({
'compute_price': 'percentage',
'percent_price': 20,
'base': 'pricelist',
'base_pricelist_id': self.sale_pricelist_id.id,
'applied_on': '3_global',
}),
],
})
with Form(pricelist_2.item_ids) as item_form:
item_form.compute_price = 'formula'
item_form.compute_price = 'percentage'
item_form.percent_price = 20
self.assertFalse(pricelist_2.item_ids.base_pricelist_id.id)
def test_sync_parent_pricelist(self):
"""Check that adding a parent to a partner updates the partner's pricelist."""
self.partner.update({
'parent_id': False,
'specific_property_product_pricelist': self.sale_pricelist_id.id,
})
self.assertEqual(self.partner.property_product_pricelist, self.sale_pricelist_id)
company_2 = self.env.company.create({'name': "Company Two"})
company_1_b2b_pl, company_2_b2b_pl = self.sale_pricelist_id.create([{
'name': f"B2B ({company.name})",
'company_id': company.id,
} for company in self.env.company + company_2])
parent = self.partner.create({
'name': f"{self.partner.name}'s Company",
'is_company': True,
'specific_property_product_pricelist': company_1_b2b_pl.id,
})
parent.with_company(company_2).specific_property_product_pricelist = company_2_b2b_pl
self.partner.parent_id = parent
self.assertEqual(
self.partner.specific_property_product_pricelist,
company_1_b2b_pl,
"Assigning a parent with a specific pricelist should sync the parent's pricelist",
)
self.assertEqual(
self.partner.with_company(company_2).specific_property_product_pricelist,
company_2_b2b_pl,
"Company-specific pricelists should get synced on parent assignment",
)
parent.specific_property_product_pricelist = self.sale_pricelist_id
self.assertEqual(
self.partner.specific_property_product_pricelist,
self.sale_pricelist_id,
"Setting a specific parent pricelist should update the partner's pricelist",
)
self.assertEqual(
self.partner.with_company(company_2).specific_property_product_pricelist,
company_2_b2b_pl,
"Assigning pricelists in one company shouldn't impact pricelists in other companies",
)
def test_prevent_pricelist_recursion(self):
"""Ensure recursive pricelist rules raise an error on creation."""
def create_item_vals(pl_from, pl_to):
return {
'pricelist_id': pl_from.id,
'compute_price': 'formula',
'base': 'pricelist',
'base_pricelist_id': pl_to.id,
'applied_on': '3_global',
}
Pricelist = self.env['product.pricelist']
pl_a, pl_b, pl_c, pl_d = pricelists = Pricelist.create([{
'name': f"Pricelist {c}",
} for c in 'ABCD'])
# A -> B -> C -> D
Pricelist.item_ids.create([
create_item_vals(pl_from, pl_to)
for (pl_from, pl_to) in pairwise(pricelists)
])
with self.assertRaises(ValidationError):
# A -> B -> C -> D -> D -> _ (recurs)
Pricelist.item_ids.create(create_item_vals(pl_d, pl_d))
with self.assertRaises(ValidationError):
# A -> B -> C -> D -> A -> _ (recurs)
Pricelist.item_ids.create(create_item_vals(pl_d, pl_a))
with self.assertRaises(ValidationError):
# A -> B -> C -> [B -> _, D] (recurs)
Pricelist.item_ids.create(create_item_vals(pl_c, pl_b))
# A -> B, C -> D
pl_b.item_ids.unlink()
# C -> D -> A -> B
Pricelist.item_ids.create(create_item_vals(pl_d, pl_a))
# C -> [B, D -> A -> B]
Pricelist.item_ids.create(create_item_vals(pl_c, pl_b))
with self.assertRaises(ValidationError):
# C -> [B, D -> A -> [B, C -> _]] (recurs)
Pricelist.item_ids.create(create_item_vals(pl_a, pl_c))
with self.assertRaises(ValidationError):
# C -> [B -> D -> A -> B -> _, D -> _] (recurs)
Pricelist.item_ids.create(create_item_vals(pl_b, pl_d))
def test_pricelist_rule_linked_to_product_variant(self):
"""Verify that pricelist rules assigned to a variant remain linked after write."""
self.product_sofa_red.pricelist_rule_ids = [
Command.create({
'applied_on': '0_product_variant',
'product_id': self.product_sofa_red.id,
'compute_price': 'fixed',
'fixed_price': 99.9,
'pricelist_id': self.pricelist.id,
}),
Command.create({
'applied_on': '0_product_variant',
'product_id': self.product_sofa_red.id,
'compute_price': 'fixed',
'fixed_price': 89.9,
'pricelist_id': self.pricelist.id,
}),
]
self.assertEqual(len(self.product_sofa_red.pricelist_rule_ids), 2)
first_rule, second_rule = self.product_sofa_red.pricelist_rule_ids
self.product_sofa_red.pricelist_rule_ids = [
Command.update(first_rule.id, {'fixed_price': 79.9}),
Command.unlink(second_rule.id),
]
self.assertEqual(len(self.product_sofa_red.pricelist_rule_ids), 1)
self.assertEqual(self.pricelist.item_ids.fixed_price, 79.9)
self.assertIn(self.product_sofa_red, self.pricelist.item_ids.product_id)
# Update of template-based rules through variant form
self.product_template_sofa.pricelist_rule_ids = [
# Template-based rule (can be edited through the variants)
Command.create({
'applied_on': '1_product',
'product_tmpl_id': self.product_template_sofa.id,
'pricelist_id': self.pricelist.id,
}),
# Rule on another variant than the one being edited. It cannot be edited through the
# current variant and therefore shouldn't change when another variant rules are edited.
Command.create({
'applied_on': '0_product_variant',
'product_id': self.product_sofa_blue.id,
'compute_price': 'fixed',
'fixed_price': 89.9,
'pricelist_id': self.pricelist.id,
})
]
self.assertEqual(len(self.product_template_sofa.pricelist_rule_ids), 3)
template_rule = self.product_template_sofa.pricelist_rule_ids.filtered(
lambda item: not item.product_id
)
self.assertEqual(len(self.product_sofa_red.pricelist_rule_ids), 2)
self.product_sofa_red.pricelist_rule_ids = [
Command.update(template_rule.id, {'fixed_price': 133}),
]
self.assertEqual(template_rule.fixed_price, 133)
self.product_sofa_red.pricelist_rule_ids = [
Command.unlink(template_rule.id),
]
self.assertFalse(template_rule.exists())
self.assertTrue(self.product_sofa_blue.pricelist_rule_ids)
self.assertEqual(len(self.product_template_sofa.pricelist_rule_ids), 2)
def test_pricelist_applied_on_product_variant(self):
# product template with variants
sofa_1 = self.product_template_sofa.product_variant_ids[0]
# create pricelist with rule on template
pricelist = self.env["product.pricelist"].create(
{
"name": "Pricelist for Acoustic Bloc Screens",
"item_ids": [
Command.create(
{
"compute_price": "fixed",
"fixed_price": 123,
"base": "list_price",
"applied_on": "1_product",
"product_tmpl_id": self.product_template_sofa.id,
}
),
],
}
)
# open rule form and change rule to apply on variant instead of template
with Form(pricelist.item_ids) as item_form:
item_form.product_id = sofa_1
# check that `applied_on` changed to variant
self.assertEqual(pricelist.item_ids.applied_on, "0_product_variant")
# re-edit rule to apply on template again by clearing `product_id`
with Form(pricelist.item_ids) as item_form:
item_form.product_id = self.env["product.product"]
# check that `applied_on` changed to template
self.assertEqual(pricelist.item_ids.applied_on, "1_product")
# check that product_id is cleared
self.assertFalse(pricelist.item_ids.product_id)