19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,11 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_barcode
from . import test_common
from . import test_import_files
from . import test_name
from . import test_pricelist
from . import test_pricelist_auto_creation
from . import test_product_attribute_value_config
from . import test_product_combo
from . import test_product_pricelist
from . import test_seller
from . import test_update_pav_wizard
from . import test_variants
from . import test_product_rounding

View file

@ -1,66 +1,75 @@
# -*- coding: utf-8 -*-
from contextlib import nullcontext
from unittest.mock import patch
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.addons.base.tests.common import BaseCommon
from odoo.addons.uom.tests.common import UomCommon
class ProductCommon(
BaseCommon, # enforce constant test currency (USD)
UomCommon,
):
class ProductCommon(UomCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Ideally, this logic should be moved into sthg like a NoAccountCommon in account :D
# Since tax fields are specified in account module, cannot be given as create values
NO_TAXES_CONTEXT = {
'default_taxes_id': False
}
cls.group_product_pricelist = cls.quick_ref('product.group_product_pricelist')
cls.group_product_variant = cls.quick_ref('product.group_product_variant')
cls.product_category = cls.env['product.category'].create({
'name': 'Test Category',
})
cls.product = cls.env['product.product'].with_context(**NO_TAXES_CONTEXT).create({
cls.product, cls.service_product = cls.env['product.product'].create([{
'name': 'Test Product',
'detailed_type': 'consu',
'type': 'consu',
'list_price': 20.0,
'categ_id': cls.product_category.id,
})
cls.service_product = cls.env['product.product'].with_context(**NO_TAXES_CONTEXT).create({
}, {
'name': 'Test Service Product',
'detailed_type': 'service',
'type': 'service',
'list_price': 50.0,
'categ_id': cls.product_category.id,
})
cls.consumable_product = cls.product
}])
cls.pricelist = cls.env['product.pricelist'].create({
'name': 'Test Pricelist',
})
cls._archive_other_pricelists()
# Archive all existing pricelists
cls.env['product.pricelist'].search([
('id', '!=', cls.pricelist.id),
]).action_archive()
@classmethod
def _archive_other_pricelists(cls):
"""Do not raise if there is no pricelist(s) for a given website"""
website_sale = cls.env['ir.module.module']._get('website_sale')
if website_sale.state == 'installed':
archive_context = patch('odoo.addons.website_sale.models.product_pricelist.ProductPricelist._check_website_pricelist')
else:
archive_context = nullcontext()
def get_default_groups(cls):
groups = super().get_default_groups()
return groups | cls.quick_ref('product.group_product_manager')
with archive_context:
cls.env['product.pricelist'].search([
('id', '!=', cls.pricelist.id),
]).action_archive()
@classmethod
def _enable_pricelists(cls):
cls.env.user.group_ids += cls.group_product_pricelist
@classmethod
def _enable_variants(cls):
cls.env.user.group_ids += cls.group_product_variant
@classmethod
def _create_pricelist(cls, **create_vals):
return cls.env['product.pricelist'].create({
'name': "Test Pricelist",
**create_vals,
})
@classmethod
def _create_product(cls, **create_vals):
return cls.env['product.product'].create({
'name': "Test Product",
'type': 'consu',
'list_price': 100.0,
'standard_price': 50.0,
'uom_id': cls.uom_unit.id,
'categ_id': cls.product_category.id,
**create_vals,
})
class ProductAttributesCommon(ProductCommon):
class ProductVariantsCommon(ProductCommon):
@classmethod
def setUpClass(cls):
@ -94,17 +103,31 @@ class ProductAttributesCommon(ProductCommon):
cls.color_attribute_green,
) = cls.color_attribute.value_ids
cls.no_variant_attribute = cls.env['product.attribute'].create({
'name': 'No variant',
'create_variant': 'no_variant',
'value_ids': [
Command.create({'name': 'extra'}),
Command.create({'name': 'second'}),
]
})
(
cls.no_variant_attribute_extra,
cls.no_variant_attribute_second,
) = cls.no_variant_attribute.value_ids
class ProductVariantsCommon(ProductAttributesCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.dynamic_attribute = cls.env['product.attribute'].create({
'name': 'Dynamic',
'create_variant': 'dynamic',
'value_ids': [
Command.create({'name': 'dyn1'}),
Command.create({'name': 'dyn2'}),
]
})
cls.product_template_sofa = cls.env['product.template'].create({
'name': 'Sofa',
'uom_id': cls.uom_unit.id,
'uom_po_id': cls.uom_unit.id,
'categ_id': cls.product_category.id,
'attribute_line_ids': [Command.create({
'attribute_id': cls.color_attribute.id,
@ -116,62 +139,21 @@ class ProductVariantsCommon(ProductAttributesCommon):
})]
})
cls.product_template_shirt = cls.env['product.template'].create({
'name': 'Shirt',
'categ_id': cls.product_category.id,
'attribute_line_ids': [
Command.create({
'attribute_id': cls.size_attribute.id,
'value_ids': [Command.set([cls.size_attribute_l.id])],
}),
],
})
class TestProductCommon(ProductVariantsCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Product environment related data
cls.uom_dunit = cls.env['uom.uom'].create({
'name': 'DeciUnit',
'category_id': cls.uom_unit.category_id.id,
'factor_inv': 0.1,
'factor': 10.0,
'uom_type': 'smaller',
'rounding': 0.001,
})
cls.product_1, cls.product_2 = cls.env['product.product'].create([{
'name': 'Courage', # product_1
'type': 'consu',
'default_code': 'PROD-1',
'uom_id': cls.uom_dunit.id,
'uom_po_id': cls.uom_dunit.id,
}, {
'name': 'Wood', # product_2
}])
# Kept for reduced diff in other modules (mainly stock & mrp)
cls.prod_att_1 = cls.color_attribute
cls.prod_attr1_v1 = cls.color_attribute_red
cls.prod_attr1_v2 = cls.color_attribute_blue
cls.prod_attr1_v3 = cls.color_attribute_green
cls.product_7_template = cls.product_template_sofa
cls.product_7_attr1_v1 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[0]
cls.product_7_attr1_v2 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[1]
cls.product_7_attr1_v3 = cls.product_7_template.attribute_line_ids[
0].product_template_value_ids[2]
cls.product_7_1 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v1)
cls.product_7_2 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v2)
cls.product_7_3 = cls.product_7_template._get_variant_for_combination(
cls.product_7_attr1_v3)
cls.product_sofa_red = cls.product_template_sofa.product_variant_ids.filtered(
lambda pp:
pp.product_template_attribute_value_ids.product_attribute_value_id
==
cls.color_attribute_red
)
cls.product_sofa_blue = cls.product_template_sofa.product_variant_ids.filtered(
lambda pp:
pp.product_template_attribute_value_ids.product_attribute_value_id
==
cls.color_attribute_blue
)
cls.product_sofa_green = cls.product_template_sofa.product_variant_ids.filtered(
lambda pp:
pp.product_template_attribute_value_ids.product_attribute_value_id
==
cls.color_attribute_green
)

View file

