From 1ebc7db135364a85fe10dc0d2270aa6e459a9c68 Mon Sep 17 00:00:00 2001 From: Christopher Ormaza Date: Fri, 6 Sep 2024 16:02:12 -0500 Subject: [PATCH] [16.0][ADD] product_variant_change_template --- product_variant_change_template/README.rst | 77 ++++ product_variant_change_template/__init__.py | 1 + .../__manifest__.py | 20 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 2 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 424 ++++++++++++++++++ .../tests/__init__.py | 1 + .../test_product_variant_change_template.py | 250 +++++++++++ .../wizard/__init__.py | 1 + .../wizard_product_variant_change_template.py | 196 ++++++++ ...d_product_variant_change_template_view.xml | 91 ++++ .../addons/product_variant_change_template | 1 + .../product_variant_change_template/setup.py | 6 + 14 files changed, 1073 insertions(+) create mode 100644 product_variant_change_template/README.rst create mode 100644 product_variant_change_template/__init__.py create mode 100644 product_variant_change_template/__manifest__.py create mode 100644 product_variant_change_template/readme/CONTRIBUTORS.rst create mode 100644 product_variant_change_template/readme/DESCRIPTION.rst create mode 100644 product_variant_change_template/security/ir.model.access.csv create mode 100644 product_variant_change_template/static/description/index.html create mode 100644 product_variant_change_template/tests/__init__.py create mode 100644 product_variant_change_template/tests/test_product_variant_change_template.py create mode 100644 product_variant_change_template/wizard/__init__.py create mode 100644 product_variant_change_template/wizard/wizard_product_variant_change_template.py create mode 100644 product_variant_change_template/wizard/wizard_product_variant_change_template_view.xml create mode 120000 setup/product_variant_change_template/odoo/addons/product_variant_change_template create mode 100644 setup/product_variant_change_template/setup.py diff --git a/product_variant_change_template/README.rst b/product_variant_change_template/README.rst new file mode 100644 index 000000000..64f2bf08d --- /dev/null +++ b/product_variant_change_template/README.rst @@ -0,0 +1,77 @@ +=============================== +Product Variant Change Template +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a35037acacc68bbe6f07542ccdddc8b41bda916af5297c8065baf087d1900952 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--variant-lightgray.png?logo=github + :target: https://github.com/OCA/product-variant/tree/16.0/product_variant_change_template + :alt: OCA/product-variant +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-variant-16-0/product-variant-16-0-product_variant_change_template + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-variant&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allow possibility to change current template in a product variant, +after a template empty its variants this is automatic inactivated + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow S.L + +Contributors +~~~~~~~~~~~~ + +* Christopher Ormaza + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/product-variant `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_variant_change_template/__init__.py b/product_variant_change_template/__init__.py new file mode 100644 index 000000000..40272379f --- /dev/null +++ b/product_variant_change_template/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/product_variant_change_template/__manifest__.py b/product_variant_change_template/__manifest__.py new file mode 100644 index 000000000..bab03e8ba --- /dev/null +++ b/product_variant_change_template/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Product Variant Change Template", + "version": "16.0.1.0.0", + "summary": "Tool to allow change template of a existing product variant", + "category": "Tools", + "author": "ForgeFlow S.L, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-variant", + "license": "AGPL-3", + "depends": ["product_variant_configurator", "product_variant_sale_price"], + "data": [ + "security/ir.model.access.csv", + "wizard/wizard_product_variant_change_template_view.xml", + ], + "demo": [], + "installable": True, + "auto_install": False, + "external_dependencies": { + "python": [], + }, +} diff --git a/product_variant_change_template/readme/CONTRIBUTORS.rst b/product_variant_change_template/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b647a292e --- /dev/null +++ b/product_variant_change_template/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Christopher Ormaza diff --git a/product_variant_change_template/readme/DESCRIPTION.rst b/product_variant_change_template/readme/DESCRIPTION.rst new file mode 100644 index 000000000..62f183932 --- /dev/null +++ b/product_variant_change_template/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allow possibility to change current template in a product variant, +after a template empty its variants this is automatic inactivated diff --git a/product_variant_change_template/security/ir.model.access.csv b/product_variant_change_template/security/ir.model.access.csv new file mode 100644 index 000000000..70df1df10 --- /dev/null +++ b/product_variant_change_template/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_wizard_product_variant_change_template_all","access_wizard_product_variant_change_template_all","model_wizard_product_variant_change_template","base.group_user",1,1,1,1 diff --git a/product_variant_change_template/static/description/index.html b/product_variant_change_template/static/description/index.html new file mode 100644 index 000000000..03db061b5 --- /dev/null +++ b/product_variant_change_template/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Product Variant Change Template + + + +
+

