From a2f543a653cef06a846601750f4690523e0b1879 Mon Sep 17 00:00:00 2001 From: SilvioC2C Date: Thu, 21 Mar 2024 11:06:58 +0100 Subject: [PATCH 1/2] [IMP] ``product_template_tags``: use tags on both templates and variants This commit allows using tags on both templates and variants. Tags can be propagated on each template-variants couple in 2 ways: - from template to variants: in this case, templates tags are copied onto every variant - from variants to template: in this case, the template inherits all the variants' tags The propagation behavior is defined at template's level. Default propagation behavior is "from template to variants" to allow full retrocompatibility with module's previous workflow. --- product_template_tags/README.rst | 1 + product_template_tags/models/__init__.py | 1 + .../models/product_product.py | 17 ++++ .../models/product_template.py | 36 ++++++++- .../models/product_template_tag.py | 54 ++++++++++--- product_template_tags/readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 1 + .../tests/test_product_template_tags.py | 80 +++++++++++++++---- .../views/product_product.xml | 25 ++++++ .../views/product_template.xml | 7 +- .../views/product_template_tag.xml | 15 +++- 11 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 product_template_tags/models/product_product.py diff --git a/product_template_tags/README.rst b/product_template_tags/README.rst index 1417c67ed8a..c3b87f0d488 100644 --- a/product_template_tags/README.rst +++ b/product_template_tags/README.rst @@ -81,6 +81,7 @@ Contributors * `Camptocamp `_ * Iván Todorovich + * Silvio Gregorini Maintainers ~~~~~~~~~~~ diff --git a/product_template_tags/models/__init__.py b/product_template_tags/models/__init__.py index 4b28608ef18..5c5f3df6484 100644 --- a/product_template_tags/models/__init__.py +++ b/product_template_tags/models/__init__.py @@ -1,2 +1,3 @@ +from . import product_product from . import product_template from . import product_template_tag diff --git a/product_template_tags/models/product_product.py b/product_template_tags/models/product_product.py new file mode 100644 index 00000000000..6a99b1281b4 --- /dev/null +++ b/product_template_tags/models/product_product.py @@ -0,0 +1,17 @@ +# Copyright 2017 ACSONE SA/NV +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + tag_ids = fields.Many2many( + comodel_name="product.template.tag", + string="Tags", + relation="product_product_product_tag_rel", + column1="product_id", + column2="tag_id", + ) diff --git a/product_template_tags/models/product_template.py b/product_template_tags/models/product_template.py index 38a6ed59551..5f63e9d24a3 100644 --- a/product_template_tags/models/product_template.py +++ b/product_template_tags/models/product_template.py @@ -1,17 +1,49 @@ # Copyright 2017 ACSONE SA/NV +# Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class ProductTemplate(models.Model): - _inherit = "product.template" + @api.model + def _get_default_tag_propagation(self): + return "tmpl2prod" + tag_ids = fields.Many2many( comodel_name="product.template.tag", string="Tags", relation="product_template_product_tag_rel", column1="product_tmpl_id", column2="tag_id", + compute="_compute_tag_ids", + inverse="_inverse_tag_ids", + store=True, + ) + tag_propagation = fields.Selection( + string="Tag Propagation", + selection=[ + ("tmpl2prod", "From template to variants"), + ("prod2tmpl", "From variants to template"), + ], + default=lambda self: self._get_default_tag_propagation(), + required=True, + help="Defines how tags are propagated among templates and variants.\n" + "- From template to variants: variants' tags are read-only, and copied" + " from their template\n" + "- From variants to template: template's tags are read-only, and are" + " defined as a full list of all its variants' tags\n", ) + + @api.depends("product_variant_ids.tag_ids", "tag_propagation") + def _compute_tag_ids(self): + # Update only templates whose tags are read from variants + for tmpl in self.filtered(lambda x: x.tag_propagation == "prod2tmpl"): + tmpl.tag_ids = tmpl.product_variant_ids.tag_ids + + def _inverse_tag_ids(self): + # Update only variants whose tags are read from templates + for tmpl in self.filtered(lambda x: x.tag_propagation == "tmpl2prod"): + tmpl.product_variant_ids.tag_ids = tmpl.tag_ids diff --git a/product_template_tags/models/product_template_tag.py b/product_template_tags/models/product_template_tag.py index bf90aa80da1..417caec9bcc 100644 --- a/product_template_tags/models/product_template_tag.py +++ b/product_template_tags/models/product_template_tag.py @@ -1,4 +1,5 @@ # Copyright 2017 ACSONE SA/NV +# Copyright 2024 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models @@ -9,6 +10,10 @@ class ProductTemplateTag(models.Model): _description = "Product Tag" _order = "sequence, name" + @api.model + def _get_default_company_id(self): + return self.env.company + name = fields.Char(string="Name", required=True, translate=True) sequence = fields.Integer(default=10) color = fields.Integer(string="Color Index") @@ -19,13 +24,23 @@ class ProductTemplateTag(models.Model): column1="tag_id", column2="product_tmpl_id", ) - products_count = fields.Integer( - string="# of Products", compute="_compute_products_count" + product_tmpl_count = fields.Integer( + string="# of Products", compute="_compute_product_tmpl_count" + ) + product_prod_ids = fields.Many2many( + comodel_name="product.product", + string="Variants", + relation="product_product_product_tag_rel", + column1="tag_id", + column2="product_id", + ) + product_prod_count = fields.Integer( + string="# of Variants", compute="_compute_product_prod_count" ) company_id = fields.Many2one( comodel_name="res.company", string="Company", - default=lambda self: self.env.company, + default=lambda self: self._get_default_company_id(), ) _sql_constraints = [ @@ -37,16 +52,35 @@ class ProductTemplateTag(models.Model): ] @api.depends("product_tmpl_ids") - def _compute_products_count(self): - tag_id_product_count = {} + def _compute_product_tmpl_count(self): + tag_id_product_tmpl_count = {} if self.ids: self.env.cr.execute( - """SELECT tag_id, COUNT(*) + """ + SELECT tag_id, COUNT(1) FROM product_template_product_tag_rel WHERE tag_id IN %s - GROUP BY tag_id""", + GROUP BY tag_id + """, + (tuple(self.ids),), + ) + tag_id_product_tmpl_count = dict(self.env.cr.fetchall()) + for tag in self: + tag.product_tmpl_count = tag_id_product_tmpl_count.get(tag.id, 0) + + @api.depends("product_prod_ids") + def _compute_product_prod_count(self): + tag_id_product_prod_count = {} + if self.ids: + self.env.cr.execute( + """ + SELECT tag_id, COUNT(1) + FROM product_product_product_tag_rel + WHERE tag_id IN %s + GROUP BY tag_id + """, (tuple(self.ids),), ) - tag_id_product_count = dict(self.env.cr.fetchall()) - for rec in self: - rec.products_count = tag_id_product_count.get(rec.id, 0) + tag_id_product_prod_count = dict(self.env.cr.fetchall()) + for tag in self: + tag.product_prod_count = tag_id_product_prod_count.get(tag.id, 0) diff --git a/product_template_tags/readme/CONTRIBUTORS.rst b/product_template_tags/readme/CONTRIBUTORS.rst index eec5682f6ad..07502da9d8e 100644 --- a/product_template_tags/readme/CONTRIBUTORS.rst +++ b/product_template_tags/readme/CONTRIBUTORS.rst @@ -6,3 +6,4 @@ * `Camptocamp `_ * Iván Todorovich + * Silvio Gregorini diff --git a/product_template_tags/static/description/index.html b/product_template_tags/static/description/index.html index 52ed4b8f9df..75883ded1bc 100644 --- a/product_template_tags/static/description/index.html +++ b/product_template_tags/static/description/index.html @@ -427,6 +427,7 @@