@ -2,7 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import ValidationError
from odoo.tests import tagged, TransactionCase
from odoo.fields import Command
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
@ -16,6 +17,26 @@ class TestProductBarcode(TransactionCase):
{'name': 'BC2', 'barcode': '2'},
])
cls.size_attribute = cls.env['product.attribute'].create({
'name': 'Size',
'value_ids': [
Command.create({'name': 'SMALL'}),
Command.create({'name': 'LARGE'}),
]
})
cls.size_attribute_s, cls.size_attribute_l = cls.size_attribute.value_ids
cls.template = cls.env['product.template'].create({'name': 'template'})
cls.template.write({
'attribute_line_ids': [Command.create({
'attribute_id': cls.size_attribute.id,
'value_ids': [
Command.link(cls.size_attribute_s.id),
Command.link(cls.size_attribute_l.id),
],
})]
})
def test_blank_barcodes_allowed(self):
"""Makes sure duplicated blank barcodes are allowed."""
for i in range(2):
@ -54,40 +75,100 @@ class TestProductBarcode(TransactionCase):
try:
self.env['product.product'].create(batch)
except ValidationError as exc:
assert 'Barcode "3" already assigned to product(s): BC3, BC4' in exc.args[0]
assert 'Barcode "4" already assigned to product(s): BC5, BC6' in exc.args[0]
assert 'Barcode "3" already assigned to product(s): BC3 and BC4' in exc.args[0]
assert 'Barcode "4" already assigned to product(s): BC5 and BC6' in exc.args[0]
assert 'Barcode "1" already assigned to product(s): BC1' in exc.args[0]
def test_delete_package_and_use_its_barcode_in_product(self):
""" Test that the barcode of the package can be used when the package is removed from the product."""
def test_delete_packaging_and_use_its_barcode_in_product(self):
""" Test that the barcode of the packaging can be used when the packaging is removed from the product."""
pack_uom = self.env['uom.uom'].create({
'name': 'Pack of 10',
'relative_factor': 10,
'relative_uom_id': self.env.ref('uom.product_uom_unit').id,
})
product = self.env['product.product'].create({
'name': 'product',
'packaging_ids': [(0, 0, {
'name': 'packing',
'barcode': '1234',
})]
'uom_ids': [Command.link(pack_uom.id)],
})
package = product.packaging_ids
self.assertTrue(package.exists())
self.assertEqual(package.barcode, '1234')
product.packaging_ids = False
packaging_barcode = self.env['product.uom'].create({
'barcode': '1234',
'product_id': product.id,
'uom_id': pack_uom.id,
})
packaging = product.product_uom_ids
self.assertTrue(packaging.exists())
self.assertEqual(packaging.barcode, '1234')
packaging_barcode.unlink()
self.assertFalse(packaging.exists())
product.barcode = '1234'
def test_delete_product_and_reuse_barcode(self):
""" Test that the barcode of the package can be used when the package is removed from the product."""
product = self.env['product.product'].create({
'name': 'product',
'packaging_ids': [(0, 0, {
'name': 'packing',
'barcode': '1234',
})]
})
product.unlink()
def test_duplicated_barcodes_are_allowed_for_different_companies(self):
"""Barcode needs to be unique only withing the same company"""
company_a = self.env.company
company_b = self.env['res.company'].create({'name': 'CB'})
self.env['product.product'].create({
'name': 'product2',
'packaging_ids': [(0, 0, {
'name': 'packing2',
'barcode': '1234',
})]
allowed_products = [
# Allowed, barcode doesn't exist yet
{'name': 'A1', 'barcode': '3', 'company_id': company_a.id},
# Allowed, barcode exists (A1), but for a different company
{'name': 'A2', 'barcode': '3', 'company_id': company_b.id},
]
forbidden_products = [
# Forbidden, collides with BC1
{'name': 'F1', 'barcode': '1', 'company_id': False},
# Forbidden, collides with BC1
{'name': 'F2', 'barcode': '1', 'company_id': company_a.id},
# Forbidden, collides with BC2
{'name': 'F3', 'barcode': '2', 'company_id': company_b.id},
# Forbidden, collides with A1
{'name': 'F4', 'barcode': '3', 'company_id': company_a.id},
# Forbidden, collides with A2
{'name': 'F5', 'barcode': '3', 'company_id': company_b.id},
# Forbidden, collides with A1 and A2
{'name': 'F6', 'barcode': '3', 'company_id': False},
]
for product in allowed_products:
self.env['product.product'].create(product)
for product in forbidden_products:
with self.assertRaises(ValidationError):
self.env['product.product'].create(product)
def test_duplicated_barcodes_in_product_variants(self):
"""
Create 2 variants with different barcodes and same company.
Assign a duplicated barcode to one of them while changing the company.
Barcode validation should be triggered and a duplicated barcode should be detected.
"""
company_a = self.env.company
company_b = self.env['res.company'].create({'name': 'CB'})
variant_1 = self.template.product_variant_ids[0]
variant_2 = self.template.product_variant_ids[1]
variant_1.barcode = 'barcode_1'
variant_1.company_id = company_a
variant_2.barcode = 'barcode_2'
variant_2.company_id = company_a
with self.assertRaises(ValidationError):
variant_2.write({
'barcode': 'barcode_1',
'company_id': company_b
})
# Variant 1 was not updated
self.assertEqual(variant_2.barcode, 'barcode_2')
self.assertEqual(variant_2.company_id, company_a)
variant_2.write({
'barcode': 'barcode_3',
'company_id': company_b
})
self.assertEqual(variant_1.barcode, 'barcode_1')
self.assertEqual(variant_1.company_id, company_b)
self.assertEqual(variant_2.barcode, 'barcode_3')
self.assertEqual(variant_2.company_id, company_b)

View file

@ -10,14 +10,10 @@ from odoo.addons.product.tests.common import ProductCommon
class TestProduct(ProductCommon):
def test_common(self):
self.assertEqual(self.consumable_product.type, 'consu')
self._enable_pricelists()
self.assertEqual(self.product.type, 'consu')
self.assertEqual(self.service_product.type, 'service')
account_module = self.env['ir.module.module']._get('account')
if account_module.state == 'installed':
self.assertFalse(self.consumable_product.taxes_id)
self.assertFalse(self.service_product.taxes_id)
self.assertFalse(self.pricelist.item_ids)
self.assertEqual(
self.env['product.pricelist'].search([]),
@ -27,5 +23,4 @@ class TestProduct(ProductCommon):
self.env['res.partner'].search([]).property_product_pricelist,
self.pricelist,
)
self.assertEqual(self.pricelist.currency_id.name, 'USD')
self.assertEqual(self.pricelist.discount_policy, 'with_discount')
self.assertEqual(self.pricelist.currency_id.name, self.currency.name)

View file

@ -0,0 +1,63 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
from odoo.tests import TransactionCase, can_import, loaded_demo_data, tagged
from odoo.tools.misc import file_open
@tagged("post_install", "-at_install")
class TestImportFiles(TransactionCase):
@unittest.skipUnless(
can_import("xlrd.xlsx") or can_import("openpyxl"), "XLRD/XLSX not available"
)
def test_import_product_demo_xls(self):
if not loaded_demo_data(self.env):
self.skipTest('Needs demo data to be able to import those files')
for model in ("product.pricelist", "product.supplierinfo", "product.template"):
with self.subTest(model):
filename = f'{model.replace(".", "_")}.xls'
file_content = file_open(f"product/static/xls/{filename}", "rb").read()
import_wizard = self.env["base_import.import"].create(
{
"res_model": model,
"file": file_content,
"file_type": "application/vnd.ms-excel",
}
)
result = import_wizard.parse_preview(
{
"has_headers": True,
}
)
self.assertIsNone(result.get("error"))
field_names = ['/'.join(v) for v in result["matches"].values()]
results = import_wizard.execute_import(
field_names,
[r.lower() for r in result["headers"]],
{
"import_skip_records": [],
"import_set_empty_fields": [],
"fallback_values": {},
"name_create_enabled_fields": {},
"encoding": "",
"separator": "",
"quoting": '"',
"date_format": "",
"datetime_format": "",
"float_thousand_separator": ",",
"float_decimal_separator": ".",
"advanced": True,
"has_headers": True,
"keep_matches": False,
"limit": 2000,
"skip": 0,
"tracking_disable": True,
},
)
self.assertFalse(
results["messages"],
"results should be empty on successful import of ",
)

View file

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged, TransactionCase
from odoo.fields import Command
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
@ -32,7 +33,7 @@ class TestName(TransactionCase):
def test_product_template_search_name_no_product_product(self):
# To be able to test dynamic variant "variants" feature must be set up
self.env.user.write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]})
self.env.user.write({'group_ids': [(4, self.env.ref('product.group_product_variant').id)]})
color_attr = self.env['product.attribute'].create({'name': 'Color', 'create_variant': 'dynamic'})
color_attr_value_r = self.env['product.attribute.value'].create({'name': 'Red', 'attribute_id': color_attr.id})
color_attr_value_b = self.env['product.attribute.value'].create({'name': 'Blue', 'attribute_id': color_attr.id})
@ -56,3 +57,27 @@ class TestName(TransactionCase):
res_ids = [r[0] for r in res]
self.assertIn(template_dyn.id, res_ids)
self.assertIn(product.product_tmpl_id.id, res_ids)
def test_product_product_name_search(self):
attribute = self.env['product.attribute'].create({
'name': 'Attribute',
'value_ids': [
Command.create({'name': f'value {i}'})
for i in range(3)
]
})
template = self.env['product.template'].create({
'name': 'Whatever',
'attribute_line_ids': [
Command.create({
'attribute_id': attribute.id,
'value_ids': [Command.set(attribute.value_ids.ids)]
})
]
})
variant1, _variant2, _variant3 = template.product_variant_ids
variant1.default_code = 'HOHO'
product_search = self.env['product.product'].with_context(partner_id=33).search([
('display_name', '=', 'HOHO'),
])
self.assertEqual(variant1, product_search)