Product Variant Change Template

+ + +

Beta License: AGPL-3 OCA/product-variant Translate me on Weblate Try me on Runboat

+

This module allow possibility to change current template in a product variant, +after a template empty its variants this is automatic inactivated

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow S.L
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/product-variant project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_variant_change_template/tests/__init__.py b/product_variant_change_template/tests/__init__.py new file mode 100644 index 000000000..f6ce9e05d --- /dev/null +++ b/product_variant_change_template/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_variant_change_template diff --git a/product_variant_change_template/tests/test_product_variant_change_template.py b/product_variant_change_template/tests/test_product_variant_change_template.py new file mode 100644 index 000000000..3b7de39f3 --- /dev/null +++ b/product_variant_change_template/tests/test_product_variant_change_template.py @@ -0,0 +1,250 @@ +from odoo.exceptions import UserError +from odoo.tests.common import Form, TransactionCase + + +class TestProductVariantChangeTemplate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.wizard_model = cls.env["wizard.product.variant.change.template"] + # ENVIRONMENTS + cls.product_attribute = cls.env["product.attribute"] + cls.product_attribute_value = cls.env["product.attribute.value"] + cls.product_configurator_attribute = cls.env["product.configurator.attribute"] + cls.product_category = cls.env["product.category"] + cls.product_product = cls.env["product.product"] + cls.product_template = cls.env["product.template"].with_context( + check_variant_creation=True + ) + + # Instances: product attribute + cls.attribute1 = cls.product_attribute.create({"name": "Test Attribute 1"}) + cls.attribute2 = cls.product_attribute.create({"name": "Test Attribute 2"}) + # Instances: product attribute value + cls.value1 = cls.product_attribute_value.create( + {"name": "Value 1", "attribute_id": cls.attribute1.id} + ) + cls.value2 = cls.product_attribute_value.create( + {"name": "Value 2", "attribute_id": cls.attribute1.id} + ) + cls.value3 = cls.product_attribute_value.create( + {"name": "Value 3", "attribute_id": cls.attribute2.id} + ) + cls.value4 = cls.product_attribute_value.create( + {"name": "Value 4", "attribute_id": cls.attribute2.id} + ) + cls.price_list = cls.env["product.pricelist"].create( + { + "name": "Pricelist Test", + } + ) + + def test_01_update_template(self): + tmpl = self.product_template.with_context( + product_name="No create variants template" + ).create( + { + "name": "No create variants template", + "no_create_variants": "yes", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.attribute1.id, + "value_ids": [(6, 0, [self.value1.id, self.value2.id])], + "required": True, + }, + ), + ( + 0, + 0, + { + "attribute_id": self.attribute2.id, + "value_ids": [(6, 0, [self.value3.id, self.value4.id])], + }, + ), + ], + } + ) + + self.assertEqual(len(tmpl.product_variant_ids), 0) + + product_1 = ( + self.env["product.product"] + .with_context(product_name="Product 1") + .create({"name": "Product 1", "lst_price": 100.0}) + ) + product_tmpl_1 = product_1.product_tmpl_id + self.price_list_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.price_list.id, + "applied_on": "1_product", + "product_tmpl_id": product_tmpl_1.id, + "fixed_price": 100.0, + } + ) + + product_2 = ( + self.env["product.product"] + .with_context(product_name="Product 2") + .create({"name": "Product 2", "lst_price": 150.0}) + ) + product_tmpl_2 = product_2.product_tmpl_id + self.price_list_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.price_list.id, + "applied_on": "1_product", + "product_tmpl_id": product_tmpl_2.id, + "fixed_price": 150.0, + } + ) + product_3 = ( + self.env["product.product"] + .with_context(product_name="Product 3") + .create({"name": "Product 3", "lst_price": 130.0}) + ) + product_tmpl_3 = product_3.product_tmpl_id + product_4 = ( + self.env["product.product"] + .with_context(product_name="Product 4") + .create({"name": "Product 4", "lst_price": 180.0}) + ) + product_tmpl_4 = product_4.product_tmpl_id + product_5 = ( + self.env["product.product"] + .with_context(product_name="Product 5") + .create( + { + "name": "Product 5", + "lst_price": 200.0, + "uom_id": self.env.ref("uom.product_uom_meter").id, + "uom_po_id": self.env.ref("uom.product_uom_meter").id, + } + ) + ) + + # Associate product 1 + wizard_form = Form( + self.wizard_model.with_context( + active_model="product.product", active_id=product_1.id + ) + ) + + # Should be shown all values of product template + wizard_form.destination_template_id = tmpl + wizard = wizard_form.save() + self.assertEqual( + wizard.available_value_ids.ids, + [self.value1.id, self.value2.id, self.value3.id, self.value4.id], + ) + + wizard_form = Form( + wizard.with_context(active_model="product.product", active_id=product_1.id) + ) + + # When a value is selected then should be removed all values associated to its attribute + wizard_form.selected_value_ids.add(self.value1) + wizard = wizard_form.save() + self.assertEqual( + wizard.available_value_ids.ids, [self.value3.id, self.value4.id] + ) + + wizard_form = Form( + wizard.with_context(active_model="product.product", active_id=product_1.id) + ) + + # When a value is selected then should be removed all values associated to its attribute + wizard_form.selected_value_ids.add(self.value3) + wizard = wizard_form.save() + self.assertEqual(wizard.available_value_ids.ids, []) + + wizard.action_change_template() + + self.assertEqual(product_1.product_tmpl_id, tmpl) + self.assertEqual(product_1.lst_price, 100.0) + self.assertFalse(product_tmpl_1.active) + self.assertEqual(tmpl.pricelist_item_count, 1) + + # Associate product 2 + wizard_form = Form( + self.wizard_model.with_context( + active_model="product.product", active_id=product_2.id + ) + ) + wizard_form.destination_template_id = tmpl + wizard_form.selected_value_ids.add(self.value1) + wizard_form.selected_value_ids.add(self.value3) + + # With select combination of values should be added as an existing variant + wizard = wizard_form.save() + self.assertTrue(bool(wizard.already_exist_variant_ids)) + + wizard_form = Form( + wizard.with_context(active_model="product.product", active_id=product_2.id) + ) + wizard_form.selected_value_ids.remove(self.value3.id) + wizard_form.selected_value_ids.add(self.value4) + wizard = wizard_form.save() + wizard.action_change_template() + + self.assertEqual(product_2.product_tmpl_id, tmpl) + self.assertEqual(product_1.lst_price, 100.0) + self.assertEqual(product_2.lst_price, 150.0) + self.assertFalse(product_tmpl_2.active) + self.assertEqual(tmpl.pricelist_item_count, 2) + + # Associate product 3 + wizard_form = Form( + self.wizard_model.with_context( + active_model="product.product", active_id=product_3.id + ) + ) + wizard_form.destination_template_id = tmpl + wizard_form.selected_value_ids.add(self.value3) + wizard = wizard_form.save() + # Attribute 1 should be setup to continue + with self.assertRaises(UserError): + wizard.action_change_template() + + wizard_form = Form( + wizard.with_context(active_model="product.product", active_id=product_3.id) + ) + wizard_form.selected_value_ids.add(self.value2) + wizard = wizard_form.save() + wizard.action_change_template() + + self.assertEqual(product_3.product_tmpl_id, tmpl) + self.assertEqual(product_1.lst_price, 100.0) + self.assertEqual(product_2.lst_price, 150.0) + self.assertEqual(product_3.lst_price, 130.0) + self.assertFalse(product_tmpl_3.active) + + # Associate product 4 + wizard_form = Form( + self.wizard_model.with_context( + active_model="product.product", active_id=product_4.id + ) + ) + wizard_form.destination_template_id = tmpl + wizard_form.selected_value_ids.add(self.value2) + wizard_form.selected_value_ids.add(self.value4) + wizard = wizard_form.save() + wizard.action_change_template() + + self.assertEqual(product_4.product_tmpl_id, tmpl) + self.assertEqual(product_1.lst_price, 100.0) + self.assertEqual(product_2.lst_price, 150.0) + self.assertEqual(product_3.lst_price, 130.0) + self.assertEqual(product_4.lst_price, 180.0) + self.assertFalse(product_tmpl_4.active) + + # Associate product 5 with different uom + wizard_form = Form( + self.wizard_model.with_context( + active_model="product.product", active_id=product_5.id + ) + ) + wizard_form.destination_template_id = tmpl + wizard = wizard_form.save() + self.assertTrue(wizard.is_different_uom) diff --git a/product_variant_change_template/wizard/__init__.py b/product_variant_change_template/wizard/__init__.py new file mode 100644 index 000000000..0d63681fd --- /dev/null +++ b/product_variant_change_template/wizard/__init__.py @@ -0,0 +1 @@ +from . import wizard_product_variant_change_template diff --git a/product_variant_change_template/wizard/wizard_product_variant_change_template.py b/product_variant_change_template/wizard/wizard_product_variant_change_template.py new file mode 100644 index 000000000..c623c9134 --- /dev/null +++ b/product_variant_change_template/wizard/wizard_product_variant_change_template.py @@ -0,0 +1,196 @@ +import logging + +from odoo import Command, _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class WizardProductVariantChangeTemplate(models.TransientModel): + + _name = "wizard.product.variant.change.template" + + _description = "Wizard to change template in product variant" + + current_product_id = fields.Many2one( + comodel_name="product.product", + string="Current Product", + required=True, + readonly=True, + ) + current_template_id = fields.Many2one( + related="current_product_id.product_tmpl_id", + ) + destination_template_id = fields.Many2one( + comodel_name="product.template", string="Destination Template", required=True + ) + available_value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="rel_wizard_change_template_value_available", + column1="wizard_line_id", + column2="value_id", + string="Available Attributes", + readonly=True, + ) + selected_value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="rel_wizard_change_template_value_selected", + column1="wizard_line_id", + column2="value_id", + string="Attributes to apply", + ) + require_attribute_selection = fields.Boolean( + compute="_compute_require_attribute_selection" + ) + already_exist_variant_ids = fields.Many2many( + comodel_name="product.product", compute="_compute_already_exist_variant" + ) + is_different_uom = fields.Boolean(compute="_compute_different_uom") + current_uom_id = fields.Many2one( + related="current_template_id.uom_id", string="Current UoM" + ) + destination_uom_id = fields.Many2one( + related="destination_template_id.uom_id", string="Destination UoM" + ) + + @api.depends("destination_template_id", "current_template_id") + def _compute_different_uom(self): + for rec in self: + different = ( + rec.destination_template_id.uom_id != rec.current_template_id.uom_id + ) + rec.is_different_uom = rec.destination_template_id and different or False + + @api.depends("destination_template_id", "selected_value_ids") + def _compute_already_exist_variant(self): + for rec in self: + already_exist_product = self.env["product.product"].search( + [ + ("product_tmpl_id", "=", rec.destination_template_id.id), + ( + "product_template_attribute_value_ids.product_attribute_value_id", + "=", + self.selected_value_ids.ids, + ), + ] + ) + already_exist_product = already_exist_product.filtered( + lambda x: sorted( + x.mapped( + "product_template_attribute_value_ids.product_attribute_value_id" + ).ids + ) + == sorted(self.selected_value_ids.ids) + ) + self.already_exist_variant_ids = already_exist_product.ids or [ + Command.clear() + ] + + @api.depends("destination_template_id") + def _compute_require_attribute_selection(self): + for rec in self: + rec.require_attribute_selection = bool( + rec.destination_template_id.attribute_line_ids.filtered( + lambda x: x.value_ids + ) + ) + + def default_get(self, fields_list): + values = super().default_get(fields_list) + values.update( + { + "current_product_id": self.env.context.get("active_id"), + } + ) + return values + + @api.onchange("destination_template_id", "selected_value_ids") + def _onchange_attributes(self): + all_values = self.destination_template_id.attribute_line_ids.mapped("value_ids") + attributes_selected = self.selected_value_ids.mapped("attribute_id") + available_values = all_values.filtered( + lambda x: x.id not in self.selected_value_ids.ids + ) + available_values = available_values.filtered( + lambda x: x.attribute_id.id not in attributes_selected.ids + ) + self.available_value_ids = available_values.ids or [Command.clear()] + + def _check_attributes_required(self): + attributes_required = self.destination_template_id.attribute_line_ids.filtered( + lambda x: x.required + ).mapped("attribute_id") + attributed_selected = self.selected_value_ids.mapped("attribute_id") + for attribute in attributes_required: + if attribute.id not in attributed_selected.ids: + raise UserError( + _( + "Required attribute %s is not in selected attributes, please check" + ) + % attribute.display_name + ) + + def _handle_price_list_item_change(self): + if self.current_template_id != self.destination_template_id: + if self.current_template_id.product_variant_count == 1: + price_list_related = self.env["product.pricelist.item"].search( + [ + ("product_tmpl_id", "=", self.current_template_id.id), + ("applied_on", "=", "1_product"), + ] + ) + if price_list_related: + price_list_related.write( + { + "applied_on": "0_product_variant", + "product_tmpl_id": False, + "product_id": self.current_product_id.id, + } + ) + + def action_change_template(self): + if self.is_different_uom: + raise UserError( + _( + "You can't use a destination template " + "with different UoM of original template" + ) + ) + if self.already_exist_variant_ids: + raise UserError( + _( + "You can't assign this combination, a " + "variant already exists in template selected" + ) + ) + self._check_attributes_required() + current_attribute_value = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", self.destination_template_id.id), + ("product_attribute_value_id", "in", self.selected_value_ids.ids), + ] + ) + current_template = self.current_template_id + current_lst_price = self.current_product_id.lst_price + self._handle_price_list_item_change() + self.current_product_id.write( + { + "product_tmpl_id": self.destination_template_id.id, + "product_template_attribute_value_ids": [Command.clear()] + + [Command.link(value.id) for value in current_attribute_value], + } + ) + self.current_product_id.write( + { + "lst_price": current_lst_price, + } + ) + self.current_product_id.invalidate_model(["product_tmpl_id"]) + current_template.invalidate_model(["product_variant_ids"]) + if len(current_template.product_variant_ids) == 0: + current_template.write( + { + "active": False, + } + ) + return {"type": "ir.actions.act_window_close"} diff --git a/product_variant_change_template/wizard/wizard_product_variant_change_template_view.xml b/product_variant_change_template/wizard/wizard_product_variant_change_template_view.xml new file mode 100644 index 000000000..f47fe62a8 --- /dev/null +++ b/product_variant_change_template/wizard/wizard_product_variant_change_template_view.xml @@ -0,0 +1,91 @@ + + + + + wizard_product_variant_change_template_view_form + wizard.product.variant.change.template + +
+ + + + + + + + + + +
+
+
+
+
+ + + Change Template of Variant + ir.actions.act_window + wizard.product.variant.change.template + form + new + + form + + +
diff --git a/setup/product_variant_change_template/odoo/addons/product_variant_change_template b/setup/product_variant_change_template/odoo/addons/product_variant_change_template new file mode 120000 index 000000000..a1071bb33 --- /dev/null +++ b/setup/product_variant_change_template/odoo/addons/product_variant_change_template @@ -0,0 +1 @@ +../../../../product_variant_change_template \ No newline at end of file diff --git a/setup/product_variant_change_template/setup.py b/setup/product_variant_change_template/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/product_variant_change_template/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)