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