View file

@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import tagged
from itertools import pairwise
from unittest.mock import patch
from odoo.addons.product.tests.common import ProductCommon
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(ProductCommon):
class TestPricelist(ProductVariantsCommon):
@classmethod
def setUpClass(cls):
@ -17,7 +20,7 @@ class TestPricelist(ProductCommon):
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.env['product.pricelist'].create({
cls.sale_pricelist_id, cls.pricelist_eu = cls.env['product.pricelist'].create([{
'name': 'Sale pricelist',
'item_ids': [
Command.create({
@ -34,8 +37,21 @@ class TestPricelist(ProductCommon):
'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
@ -58,19 +74,29 @@ class TestPricelist(ProductCommon):
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
# make sure 'tonne' resolves down to 1 'kg'.
self.uom_ton.write({'rounding': 0.001})
# 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,
'uom_po_id': self.uom_ton.id,
'list_price': tonne_price,
'type': 'consu'
})
@ -96,3 +122,345 @@ class TestPricelist(ProductCommon):
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)

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.product.tests.common import ProductCommon
class TestPricelistAutoCreation(ProductCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Only one currency enabled and used on companies (multi-curr disabled)
cls.currency_euro = cls._enable_currency('EUR')
cls.currency_usd = cls.env['res.currency'].search([('name', '=', "USD")])
cls.env['res.company'].search([]).currency_id = cls.currency_euro
cls.env['res.currency'].search([('name', '!=', 'EUR')]).action_archive()
# Disabled pricelists feature
cls.group_user = cls.env.ref('base.group_user').sudo()
cls.group_user._remove_group(cls.group_product_pricelist)
cls.env['product.pricelist'].search([]).unlink()
def test_inactive_curr_set_on_company(self):
"""Make sure that when setting an inactive currency on a company, the activation of the
multi-currency group won't
"""
self.env.company.currency_id = self.currency_usd
self.assertFalse(
self.env['product.pricelist'].search([
('currency_id.name', '=', 'EUR'),
('company_id', '=', self.env.company.id),
])
)
self.assertTrue(self.currency_usd.active)
self.assertTrue(
self.env['product.pricelist'].search([
('currency_id.name', '=', 'USD'),
('company_id', '=', self.env.company.id),
])
)
# self.env.user.clear_caches()
# self.group_user.invalidate_recordset()
# self.assertTrue(
# self.group_product_pricelist in self.group_user.implied_ids
# )

View file

@ -2,25 +2,21 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from psycopg2 import IntegrityError
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command
from odoo.tests import tagged, TransactionCase
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT
from odoo.addons.base.tests.common import BaseCommon
class TestProductAttributeValueCommon(TransactionCase):
class TestProductAttributeValueCommon(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env['base'].with_context(**DISABLED_MAIL_CONTEXT).env
cls.env.company.country_id = cls.env.ref('base.us')
cls.computer = cls.env['product.template'].create({
'name': 'Super Computer',
'list_price': 2000,
@ -31,6 +27,8 @@ class TestProductAttributeValueCommon(TransactionCase):
cls.ram_attribute,
cls.hdd_attribute,
cls.size_attribute,
cls.extras_attribute,
cls.operating_system_attribute,
) = cls.env['product.attribute'].create([{
'name': 'Memory',
'sequence': 1,
@ -95,12 +93,44 @@ class TestProductAttributeValueCommon(TransactionCase):
'sequence': 3,
}),
],
}, {
'name': "Extras",
'sequence': 5,
'display_type': 'multi',
'create_variant': 'no_variant',
'value_ids': [
Command.create({
'name': "CPU overclock",
'sequence': 1,
}),
Command.create({
'name': "RAM overclock",
'sequence': 2,
}),
],
}, {
'name': "Operating System",
'sequence': 6,
'display_type': 'multi',
'create_variant': 'no_variant',
'value_ids': [
Command.create({
'name': "Linux",
'sequence': 1,
}),
Command.create({
'name': "Windows",
'sequence': 2,
}),
],
}])
cls.ssd_256, cls.ssd_512 = cls.ssd_attribute.value_ids
cls.ram_8, cls.ram_16, cls.ram_32 = cls.ram_attribute.value_ids
cls.hdd_1, cls.hdd_2, cls.hdd_4 = cls.hdd_attribute.value_ids
cls.size_m, cls.size_l, cls.size_xl = cls.size_attribute.value_ids
cls.extra_cpu, cls.extra_ram = cls.extras_attribute.value_ids
cls.linux_operating_system, cls.windows_operating_system = cls.operating_system_attribute.value_ids
cls.COMPUTER_SSD_PTAL_VALUES = {
'product_tmpl_id': cls.computer.id,
@ -117,6 +147,16 @@ class TestProductAttributeValueCommon(TransactionCase):
'attribute_id': cls.hdd_attribute.id,
'value_ids': [Command.set([cls.hdd_1.id, cls.hdd_2.id, cls.hdd_4.id])],
}
cls.COMPUTER_EXTRAS_PTAL_VALUES = {
'product_tmpl_id': cls.computer.id,
'attribute_id': cls.extras_attribute.id,
'value_ids': [Command.set([cls.extra_cpu.id, cls.extra_ram.id])],
}
cls.COMPUTER_OPERATING_SYSTEM_VALUES = {
'product_tmpl_id': cls.computer.id,
'attribute_id': cls.operating_system_attribute.id,
'value_ids': [Command.set([cls.windows_operating_system.id, cls.linux_operating_system.id])],
}
cls._add_computer_attribute_lines()
@ -137,10 +177,14 @@ class TestProductAttributeValueCommon(TransactionCase):
cls.computer_ssd_attribute_lines,
cls.computer_ram_attribute_lines,
cls.computer_hdd_attribute_lines,
cls.computer_extras_attribute_lines,
cls.computer_operating_system_lines,
) = cls.env['product.template.attribute.line'].create([
cls.COMPUTER_SSD_PTAL_VALUES,
cls.COMPUTER_RAM_PTAL_VALUES,
cls.COMPUTER_HDD_PTAL_VALUES,
cls.COMPUTER_EXTRAS_PTAL_VALUES,
cls.COMPUTER_OPERATING_SYSTEM_VALUES,
])
# Setup extra prices
@ -446,7 +490,7 @@ class TestProductAttributeValueConfig(TestProductAttributeValueCommon):
self.assertEqual(self.computer._get_first_possible_combination(), computer_ssd_512 + computer_ram_32 + computer_hdd_4)
# Not possible to add an exclusion when only one variant is left -> it deletes the product template associated to it
with self.assertRaises(UserError), self.cr.savepoint():
with self.assertRaises(UserError):
self._add_exclude(computer_ram_32, computer_hdd_4)
# If an exclusion rule deletes all variants at once it does not delete the template.
@ -717,12 +761,6 @@ class TestProductAttributeValueConfig(TestProductAttributeValueCommon):
with self.assertRaises(UserError, msg="can't change the product of a product template attribute value"):
self.computer_ram_attribute_lines.product_template_value_ids[0].product_tmpl_id = self.computer_case.id
with mute_logger('odoo.sql_db'), self.assertRaises(IntegrityError, msg="can't have two values with the same name for the same attribute"):
self.env['product.attribute.value'].create({
'name': '32 GB',
'attribute_id': self.ram_attribute.id,
})
@mute_logger('odoo.models.unlink')
def test_inactive_related_product_update(self):
"""
@ -768,3 +806,53 @@ class TestProductAttributeValueConfig(TestProductAttributeValueCommon):
'price_extra'
)
self.assertEqual(extra_prices, copied_extra_prices)
def test_04_create_product_variant_non_dynamic(self):
"""The goal of this test is to make sure the _create_product_variant does
not create variant if the type is not dynamic. It can however return a
variant if it already exists."""
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
computer_ram_16 = self._get_product_template_attribute_value(self.ram_16)
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
self._add_exclude(computer_ram_16, computer_hdd_1)
# CASE: variant is already created, it should return it
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
variant1 = self.computer._get_variant_for_combination(combination)
self.assertEqual(self.computer._create_product_variant(combination), variant1)
# CASE: variant does not exist, but template is non-dynamic, so it
# should not create it
Product = self.env['product.product']
variant1.unlink()
self.assertEqual(self.computer._create_product_variant(combination), Product)
def test_05_create_product_variant_dynamic(self):
"""The goal of this test is to make sure the _create_product_variant does
work with dynamic. If the combination is possible, it should create it.
If it's not possible, it should not create it."""
self.computer_hdd_attribute_lines.write({'active': False})
self.hdd_attribute.create_variant = 'dynamic'
self._add_hdd_attribute_line()
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
computer_ram_16 = self._get_product_template_attribute_value(self.ram_16)
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
self._add_exclude(computer_ram_16, computer_hdd_1)
# CASE: variant does not exist, but combination is not possible
# so it should not create it
impossible_combination = computer_ssd_256 + computer_ram_16 + computer_hdd_1
Product = self.env['product.product']
self.assertEqual(self.computer._create_product_variant(impossible_combination), Product)
# CASE: the variant does not exist, and the combination is possible, so
# it should create it
combination = computer_ssd_256 + computer_ram_8 + computer_hdd_1
variant = self.computer._create_product_variant(combination)
self.assertTrue(variant)
# CASE: the variant already exists, so it should return it
self.assertEqual(variant, self.computer._create_product_variant(combination))

View file

@ -0,0 +1,148 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command
from odoo.tools import mute_logger
from odoo.addons.product.tests.common import ProductCommon
class TestProductCombo(ProductCommon):
def test_combo_item_count(self):
combo = self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [
Command.create({'product_id': self._create_product().id}),
Command.create({'product_id': self._create_product().id}),
Command.create({'product_id': self._create_product().id}),
],
})
self.assertEqual(combo.combo_item_count, 3)
def test_currency_without_company_set(self):
self.setup_main_company(currency_code='GBP')
combo = self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [Command.create({'product_id': self.product.id})],
})
self.assertEqual(combo.currency_id.name, 'GBP')
def test_currency_with_company_set(self):
company_eur = self._create_company(
name="Company EUR", currency_id=self._enable_currency('EUR').id
)
company_isk = self._create_company(
name="Company ISK", currency_id=self._enable_currency('ISK').id
)
combo = self.env['product.combo'].create({
'name': "Test combo",
'company_id': company_eur.id,
'combo_item_ids': [Command.create({'product_id': self.product.id})],
})
self.assertEqual(combo.currency_id.name, 'EUR')
combo.company_id = company_isk
self.assertEqual(combo.currency_id.name, 'ISK')
@freeze_time('2000-01-01')
def test_base_price_multiple_currencies(self):
self.setup_main_company(currency_code='GBP')
currency_eur = self._enable_currency('EUR')
company = self._create_company(currency_id=currency_eur.id)
# For the sake of this test, we consider that 1 EUR is equivalent to 0.5 GBP.
currency_eur.rate_ids = [Command.create({
'name': '2000-01-01', 'rate': 2, 'company_id': company.id
})]
product_gbp = self._create_product(list_price=50)
product_eur_a = self._create_product(company_id=company.id, list_price=90)
product_eur_b = self._create_product(company_id=company.id, list_price=110)
combo = self.env['product.combo'].create({
'name': "Test combo",
'company_id': company.id,
'combo_item_ids': [
Command.create({'product_id': product_gbp.id}),
Command.create({'product_id': product_eur_a.id}),
Command.create({'product_id': product_eur_b.id}),
],
})
self.assertEqual(combo.base_price, 90)
def test_empty_combo_items_raises(self):
with self.assertRaises(ValidationError):
self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [],
})
def test_duplicate_combo_items_raises(self):
with self.assertRaises(ValidationError):
self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [
Command.create({'product_id': self.product.id}),
Command.create({'product_id': self.product.id}),
],
})
@mute_logger('odoo.sql_db')
def test_nested_combos_raises(self):
combo = self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [Command.create({'product_id': self.product.id})],
})
combo_product = self._create_product(type='combo', combo_ids=[Command.link(combo.id)])
with self.assertRaises(ValidationError):
self.env['product.combo'].create({
'name': "Test combo",
'combo_item_ids': [Command.create({'product_id': combo_product.id})],
})
def test_multi_company_consistency(self):
company_a = self._create_company(name="Company A")
company_b = self._create_company(name="Company B")
product_in_company_a = self._create_product(company_id=company_a.id)
# Raise if we try to create a combo in company B with a product in company A.
with self.assertRaises(UserError):
self.env['product.combo'].create({
'name': "Test combo",
'company_id': company_b.id,
'combo_item_ids': [Command.create({'product_id': product_in_company_a.id})],
})
# Don't raise if we try to create a combo in company A with a product in company A.
combo_in_company_a = self.env['product.combo'].create({
'name': "Test combo",
'company_id': company_a.id,
'combo_item_ids': [Command.create({'product_id': product_in_company_a.id})],
})
# Raise if we try to create a combo product in company B with a combo in company A.
with self.assertRaises(UserError):
self._create_product(
company_id=company_b.id,
type='combo',
combo_ids=[Command.link(combo_in_company_a.id)],
)
# Don't raise if we try to create a combo product in company A with a combo in company A.
self._create_product(
company_id=company_a.id,
type='combo',
combo_ids=[Command.link(combo_in_company_a.id)],
)
# Raise if we try to update a combo product in company A with a combo without company.
with self.assertRaises(UserError):
combo_in_company_a.write({
'company_id': False,
})