Contributors

  • Pimolnat Suntian <pimolnats@ecosoft.co.th>
  • Camptocamp
  • diff --git a/product_template_tags/tests/test_product_template_tags.py b/product_template_tags/tests/test_product_template_tags.py index 156abbbb146..9417c85317f 100644 --- a/product_template_tags/tests/test_product_template_tags.py +++ b/product_template_tags/tests/test_product_template_tags.py @@ -12,29 +12,77 @@ class TestProductTemplateTagBase(SavepointCase): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.product_tmpl = cls.env["product.template"].create({"name": "Test Product"}) + cls.tag = cls.env["product.template.tag"].create({"name": "Test Tag"}) + cls.product_attr = cls.env["product.attribute"].create( + { + "name": "Test Attrib", + "value_ids": [ + (0, 0, {"name": "Test Attrib Value %s" % str(x)}) for x in (1, 2) + ], + } + ) + cls.product_tmpl = cls.env["product.template"].create( + { + "name": "Test Product Tmpl", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.product_attr.id, + "value_ids": [(6, 0, cls.product_attr.value_ids.ids)], + }, + ) + ], + } + ) class TestProductTemplateTag(TestProductTemplateTagBase): - def test_product_template_tag(self): - product_tmpl_tag = self.env["product.template.tag"].create( - {"name": "Test Tag", "product_tmpl_ids": [(6, 0, [self.product_tmpl.id])]} - ) - product_tmpl_tag._compute_products_count() - self.assertEqual(product_tmpl_tag.products_count, 1) - - def test_product_template_tag_uniq(self): - product_tmpl_tag = self.env["product.template.tag"].create({"name": "Test Tag"}) - self.assertTrue(product_tmpl_tag) + def test_00_product_template_tag_uniq(self): # test same tag and same company with mute_logger("odoo.sql_db"): with self.assertRaises(IntegrityError): with self.cr.savepoint(): self.env["product.template.tag"].create({"name": "Test Tag"}) - # test same tag and different company company = self.env["res.company"].create({"name": "Test"}) - same_product_tmpl_tag_diff_company = self.env["product.template.tag"].create( - {"name": "Test Tag", "company_id": company.id} - ) - self.assertTrue(same_product_tmpl_tag_diff_company) + vals = {"name": "Test Tag", "company_id": company.id} + self.assertTrue(self.env["product.template.tag"].create(vals)) + + def test_01_tag_propagation_tmpl2prod(self): + """Test tag propagation from template to products + + On templates where ``tag_propagation = "tmpl2prod"``, setting tags on the + template should propagate them to all the variants + """ + tag = self.tag + template = self.product_tmpl + variants = template.product_variant_ids + template.tag_propagation = "tmpl2prod" + template.tag_ids = tag + for variant in variants: + self.assertEqual(variant.tag_ids, tag) + self.assertEqual(tag.product_tmpl_ids, template) + self.assertEqual(tag.product_tmpl_count, 1) + self.assertEqual(tag.product_prod_ids, variants) + self.assertEqual(tag.product_prod_count, 2) + + def test_02_tag_propagation_prod2tmpl(self): + """Test tag propagation from products to template + + On templates where ``tag_propagation = "prod2tmpl"``, setting tags on a variant + should propagate them to the template, not the other variants + """ + tag = self.tag + template = self.product_tmpl + variant_1, variant_2 = template.product_variant_ids + # Test tag propagation from products to template + template.tag_propagation = "prod2tmpl" + variant_1.tag_ids = tag + self.assertEqual(template.tag_ids, tag) + self.assertFalse(variant_2.tag_ids) + self.assertEqual(tag.product_tmpl_ids, template) + self.assertEqual(tag.product_tmpl_count, 1) + self.assertEqual(tag.product_prod_ids, variant_1) + self.assertEqual(tag.product_prod_count, 1) diff --git a/product_template_tags/views/product_product.xml b/product_template_tags/views/product_product.xml index 87eec41a76c..c142e497c64 100644 --- a/product_template_tags/views/product_product.xml +++ b/product_template_tags/views/product_product.xml @@ -5,6 +5,21 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> + + product.product + + + + + + + + product.product @@ -22,6 +37,16 @@ + + product.product + + + + + + + + product.product diff --git a/product_template_tags/views/product_template.xml b/product_template_tags/views/product_template.xml index 7b71307bf1a..a7734f62157 100644 --- a/product_template_tags/views/product_template.xml +++ b/product_template_tags/views/product_template.xml @@ -1,16 +1,19 @@ + - product.template.form + product.template.form (in product_template_tags) product.template - + + diff --git a/product_template_tags/views/product_template_tag.xml b/product_template_tags/views/product_template_tag.xml index 0be40c967e1..b265d4a8421 100644 --- a/product_template_tags/views/product_template_tag.xml +++ b/product_template_tags/views/product_template_tag.xml @@ -18,11 +18,24 @@ context="{'search_default_tag_ids': active_id}" > +