mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-25 01:12:04 +02:00
466 lines
21 KiB
Python
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)
|