View file

@ -4,7 +4,9 @@
from datetime import datetime
import time
from odoo.fields import Command, first
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import Form
from odoo.tools import float_compare
from odoo.addons.product.tests.common import ProductCommon
@ -16,13 +18,6 @@ class TestProductPricelist(ProductCommon):
def setUpClass(cls):
super().setUpClass()
# Required for some of the tests below
# Breaks if run after account installation (and with demo data)
# as generic chart of accounts changes company currency to USD
# therefore the test must stay at_install until adapted to work with USD
# or according to current currency.
cls._use_currency('EUR')
cls.category_5_id = cls.env['product.category'].create({
'name': 'Office Furniture',
'parent_id': cls.product_category.id
@ -207,11 +202,11 @@ class TestProductPricelist(ProductCommon):
product = self.product_multi_price
price = self.customer_pricelist._get_product_price(product, quantity=1.0)
msg = "Wrong price: Multi Product Price. should be 99 instead of %s" % price
self.assertEqual(float_compare(price, 99, precision_digits=2), 0)
self.assertEqual(float_compare(price, 99, precision_digits=2), 0, msg)
price = self.business_pricelist._get_product_price(product, quantity=1.0)
msg = "Wrong price: Multi Product Price. should be 50 instead of %s" % price
self.assertEqual(float_compare(price, 50, precision_digits=2), 0)
self.assertEqual(float_compare(price, 50, precision_digits=2), 0, msg)
def test_20_price_different_currency_pricelist(self):
pricelist = self.env['product.pricelist'].create({
@ -257,6 +252,39 @@ class TestProductPricelist(ProductCommon):
# product price use the currency of the pricelist
self.assertEqual(price, 10090)
def test_price_without_pricelist_fallback_product_price(self):
ProductPricelist = self.env['product.pricelist']
spam = self.env['product.product'].create({
'name': '1 tonne of spam',
'uom_id': self.uom_ton.id,
'list_price': 100,
'type': 'consu'
})
self.assertEqual(
ProductPricelist._get_product_price(self.monitor, quantity=1.0),
self.monitor.list_price,
msg="without pricelist, the price should be the same as the list price",
)
self.assertEqual(
ProductPricelist._get_product_price(self.monitor, quantity=1.0, currency=self.new_currency),
self.monitor.list_price*10,
msg="without pricelist but with a currency different than the product one, the price "
"should be the same as the list price converted with the currency rate",
)
self.assertEqual(
ProductPricelist._get_product_price(spam, quantity=1.0, uom=self.uom_kgm),
spam.list_price / 1000,
msg="the product price should be converted using the specified uom",
)
self.assertEqual(
ProductPricelist._get_product_price(
spam, quantity=1.0, currency=self.new_currency, uom=self.uom_kgm
),
spam.list_price / 100,
msg="the product price should be converted using the specified uom and converted to the"
" correct currency",
)
def test_30_pricelist_delete(self):
""" Test that `unlink` on many records doesn't raise a RecursionError. """
self.customer_pricelist = self.env['product.pricelist'].create({
@ -264,7 +292,6 @@ class TestProductPricelist(ProductCommon):
'item_ids': [
Command.create({
'compute_price': 'formula',
'base': 'pricelist',
}),
] * 101,
})
@ -273,9 +300,9 @@ class TestProductPricelist(ProductCommon):
def test_40_pricelist_item_min_quantity_precision(self):
"""Test that the min_quantity has the precision of Product UoM."""
# Arrange: Change precision digits
uom_precision = self.env.ref("product.decimal_product_uom")
uom_precision = self.env.ref("uom.decimal_product_uom")
uom_precision.digits = 3
pricelist_item = first(self.customer_pricelist.item_ids[0])
pricelist_item = self.customer_pricelist.item_ids[0]
precise_value = 1.234
# Act: Set a value for the increased precision
@ -283,3 +310,64 @@ class TestProductPricelist(ProductCommon):
# Assert: The set value is kept
self.assertEqual(pricelist_item.min_quantity, precise_value)
def test_remove_product_on_0_product_variant_applied_on_rule(self):
"""Test generation of applied on based on rule data"""
self.pricelist_item = self.env['product.pricelist.item'].create({
'pricelist_id': self.pricelist.id,
'applied_on': '0_product_variant',
'product_tmpl_id': self.product.product_tmpl_id.id,
'product_id': self.product.id,
'compute_price': 'fixed',
'fixed_price': 50,
'base': 'list_price',
})
self.assertEqual(self.pricelist_item.applied_on, '0_product_variant')
with Form(self.pricelist_item) as form:
form.product_tmpl_id = self.env['product.template']
# Test values after on change
self.assertFalse(self.pricelist_item.product_tmpl_id)
self.assertFalse(self.pricelist_item.product_id)
self.assertEqual(self.pricelist_item.applied_on, '3_global')
def test_pricelist_sync_on_partners(self):
ResPartner = self.env['res.partner']
company_1, company_2 = self.env['res.company'].create([
{'name': 'company_1'},
{'name': 'company_2'},
])
test_partner_company = ResPartner.create({
'name': 'This company',
'is_company': True,
})
test_partner_company.with_company(company_1).property_product_pricelist = self.business_pricelist.id
test_partner_company.with_company(company_2).property_product_pricelist = self.customer_pricelist.id
child_address = ResPartner.create({
'name': 'Contact',
'parent_id': test_partner_company.id,
})
self.assertEqual(
child_address.property_product_pricelist,
test_partner_company.property_product_pricelist,
)
self.assertEqual(
child_address.with_company(company_1).property_product_pricelist,
self.business_pricelist,
)
self.assertEqual(
child_address.with_company(company_2).property_product_pricelist,
self.customer_pricelist,
)
def test_no_negative_cost(self):
form = Form(self.product)
with self.assertRaises(ValidationError):
form.standard_price = -5
form = Form(self.product.product_tmpl_id)
with self.assertRaises(ValidationError):
form.standard_price = -5

View file

@ -0,0 +1,87 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.product.tests.common import ProductCommon
@tagged('post_install', '-at_install')
class TestProductRounding(ProductCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# test-specific currencies
cls.currency_jpy = cls.env['res.currency'].create({
'name': 'JPX',
'symbol': '¥',
'rounding': 1.0,
'rate_ids': [Command.create({'rate': 133.6200, 'name': time.strftime('%Y-%m-%d')})],
})
cls.currency_cad = cls.env['res.currency'].create({
'name': 'CXD',
'symbol': '$',
'rounding': 0.01,
'rate_ids': [Command.create({'rate': 1.338800, 'name': time.strftime('%Y-%m-%d')})],
})
cls.pricelist_usd = cls.pricelist
cls.pricelist_jpy = cls.env['product.pricelist'].create({
'name': 'Pricelist Testing JPY',
'currency_id': cls.currency_jpy.id,
})
cls.pricelist_cad = cls.env['product.pricelist'].create({
'name': 'Pricelist Testing CAD',
'currency_id': cls.currency_cad.id,
})
cls.product_1_dollar = cls.env['product.product'].create({
'name': 'Test Product $1',
'list_price': 1.00,
'categ_id': cls.product_category.id,
})
cls.product_100_dollars = cls.env['product.product'].create({
'name': 'Test Product $100',
'list_price': 100.00,
'categ_id': cls.product_category.id,
})
def test_no_discount_1_dollar_product(self):
"""Ensure that no discount is applied when there shouldn't be, even for very small amounts."""
product = self.product_1_dollar
product_in_jpy = product.with_context(pricelist=self.pricelist_jpy.id)
discount_jpy = product_in_jpy._get_contextual_discount()
self.assertAlmostEqual(discount_jpy, 0.0, places=6, msg="No discount should be applied for $1 product in Testing JPY.")
product_in_usd = product.with_context(pricelist=self.pricelist_usd.id)
discount_usd = product_in_usd._get_contextual_discount()
self.assertAlmostEqual(discount_usd, 0.0, places=6, msg="No discount should be applied for $1 product in USD.")
product_in_cad = product.with_context(pricelist=self.pricelist_cad.id)
discount_cad = product_in_cad._get_contextual_discount()
self.assertAlmostEqual(discount_cad, 0.0, places=6, msg="No discount should be applied for $1 product in Testing CAD.")
def test_no_discount_100_dollars_product(self):
"""Ensure that no discount is applied when there shouldn't be, even for very small amounts."""
product = self.product_100_dollars
product_in_jpy = product.with_context(pricelist=self.pricelist_jpy.id)
discount_jpy = product_in_jpy._get_contextual_discount()
self.assertAlmostEqual(discount_jpy, 0.0, places=6, msg="No discount should be applied for $100 product in Testing JPY.")
product_in_usd = product.with_context(pricelist=self.pricelist_usd.id)
discount_usd = product_in_usd._get_contextual_discount()
self.assertAlmostEqual(discount_usd, 0.0, places=6, msg="No discount should be applied for $100 product in USD.")
product_in_cad = product.with_context(pricelist=self.pricelist_cad.id)
discount_cad = product_in_cad._get_contextual_discount()
self.assertAlmostEqual(discount_cad, 0.0, places=6, msg="No discount should be applied for $100 product in Testing CAD.")

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import first, Command
from odoo.fields import Command
from odoo.tests import tagged, TransactionCase
from odoo.tools import float_compare
@ -49,26 +49,20 @@ class TestSeller(TransactionCase):
(0, 0, {'partner_id': self.asustec.id, 'product_code': 'NO', 'company_id': False}),
]})
names = self.product_consu.with_context(
name = self.product_consu.with_context(
partner_id=self.asustec.id,
).name_get()
ref = set([x[1] for x in names])
self.assertEqual(len(names), 3, "3 vendor references should have been found")
self.assertEqual(ref, {'[A] Boudin', '[B] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list")
names = self.product_consu.with_context(
).display_name
self.assertEqual(name, '[A] Boudin, [B] Boudin, [NO] Boudin', "Incorrect vendor reference list")
name = self.product_consu.with_context(
partner_id=self.asustec.id,
company_id=company_a.id,
).name_get()
ref = set([x[1] for x in names])
self.assertEqual(len(names), 2, "2 vendor references should have been found")
self.assertEqual(ref, {'[A] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list")
names = self.product_consu.with_context(
).display_name
self.assertEqual(name, '[A] Boudin, [NO] Boudin', "Incorrect vendor reference list")
name = self.product_consu.with_context(
partner_id=self.asustec.id,
company_id=company_b.id,
).name_get()
ref = set([x[1] for x in names])
self.assertEqual(len(names), 2, "2 vendor references should have been found")
self.assertEqual(ref, {'[B] Boudin', '[NO] Boudin'}, "Incorrect vendor reference list")
).display_name
self.assertEqual(name, '[B] Boudin, [NO] Boudin', "Incorrect vendor reference list")
def test_30_select_seller(self):
self.res_partner_1 = self.asustec
@ -122,10 +116,25 @@ class TestSeller(TransactionCase):
msg = "Wrong cost price: LCD Monitor if more than 3 Unit.should be 785 instead of %s" % price
self.assertEqual(float_compare(price, 785, precision_digits=2), 0, msg)
def test_31_select_seller(self):
"""Check that the right seller is selected, even when the decimal precision of
Product Price is higher than the precision of the currency.
"""
self.env.ref('product.decimal_price').digits = 3
partner = self.asustec
product = self.product_consu
self.env['product.supplierinfo'].create([{
'partner_id': partner.id,
'product_tmpl_id': product.product_tmpl_id.id,
'price': price,
} for price in (0.025, 0.022, 0.020)])
price = product._select_seller(partner_id=partner, quantity=201).price
self.assertAlmostEqual(price, 0.02, places=3, msg="Lowest price should be returned")
def test_40_seller_min_qty_precision(self):
"""Test that the min_qty has the precision of Product UoM."""
# Arrange: Change precision digits
uom_precision = self.env.ref("product.decimal_product_uom")
uom_precision = self.env.ref("uom.decimal_product_uom")
uom_precision.digits = 3
product = self.product_service
product.seller_ids = [
@ -133,7 +142,7 @@ class TestSeller(TransactionCase):
'partner_id': self.asustec.id,
}),
]
supplier_info = first(product.seller_ids)
supplier_info = product.seller_ids[0]
precise_value = 1.234
# Act: Set a value for the increased precision

View file

@ -0,0 +1,61 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import Form, tagged
from odoo.addons.product.tests.common import ProductVariantsCommon
@tagged('post_install', '-at_install')
class TestUpdateProductAttributeValueWizard(ProductVariantsCommon):
def test_add_to_products(self):
product_template_shirt = self.env['product.template'].create({
'name': 'Shirt',
'categ_id': self.product_category.id,
'attribute_line_ids': [
Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.set([self.size_attribute_l.id])],
}),
],
})
self.assertNotIn(
self.size_attribute_m,
product_template_shirt.attribute_line_ids.value_ids,
)
action = self.size_attribute_m.action_add_to_products()
with Form(self.env[action['res_model']].with_context(action['context'])) as wizard:
wizard_record = wizard.save()
wizard_record.action_confirm()
self.assertIn(
self.size_attribute_m,
product_template_shirt.attribute_line_ids.value_ids,
)
def test_update_extra_prices(self):
self.assertEqual(
self.color_attribute.value_ids.mapped('default_extra_price'),
self.product_template_sofa.attribute_line_ids.product_template_value_ids.mapped('price_extra'),
)
self.assertEqual(
self.color_attribute.value_ids.mapped('default_extra_price'),
[0.0, 0.0, 0.0],
)
self.color_attribute_red.default_extra_price = 20.0
self.assertTrue(self.color_attribute_red.default_extra_price_changed)
wizard = Form.from_action(self.env, self.color_attribute_red.action_update_prices()).save()
wizard.action_confirm()
self.assertEqual(
self.product_template_sofa.attribute_line_ids.product_template_value_ids.filtered(
lambda ptav: ptav.product_attribute_value_id == self.color_attribute_red,
).price_extra,
20.0,
)
self.assertFalse(any(self.color_attribute.value_ids.mapped('default_extra_price_changed')))

View file

@ -1,53 +1,81 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from collections import OrderedDict
from datetime import timedelta
import io
import unittest.mock
from collections import OrderedDict
from datetime import timedelta
from unittest.mock import patch
from PIL import Image
from odoo.fields import Command
from odoo.exceptions import UserError
from odoo.tests import tagged, TransactionCase, Form
from odoo.fields import Command
from odoo.tests import Form, TransactionCase, tagged
from odoo.tools import mute_logger
from odoo.addons.product.tests.common import ProductVariantsCommon, ProductAttributesCommon
from odoo.addons.product.tests.common import ProductVariantsCommon
@tagged('post_install', '-at_install')
class TestVariantsSearch(ProductVariantsCommon):
def test_attribute_line_search(self):
product_template_shirt = self.env['product.template'].create({
'name': 'Shirt',
'categ_id': self.product_category.id,
'attribute_line_ids': [
Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.set([self.size_attribute_l.id])],
}),
],
})
search_not_to_be_found = self.env['product.template'].search(
[('attribute_line_ids', '=', 'M')]
)
self.assertNotIn(self.product_template_shirt, search_not_to_be_found,
self.assertNotIn(product_template_shirt, search_not_to_be_found,
'Shirt should not be found searching M')
search_attribute = self.env['product.template'].search(
[('attribute_line_ids', '=', 'Size')]
)
self.assertIn(self.product_template_shirt, search_attribute,
self.assertIn(product_template_shirt, search_attribute,
'Shirt should be found searching Size')
search_value = self.env['product.template'].search(
[('attribute_line_ids', '=', 'L')]
)
self.assertIn(self.product_template_shirt, search_value,
self.assertIn(product_template_shirt, search_value,
'Shirt should be found searching L')
def test_name_search(self):
self.product_slip_template = self.env['product.template'].create({
'name': 'Slip',
'default_code': 'ABC',
})
res = self.env['product.product'].name_search('Shirt', [], 'not ilike', None)
res_ids = [r[0] for r in res]
self.assertIn(self.product_slip_template.product_variant_ids.id, res_ids,
'Slip should be found searching \'not ilike\'')
templates = self.product_slip_template.name_search(
"ABC",
[['id', '!=', -1]],
)
self.assertFalse(templates, "Should not return template when searching on code")
templates = self.product_slip_template.with_context(search_product_product=True).name_search(
"ABC",
[['id', '!=', -1]],
)
self.assertTrue(templates, "Should return template when searching on code")
templates = self.product_slip_template.with_context(search_product_product=True).name_search(
"ABC",
[['id', '!=', self.product_slip_template.id]],
)
self.assertFalse(templates, "Should not return template.")
@tagged('post_install', '-at_install')
class TestVariants(ProductVariantsCommon):
@ -78,7 +106,6 @@ class TestVariants(ProductVariantsCommon):
test_template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.link(self.size_attribute_s.id)],
@ -93,7 +120,6 @@ class TestVariants(ProductVariantsCommon):
test_template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [Command.create({
'attribute_id': self.color_attribute.id,
'value_ids': [Command.link(self.color_attribute_blue.id)],
@ -111,7 +137,6 @@ class TestVariants(ProductVariantsCommon):
test_template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
Command.create({
'attribute_id': self.color_attribute.id,
@ -146,7 +171,6 @@ class TestVariants(ProductVariantsCommon):
test_template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
Command.create({
'attribute_id': self.color_attribute.id,
@ -185,7 +209,6 @@ class TestVariants(ProductVariantsCommon):
test_template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [Command.create({
'attribute_id': self.color_attribute.id,
'value_ids': [Command.link(self.color_attribute_red.id), Command.link(self.color_attribute_blue.id)],
@ -243,6 +266,10 @@ class TestVariants(ProductVariantsCommon):
self.assertEqual(template_dyn.name, 'Test Dynamical')
variant_dyn = template_dyn._create_product_variant(template_dyn._get_first_possible_combination())
if 'create_product_product' in variant_dyn.env.context:
new_context = dict(variant_dyn.env.context)
new_context.pop('create_product_product')
variant_dyn = variant_dyn.with_context(new_context)
self.assertEqual(len(template_dyn.product_variant_ids), 1)
variant_dyn_copy = variant_dyn.copy()
@ -291,12 +318,12 @@ class TestVariants(ProductVariantsCommon):
})
self.assertEqual(len(template.product_variant_ids), 2)
variant_1 = template.product_variant_ids[0]
variant_1.toggle_active()
variant_1.action_archive()
self.assertFalse(variant_1.active)
self.assertEqual(len(template.product_variant_ids), 1)
self.assertEqual(len(template.with_context(
active_test=False).product_variant_ids), 2)
variant_1.toggle_active()
variant_1.action_unarchive()
self.assertTrue(variant_1.active)
self.assertTrue(template.active)
@ -360,16 +387,57 @@ class TestVariants(ProductVariantsCommon):
self.assertEqual(len(template.product_variant_ids), 2)
variant_1 = template.product_variant_ids[0]
variant_2 = template.product_variant_ids[1]
template.product_variant_ids.toggle_active()
template.product_variant_ids.action_archive()
self.assertFalse(variant_1.active, 'Should archive all variants')
self.assertFalse(template.active, 'Should archive related template')
variant_1.toggle_active()
variant_1.action_unarchive()
self.assertTrue(variant_1.active, 'Should activate variant')
self.assertFalse(variant_2.active, 'Should not re-activate other variant')
self.assertTrue(template.active, 'Should re-activate template')
def test_open_product_form_with_default_uom_id_is_false(self):
""" Test default UoM is False when creating a product. """
uom_unit = self.env.ref('uom.product_uom_unit')
product_form = Form(self.env['product.product'].with_context(
default_uom_id=False,
))
product_form.name = 'Test Product'
product = product_form.save()
self.assertEqual(uom_unit, product.uom_id)
def test_single_variant_template_computed_values_after_creation(self):
"""Check that variant-related fields on templates are correctly set."""
product_template = self.env['product.template'].create({
'name': "one variant template",
'attribute_line_ids': [Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.set(self.size_attribute_s.ids)],
})],
'barcode': 'THIS IS A TEST',
})
self.assertEqual(
product_template.barcode,
product_template.product_variant_id.barcode,
)
self.assertEqual(
product_template.barcode,
'THIS IS A TEST',
)
product_template = self.env['product.template'].create({
'name': "one variant template without attribute lines",
'barcode': 'THIS IS A BARCODE',
})
self.assertEqual(
product_template.barcode,
product_template.product_variant_id.barcode,
)
self.assertEqual(
product_template.barcode,
'THIS IS A BARCODE',
)
@tagged('post_install', '-at_install')
class TestVariantsNoCreate(ProductAttributesCommon):
class TestVariantsNoCreate(ProductVariantsCommon):
@classmethod
def setUpClass(cls):
@ -382,7 +450,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.link(self.size_attribute_s.id)],
@ -396,7 +463,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
})
self.assertEqual(len(template.product_variant_ids), 1)
@ -414,7 +480,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [Command.create({
'attribute_id': self.size_attribute.id,
'value_ids': [Command.set(self.size_attribute.value_ids.ids)],
@ -428,7 +493,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
})
self.assertEqual(len(template.product_variant_ids), 1)
@ -446,7 +510,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
Command.create({ # no variants for this one
'attribute_id': self.size_attribute.id,
@ -470,7 +533,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
})
self.assertEqual(len(template.product_variant_ids), 1)
@ -497,7 +559,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
Command.create({ # no variants for this one
'attribute_id': self.size_attribute.id,
@ -521,7 +582,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofa',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
})
self.assertEqual(len(template.product_variant_ids), 1)
@ -548,7 +608,6 @@ class TestVariantsNoCreate(ProductAttributesCommon):
template = self.env['product.template'].create({
'name': 'Sofax',
'uom_id': self.uom_unit.id,
'uom_po_id': self.uom_unit.id,
'attribute_line_ids': [
Command.create({ # one variant for this one
'attribute_id': self.color_attribute.id,
@ -780,11 +839,11 @@ class TestVariantsImages(ProductVariantsCommon):
images = self.variants.mapped('image_1920')
self.assertEqual(len(set(images)), 4)
variant_no_image = self.variants[0]
old_last_update = variant_no_image['__last_update']
old_last_update = variant_no_image.write_date
self.assertFalse(variant_no_image.image_1920)
self.template.image_1920 = image_black
new_last_update = variant_no_image['__last_update']
new_last_update = variant_no_image.write_date
# the first has no image variant, all the others do
self.assertFalse(variant_no_image.image_variant_1920)
@ -881,7 +940,7 @@ class TestVariantsArchive(ProductVariantsCommon):
def unlink(self):
raise Exception('just')
Product._patch_method('unlink', unlink)
self.patch(type(Product), 'unlink', unlink)
variants_2x1 = self.template.product_variant_ids
self._assert_2color_x_1size()
@ -903,8 +962,6 @@ class TestVariantsArchive(ProductVariantsCommon):
archived_variants = self._get_archived_variants()
self.assertFalse(archived_variants)
Product._revert_method('unlink')
def test_02_update_variant_archive_2_value(self):
"""We do the same operations on the template as in the previous tests,
except we simulate that the variants can't be unlinked.
@ -916,7 +973,7 @@ class TestVariantsArchive(ProductVariantsCommon):
def unlink(slef):
raise Exception('just')
Product._patch_method('unlink', unlink)
self.patch(type(Product), 'unlink', unlink)
variants_2x2 = self.template.product_variant_ids
self._assert_2color_x_2size()
@ -972,8 +1029,6 @@ class TestVariantsArchive(ProductVariantsCommon):
self.assertEqual(archived_variants, variants_2x2)
self._assert_2color_x_2size(archived_variants)
Product._revert_method('unlink')
@mute_logger('odoo.models.unlink')
def test_03_update_variant_archive_3_value(self):
self._remove_ptal_size()
@ -983,7 +1038,7 @@ class TestVariantsArchive(ProductVariantsCommon):
def unlink(slef):
raise Exception('just')
Product._patch_method('unlink', unlink)
self.patch(type(Product), 'unlink', unlink)
self._assert_2color_x_1size()
archived_variants = self._get_archived_variants()
@ -1031,14 +1086,12 @@ class TestVariantsArchive(ProductVariantsCommon):
archived_variants = self._get_archived_variants()
self.assertEqual(archived_variants, variants_2x1 + variant_0x0)
Product._revert_method('unlink')
def test_04_from_to_single_values(self):
Product = self.env['product.product']
def unlink(slef):
raise Exception('just')
Product._patch_method('unlink', unlink)
self.patch(type(Product), 'unlink', unlink)
# CASE: remove one value, line becoming single value
variants_2x2 = self.template.product_variant_ids
@ -1075,11 +1128,9 @@ class TestVariantsArchive(ProductVariantsCommon):
self._assert_2color_x_0size(archived_variants)
self.assertEqual(archived_variants, variants_2x0)
Product._revert_method('unlink')
def test_name_search_dynamic_attributes(self):
# To be able to test dynamic variant "variants" feature must be set up
self.env.user.write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]})
self._enable_variants()
dynamic_attr = self.env['product.attribute'].create({
'name': 'Dynamic',
'create_variant': 'dynamic',
@ -1105,20 +1156,16 @@ class TestVariantsArchive(ProductVariantsCommon):
self._enable_uom()
units = self.uom_unit
cm = self.env.ref('uom.product_uom_cm')
mm = self.env.ref('uom.product_uom_millimeter')
template = self.product.product_tmpl_id
template_form = Form(template)
template_form.uom_id = cm
self.assertEqual(template_form.uom_po_id, cm)
template_form.uom_id = mm
template = template_form.save()
variant_form = Form(template.product_variant_ids)
variant_form.uom_id = units
self.assertEqual(variant_form.uom_po_id, units)
variant = variant_form.save()
self.assertEqual(variant.uom_po_id, units)
self.assertEqual(template.uom_po_id, units)
variant_form.save()
@mute_logger('odoo.models.unlink')
def test_dynamic_attributes_archiving(self):
@ -1129,7 +1176,7 @@ class TestVariantsArchive(ProductVariantsCommon):
# Patch unlink method to force archiving instead deleting
def unlink(self):
self.active = False
Product._patch_method('unlink', unlink)
self.patch(type(Product), 'unlink', unlink)
# Creating attributes
pa_color = ProductAttribute.create({'sequence': 1, 'name': 'color', 'create_variant': 'dynamic'})
@ -1234,15 +1281,13 @@ class TestVariantsArchive(ProductVariantsCommon):
})
self.assertTrue(product_white.active)
Product._revert_method('unlink')
def test_set_barcode(self):
tmpl = self.product.product_tmpl_id
tmpl.barcode = '123'
self.assertEqual(tmpl.barcode, '123')
self.assertEqual(self.product.barcode, '123')
tmpl.toggle_active()
tmpl.action_archive()
tmpl.barcode = '456'
tmpl.invalidate_recordset(fnames=['barcode'])
@ -1367,6 +1412,40 @@ class TestVariantsArchive(ProductVariantsCommon):
self.assertEqual(len(variants), 1)
self.assertFalse(variants[0].product_template_attribute_value_ids)
@mute_logger('odoo.models.unlink')
def test_unlink_and_archive_multiple_variants(self):
"""
Test unlinking multiple variants in various scenarios
- Unlink one archived variant
- Unlink one archived and one active variant
- Unlink multiple archived variants and multiple active variants at once
"""
products = self.env['product.product'].create([
{'name': 'variant 1', 'description': 'var 1'},
{'name': 'variant 2', 'description': 'var 2'},
{'name': 'variant 3', 'description': 'var 3'},
{'name': 'variant 4', 'description': 'var 4'},
{'name': 'variant 5', 'description': 'var 5'},
{'name': 'variant 6', 'description': 'var 6'},
{'name': 'variant 7', 'description': 'var 7'},
])
# Unlink one archived variant
products[0].action_archive()
products[0].unlink()
self.assertFalse(products[0].exists())
# Unlink one archived and one active variant
products[1].action_archive()
active_and_archived = products[1] + products[2]
active_and_archived.unlink()
self.assertFalse(active_and_archived.exists())
# Unlink multiple archived variants and multiple active variants at once
multiple_archived = products[3] + products[4]
multiple_active = products[5] + products[6]
multiple_archived.action_archive()
(multiple_archived + multiple_active).unlink()
self.assertFalse(products.exists())
@tagged('post_install', '-at_install')
class TestVariantWrite(TransactionCase):
@ -1419,7 +1498,7 @@ class TestVariantWrite(TransactionCase):
@tagged('post_install', '-at_install')
class TestVariantsExclusion(ProductAttributesCommon):
class TestVariantsExclusion(ProductVariantsCommon):
@classmethod
def setUpClass(cls):
@ -1551,3 +1630,90 @@ class TestVariantsExclusion(ProductAttributesCommon):
exclude.unlink()
self.assertEqual(len(self.smartphone.product_variant_ids), 4)
@mute_logger('odoo.models.unlink')
def test_dynamic_variants_unarchive(self):
""" Make sure that exclusions creation, update & delete are correctly handled.
Exclusions updates are not necessarily done from a specific template.
"""
product_template = self.env['product.template'].create({
'name': 'Test dynamic',
'attribute_line_ids': [
Command.create({
'attribute_id': self.dynamic_attribute.id,
'value_ids': [Command.set(self.dynamic_attribute.value_ids.ids)],
}),
Command.create({
'attribute_id': self.dynamic_attribute.id,
'value_ids': [Command.set(self.dynamic_attribute.value_ids.ids)],
})
]
})
self.assertFalse(product_template.product_variant_ids)
first_line_ptavs = product_template.attribute_line_ids[0].product_template_value_ids
second_line_ptavs = product_template.attribute_line_ids[1].product_template_value_ids
for ptav1, ptav2 in zip(first_line_ptavs, second_line_ptavs, strict=True):
product_template._create_product_variant(ptav1 + ptav2)
self.assertEqual(len(product_template.product_variant_ids), 2)
pav_to_remove = self.dynamic_attribute.value_ids[:1]
variant_to_archive = product_template.product_variant_ids.filtered(
lambda pp:
pav_to_remove in pp.product_template_attribute_value_ids.product_attribute_value_id
)
# Removing one option will archive one variant
with patch(
'odoo.addons.product.models.product_product.ProductProduct._filter_to_unlink',
lambda products: products.filtered(
lambda pp: pp.product_tmpl_id.id != product_template.id
),
):
product_template.attribute_line_ids[1].value_ids = [
Command.unlink(self.dynamic_attribute.value_ids[:1].id)
]
self.assertEqual(len(product_template.product_variant_ids), 1)
self.assertFalse(variant_to_archive.active)
# Putting it back should unarchive the archived variant
product_template.attribute_line_ids[1].value_ids = [
Command.link(self.dynamic_attribute.value_ids[:1].id)
]
self.assertEqual(len(product_template.product_variant_ids), 2)
self.assertTrue(variant_to_archive.active)
def test_supplierinfo_with_dynamic_attribute(self):
"""
Ensure that supplierinfo.product_id is never automatically set when
variants are created dynamically.
The supplierinfo should remain template-level (product_id = False)
unless the user explicitly assigns a specific variant manually,
even if only one variant exists initially.
"""
product_template = self.env['product.template'].create({
'name': 'Test dynamic',
'attribute_line_ids': [
Command.create({
'attribute_id': self.dynamic_attribute.id,
'value_ids': [Command.set(self.dynamic_attribute.value_ids.ids)],
}),
]
})
self.assertFalse(product_template.product_variant_ids)
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner.id,
'product_tmpl_id': product_template.id,
})
self.assertFalse(product_template.product_variant_ids)
product_template._create_product_variant(product_template.attribute_line_ids.product_template_value_ids[0])
self.assertEqual(len(product_template.product_variant_ids), 1)
self.assertFalse(supplierinfo.product_id)
product_template._create_product_variant(product_template.attribute_line_ids.product_template_value_ids[1])
self.assertEqual(len(product_template.product_variant_ids), 2)
self.assertFalse(supplierinfo.product_id)