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}" > +