From 1b48a8527b34db820522f2add48d876c9e06b2c1 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 22 Jan 2025 17:38:50 +0100 Subject: [PATCH 1/3] [ADD] sale_product_matrix_secondary_unit: New module TT52531 --- sale_product_matrix_secondary_unit/README.rst | 122 +++++ .../__init__.py | 1 + .../__manifest__.py | 31 ++ .../sale_product_matrix_secondary_unit.pot | 76 +++ .../models/__init__.py | 2 + .../models/product_template.py | 21 + .../models/sale_order.py | 117 +++++ .../readme/CONFIGURE.md | 8 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 2 + .../readme/ROADMAP.md | 6 + .../readme/USAGE.md | 6 + .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 468 ++++++++++++++++++ .../src/js/product_matrix_configurator.js | 54 ++ .../static/src/js/section_and_note_widget.js | 176 +++++++ .../static/src/xml/product_matrix.xml | 39 ++ ...sale_product_matrix_secondary_unit_tour.js | 106 ++++ .../tests/__init__.py | 1 + ...test_sale_product_matrix_secondary_unit.py | 107 ++++ .../views/sale_order_views.xml | 29 ++ 21 files changed, 1374 insertions(+) create mode 100644 sale_product_matrix_secondary_unit/README.rst create mode 100644 sale_product_matrix_secondary_unit/__init__.py create mode 100644 sale_product_matrix_secondary_unit/__manifest__.py create mode 100644 sale_product_matrix_secondary_unit/i18n/sale_product_matrix_secondary_unit.pot create mode 100644 sale_product_matrix_secondary_unit/models/__init__.py create mode 100644 sale_product_matrix_secondary_unit/models/product_template.py create mode 100644 sale_product_matrix_secondary_unit/models/sale_order.py create mode 100644 sale_product_matrix_secondary_unit/readme/CONFIGURE.md create mode 100644 sale_product_matrix_secondary_unit/readme/CONTRIBUTORS.md create mode 100644 sale_product_matrix_secondary_unit/readme/DESCRIPTION.md create mode 100644 sale_product_matrix_secondary_unit/readme/ROADMAP.md create mode 100644 sale_product_matrix_secondary_unit/readme/USAGE.md create mode 100644 sale_product_matrix_secondary_unit/static/description/icon.png create mode 100644 sale_product_matrix_secondary_unit/static/description/index.html create mode 100644 sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js create mode 100644 sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js create mode 100644 sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml create mode 100644 sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js create mode 100644 sale_product_matrix_secondary_unit/tests/__init__.py create mode 100644 sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py create mode 100644 sale_product_matrix_secondary_unit/views/sale_order_views.xml diff --git a/sale_product_matrix_secondary_unit/README.rst b/sale_product_matrix_secondary_unit/README.rst new file mode 100644 index 00000000000..78805c3892c --- /dev/null +++ b/sale_product_matrix_secondary_unit/README.rst @@ -0,0 +1,122 @@ +================================ +Secondary unit in product matrix +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:34455330ff1ccd5b50ea1363108cddf7ace04951394d5702f2aadc9c3d9ee471 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/15.0/sale_product_matrix_secondary_unit + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-15-0/product-attribute-15-0-sale_product_matrix_secondary_unit + :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-attribute&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +With this module we'll be able to set the secondary units in the product +matrix for a quick quotation for those products using this kind of +configurator. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +- Go to a product with variants or create one. +- In the *Attributes and variants* tab, *Sales Variant Selection* + section, select **Order grid entry**. +- In the *General information* tab, *Secondary unit of measure* section, + add one or several secondary units. +- You can also set a default **Sale secondary unit**. + +Usage +===== + +To use this module, you need to: + +1. Go to a sales order and add the configured product. +2. The matrix dialog will show up and there you'll be able to select the + available secondary units. +3. If a default sale secondary unit is set, it will selected in the + dialog already. + +Known issues / Roadmap +====================== + +- When could have several lines from the same template with different + secondary units. In that case, the use of the matrix is discarded to + avoid missmatching values. From that moment, the products are forced + to be configured with the regular product configurator. +- The client side is roughly implemented right now. Probably we'll fix + some of the most obvious bugs but our roadmap is headed to v17 with + the Owl webclient. + +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 +------- + +* Tecnativa + +Contributors +------------ + +- Tecnativa (https://www.tecnativa.com) + + - David Vidal + +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. + +.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px + :target: https://github.com/chienandalu + :alt: chienandalu + +Current `maintainer `__: + +|maintainer-chienandalu| + +This module is part of the `OCA/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_product_matrix_secondary_unit/__init__.py b/sale_product_matrix_secondary_unit/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_product_matrix_secondary_unit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_product_matrix_secondary_unit/__manifest__.py b/sale_product_matrix_secondary_unit/__manifest__.py new file mode 100644 index 00000000000..7f95201349c --- /dev/null +++ b/sale_product_matrix_secondary_unit/__manifest__.py @@ -0,0 +1,31 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Secondary unit in product matrix", + "version": "15.0.1.0.0", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/product-attribute", + "category": "Sales Management", + "maintainers": ["chienandalu"], + "depends": [ + "sale_management", + "sale_product_matrix", + "sale_order_secondary_unit", + ], + "data": [ + "views/sale_order_views.xml", + ], + "assets": { + "web.assets_backend": [ + "sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js", + "sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js", + ], + "web.assets_qweb": [ + "sale_product_matrix_secondary_unit/static/src/xml/**/*", + ], + "web.assets_tests": [ + "sale_product_matrix_secondary_unit/static/tests/tours/**/*", + ], + }, +} diff --git a/sale_product_matrix_secondary_unit/i18n/sale_product_matrix_secondary_unit.pot b/sale_product_matrix_secondary_unit/i18n/sale_product_matrix_secondary_unit.pot new file mode 100644 index 00000000000..0e0ec2fe29d --- /dev/null +++ b/sale_product_matrix_secondary_unit/i18n/sale_product_matrix_secondary_unit.pot @@ -0,0 +1,76 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_product_matrix_secondary_unit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml:0 +#, python-format +msgid "(main unit of measure)" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js:0 +#, python-format +msgid "Choose Product Variants" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js:0 +#, python-format +msgid "Close" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js:0 +#, python-format +msgid "Confirm" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#: model:ir.model.fields,field_description:sale_product_matrix_secondary_unit.field_sale_order_line__force_product_configurator +msgid "Force Product Configurator" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#: model:ir.model,name:sale_product_matrix_secondary_unit.model_product_template +msgid "Product Template" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#: model:ir.model,name:sale_product_matrix_secondary_unit.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#: model:ir.model,name:sale_product_matrix_secondary_unit.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml:0 +#, python-format +msgid "Secondary Unit" +msgstr "" + +#. module: sale_product_matrix_secondary_unit +#. openerp-web +#: code:addons/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml:0 +#, python-format +msgid "Secondary unit" +msgstr "" diff --git a/sale_product_matrix_secondary_unit/models/__init__.py b/sale_product_matrix_secondary_unit/models/__init__.py new file mode 100644 index 00000000000..b48bb2dda30 --- /dev/null +++ b/sale_product_matrix_secondary_unit/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_template +from . import sale_order diff --git a/sale_product_matrix_secondary_unit/models/product_template.py b/sale_product_matrix_secondary_unit/models/product_template.py new file mode 100644 index 00000000000..5a7c6b3c4c3 --- /dev/null +++ b/sale_product_matrix_secondary_unit/models/product_template.py @@ -0,0 +1,21 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _get_template_matrix(self, **kwargs): + matrix = super()._get_template_matrix(**kwargs) + # The default secondary unit + if self.sale_secondary_uom_id: + matrix["secondary_unit_id"] = self.sale_secondary_uom_id.id + # Optional secondary units + if self.secondary_uom_ids: + matrix["secondary_units"] = [ + {"name": f"{su.name} {su.factor} {su.sudo().uom_id.name}", "id": su.id} + for su in self.secondary_uom_ids + ] + matrix["uom_name"] = self.uom_id.name + return matrix diff --git a/sale_product_matrix_secondary_unit/models/sale_order.py b/sale_product_matrix_secondary_unit/models/sale_order.py new file mode 100644 index 00000000000..5693530aaac --- /dev/null +++ b/sale_product_matrix_secondary_unit/models/sale_order.py @@ -0,0 +1,117 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import json + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _get_matrix(self, product_template): + order_lines = self.order_line.filtered( + lambda line: line.product_template_id == product_template + ) + # Check if the secondary_uom_id is the same across all the order lines + is_same_secondary_uom = all( + x.secondary_uom_id == order_lines[0].secondary_uom_id for x in order_lines + ) + # TODO: Improve this case handling + if not is_same_secondary_uom: + matrix = super()._get_matrix(product_template) + matrix.pop("secondary_units", None) + return matrix + # Whether true or false... + matrix = super( + SaleOrder, + self.with_context( + get_matrix_secondary_unit_id=order_lines.secondary_uom_id + ), + )._get_matrix(product_template) + # There could be a default secondary in unit in which case we'll set it directly + # TODO: We should be able to flag the lines as already set by the matrix somehow + # so if there's no secondary unit selected it doesn't default to that default + # secondary unit every time. + matrix["secondary_unit_id"] = order_lines.secondary_uom_id.id or ( + not order_lines and matrix.get("secondary_unit_id") + ) + return matrix + + @api.onchange("grid") + def _apply_grid(self): + if not self.grid or not self.grid_update: + return super()._apply_grid() + grid = json.loads(self.grid) + if "secondary_unit" not in grid: + return super()._apply_grid() + # In case that only the secondary unit is changed we need to set it manually + if not grid.get("changed"): + lines = self.order_line.filtered( + lambda x, grid_template=self.grid_product_tmpl_id: grid_template + == x.product_template_id + ) + lines.secondary_uom_id = self.env["product.secondary.unit"].browse( + grid["secondary_unit"] + ) + return super( + SaleOrder, self.with_context(grid_secondary_unit_id=grid["secondary_unit"]) + )._apply_grid() + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + force_product_configurator = fields.Boolean( + compute="_compute_force_product_configurator" + ) + + @api.depends("secondary_uom_id") + def _compute_force_product_configurator(self): + """Checks if there are matrix products with the same template and different + secondary unit for every order""" + self.force_product_configurator = False + for order in self.order_id: + product_templates = order.order_line.product_template_id.filtered( + lambda x: x.product_add_mode == "matrix" + ) + for product_template in product_templates: + order_lines = order.order_line.filtered( + lambda x: x.product_template_id == product_template + ) + if not all( + x.secondary_uom_id == order_lines[0].secondary_uom_id + for x in order_lines + ): + self.force_product_configurator = True + + @api.onchange("product_id") + def product_id_change(self): + if "grid_secondary_unit_id" not in self.env.context: + return super().product_id_change() + secondary_uom_id = self.env.context.get("grid_secondary_unit_id") + product_uom_qty = self.product_uom_qty + if not secondary_uom_id: + self.secondary_uom_qty = False + res = super( + SaleOrderLine, self.with_context(skip_secondary_uom_default=True) + ).product_id_change() + self.secondary_uom_id = self.env["product.secondary.unit"].browse( + secondary_uom_id + ) + if self.secondary_uom_id: + self.secondary_uom_qty = product_uom_qty + self.onchange_product_uom_for_secondary() + else: + self.product_uom_qty = product_uom_qty + return res + + def mapped(self, func): + # HACK: Use secondary_uom_qty when needed to avoid reparsing the matrix + if ( + self.env.context.get("get_matrix_secondary_unit_id") + and func + and isinstance(func, str) + and func == "product_uom_qty" + ): + func = "secondary_uom_qty" + return super().mapped(func) diff --git a/sale_product_matrix_secondary_unit/readme/CONFIGURE.md b/sale_product_matrix_secondary_unit/readme/CONFIGURE.md new file mode 100644 index 00000000000..f51340e4143 --- /dev/null +++ b/sale_product_matrix_secondary_unit/readme/CONFIGURE.md @@ -0,0 +1,8 @@ +To configure this module, you need to: + +- Go to a product with variants or create one. +- In the *Attributes and variants* tab, *Sales Variant Selection* section, select + **Order grid entry**. +- In the *General information* tab, *Secondary unit of measure* section, add one or + several secondary units. +- You can also set a default **Sale secondary unit**. diff --git a/sale_product_matrix_secondary_unit/readme/CONTRIBUTORS.md b/sale_product_matrix_secondary_unit/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..9967361ba97 --- /dev/null +++ b/sale_product_matrix_secondary_unit/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +* Tecnativa (https://www.tecnativa.com) + * David Vidal diff --git a/sale_product_matrix_secondary_unit/readme/DESCRIPTION.md b/sale_product_matrix_secondary_unit/readme/DESCRIPTION.md new file mode 100644 index 00000000000..bc455dea131 --- /dev/null +++ b/sale_product_matrix_secondary_unit/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +With this module we'll be able to set the secondary units in the product matrix for +a quick quotation for those products using this kind of configurator. diff --git a/sale_product_matrix_secondary_unit/readme/ROADMAP.md b/sale_product_matrix_secondary_unit/readme/ROADMAP.md new file mode 100644 index 00000000000..4866c30bf95 --- /dev/null +++ b/sale_product_matrix_secondary_unit/readme/ROADMAP.md @@ -0,0 +1,6 @@ +- When could have several lines from the same template with different secondary units. + In that case, the use of the matrix is discarded to avoid missmatching values. From + that moment, the products are forced to be configured with the regular product + configurator. +- The client side is roughly implemented right now. Probably we'll fix some of the + most obvious bugs but our roadmap is headed to v17 with the Owl webclient. diff --git a/sale_product_matrix_secondary_unit/readme/USAGE.md b/sale_product_matrix_secondary_unit/readme/USAGE.md new file mode 100644 index 00000000000..2fd461cc6e7 --- /dev/null +++ b/sale_product_matrix_secondary_unit/readme/USAGE.md @@ -0,0 +1,6 @@ +To use this module, you need to: + +1. Go to a sales order and add the configured product. +2. The matrix dialog will show up and there you'll be able to select the available + secondary units. +3. If a default sale secondary unit is set, it will selected in the dialog already. diff --git a/sale_product_matrix_secondary_unit/static/description/icon.png b/sale_product_matrix_secondary_unit/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +Secondary unit in product matrix + + + +
+

Secondary unit in product matrix

+ + +

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

+

With this module we’ll be able to set the secondary units in the product +matrix for a quick quotation for those products using this kind of +configurator.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  • Go to a product with variants or create one.
  • +
  • In the Attributes and variants tab, Sales Variant Selection +section, select Order grid entry.
  • +
  • In the General information tab, Secondary unit of measure section, +add one or several secondary units.
  • +
  • You can also set a default Sale secondary unit.
  • +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to a sales order and add the configured product.
  2. +
  3. The matrix dialog will show up and there you’ll be able to select the +available secondary units.
  4. +
  5. If a default sale secondary unit is set, it will selected in the +dialog already.
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • When could have several lines from the same template with different +secondary units. In that case, the use of the matrix is discarded to +avoid missmatching values. From that moment, the products are forced +to be configured with the regular product configurator.
  • +
  • The client side is roughly implemented right now. Probably we’ll fix +some of the most obvious bugs but our roadmap is headed to v17 with +the Owl webclient.
  • +
+
+
+

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

+
    +
  • Tecnativa
  • +
+
+
+

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.

+

Current maintainer:

+

chienandalu

+

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

+

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

+
+
+
+ + diff --git a/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js b/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js new file mode 100644 index 00000000000..84599c84fad --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js @@ -0,0 +1,54 @@ +odoo.define( + "sale_product_matrix_secondary_unit.product_configurator", + function (require) { + "use strict"; + const ProductConfiguratorWidget = require("sale_product_configurator.product_configurator"); + + ProductConfiguratorWidget.include({ + /* + * @override + * Whenever the secondary units differ for the same template, we'll force the + * regular configurator. + */ + _openConfigurator: function (result) { + if (this.recordData.force_product_configurator) { + result.mode = "configurator"; + } + return this._super.apply(this, arguments); + }, + /* + * @override + * Whenever the secondary units differ for the same template, we'll force the + * regular configurator. + */ + _onEditProductConfiguration: function () { + if (!this.recordData.force_product_configurator) { + this._super.apply(this, arguments); + return; + } + this._openProductConfigurator( + { + configuratorMode: "edit", + default_product_template_id: + this.recordData.product_template_id.data.id, + default_pricelist_id: this._getPricelistId(), + default_product_template_attribute_value_ids: + this._convertFromMany2Many( + this.recordData.product_template_attribute_value_ids + ), + default_product_no_variant_attribute_value_ids: + this._convertFromMany2Many( + this.recordData.product_no_variant_attribute_value_ids + ), + default_product_custom_attribute_value_ids: + this._convertFromOne2Many( + this.recordData.product_custom_attribute_value_ids + ), + default_quantity: this.recordData.product_uom_qty, + }, + this.dataPointID + ); + }, + }); + } +); diff --git a/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js b/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js new file mode 100644 index 00000000000..3ccc79f59f2 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js @@ -0,0 +1,176 @@ +odoo.define( + "sale_product_matrix_secondary_unit.section_and_note_widget", + function (require) { + "use strict"; + const Dialog = require("web.Dialog"); + const core = require("web.core"); + const _t = core._t; + const qweb = core.qweb; + const {Markup} = require("web.utils"); + const fieldRegistry = require("web.field_registry"); + const {format} = require("web.field_utils"); + require("account.section_and_note_backend"); + const SectionAndNoteFieldOne2Many = fieldRegistry.get( + "section_and_note_one2many" + ); + + SectionAndNoteFieldOne2Many.include({ + /** + * @override + * + */ + _applyGrid: function ( + changes, + productTemplateId, + secondary_unit_changed, + secondary_unit, + changed + ) { + if (!secondary_unit_changed && !secondary_unit) { + return this._super.apply(this, arguments); + } + var grid = { + changes: changes, + product_template_id: productTemplateId, + changed: changed, + }; + if (secondary_unit_changed || (secondary_unit && changes.length)) { + grid.secondary_unit = secondary_unit || false; + } + this.trigger_up("field_changed", { + dataPointID: this.dataPointID, + changes: { + grid: JSON.stringify(grid), + // To say that the changes to grid have to be applied to the SO. + grid_update: true, + }, + viewType: "form", + }); + }, + + /** + * @override + * + */ + _openMatrixConfigurator: function ( + jsonInfo, + productTemplateId, + editedCellAttributes + ) { + var self = this; + var infos = JSON.parse(jsonInfo); + if (!infos.secondary_units || !infos.secondary_units.length) { + return this._super.apply(this, arguments); + } + self.secondary_unit_id = infos.secondary_unit_id; + var MatrixDialog = new Dialog(this, { + title: _t("Choose Product Variants"), + size: "extra-large", + $content: $( + qweb.render("sale_product_matrix_secondary_unit.matrix", { + header: infos.header, + rows: infos.matrix, + secondary_unit_id: infos.secondary_unit_id, + secondary_units: infos.secondary_units, + uom_name: infos.uom_name, + format({price, currency_id}) { + if (!price) { + return ""; + } + const sign = price < 0 ? "-" : "+"; + const formatted = format.monetary( + Math.abs(price), + null, + {currency_id} + ); + return Markup`${sign} ${formatted}`; + }, + }) + ), + buttons: [ + { + text: _t("Confirm"), + classes: "btn-primary", + close: true, + click: function () { + var $inputs = this.$(".o_matrix_input"); + var matrixChanges = []; + _.each($inputs, function (matrixInput) { + if ( + (matrixInput.value && + matrixInput.value !== + matrixInput.attributes.value + .nodeValue) || + matrixInput.attributes.value.nodeValue > 0 + ) { + matrixChanges.push({ + qty: parseFloat(matrixInput.value), + ptav_ids: + matrixInput.attributes.ptav_ids.nodeValue + .split(",") + .map(function (id) { + return parseInt(id, 10); + }), + changed: + matrixInput.value && + matrixInput.value !== + matrixInput.attributes.value + .nodeValue, + }); + } + }); + var changed = matrixChanges.reduce( + (is_changed, variant) => + is_changed || variant.changed, + false + ); + var $secondary_unit = this.$( + ".o_matrix_secondary_unit" + ); + var secondary_unit_changed = false; + var secondary_unit = parseInt( + $secondary_unit.val() || 0, + 10 + ); + if (secondary_unit !== self.secondary_unit_id) { + secondary_unit_changed = true; + } + if ( + matrixChanges.length > 0 || + secondary_unit_changed + ) { + self._applyGrid( + matrixChanges, + productTemplateId, + secondary_unit_changed, + secondary_unit, + changed + ); + } + }, + }, + {text: _t("Close"), close: true}, + ], + }).open(); + + MatrixDialog.opened(function () { + MatrixDialog.$content + .closest(".o_dialog_container") + .removeClass("d-none"); + if (editedCellAttributes.length > 0) { + var str = editedCellAttributes.toString(); + MatrixDialog.$content + .find(".o_matrix_input") + .filter( + (k, v) => v.attributes.ptav_ids.nodeValue === str + )[0] + .focus(); + } else { + MatrixDialog.$content.find(".o_matrix_input:first()").focus(); + } + }); + }, + }); + return SectionAndNoteFieldOne2Many; + } +); diff --git a/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml new file mode 100644 index 00000000000..71cee50e800 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml @@ -0,0 +1,39 @@ + + + + + +
+
+ +
+ +
+
+
+
diff --git a/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js b/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js new file mode 100644 index 00000000000..0f4095e58a7 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js @@ -0,0 +1,106 @@ +odoo.define( + "sale_product_matrix_secondary_unit.sale_matrix_secondary_unit", + function (require) { + "use strict"; + var tour = require("web_tour.tour"); + + const common_steps = [ + tour.stepUtils.showAppsMenuItem(), + { + trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", + }, + { + trigger: ".o_list_button_add", + extra_trigger: ".o_sale_order", + }, + { + trigger: "div[name=partner_id] input", + run: "text Deco Addict", + }, + { + trigger: ".ui-menu-item > a:contains('Deco Addict')", + auto: true, + }, + { + trigger: "a:contains('Add a product')", + }, + { + trigger: "div[name='product_template_id'] input", + run: "text SecondaryUnitMatrix", + }, + { + trigger: "ul.ui-autocomplete a:contains('SecondaryUnitMatrix')", + }, + ]; + + tour.register( + "sale_matrix_with_secondary_unit", + { + url: "/web", + test: true, + }, + [ + ...common_steps, + { + trigger: "#secondary_unit", + content: "Select the secondary unit", + run: function () { + const select = $("select.o_matrix_secondary_unit"); + const option = select.find("option").filter(function () { + return $(this).text().includes("Unit 1 12.0 Units"); + }); + select.val(option.val()).change(); + }, + }, + { + trigger: ".o_product_variant_matrix", + run: function () { + // Fill the whole matrix with 1 + $(".o_matrix_input").val(1); + }, + }, + { + trigger: "span:contains('Confirm')", + }, + { + trigger: '.o_form_button_save:contains("Save")', + }, + { + // Ensure the form is saved before closing the browser + trigger: '.o_form_button_edit:contains("Edit")', + }, + ] + ); + tour.register( + "sale_matrix_without_secondary_unit", + { + url: "/web", + test: true, + }, + [ + ...common_steps, + { + // This product does not have a secondary unit + trigger: ":not(select#secondary_unit)", + }, + { + trigger: ".o_product_variant_matrix", + run: function () { + // Fill the whole matrix with 1 + $(".o_matrix_input").val(1); + }, + }, + { + trigger: "span:contains('Confirm')", + }, + { + trigger: '.o_form_button_save:contains("Save")', + }, + { + // Ensure the form is saved before closing the browser + trigger: '.o_form_button_edit:contains("Edit")', + }, + ] + ); + } +); diff --git a/sale_product_matrix_secondary_unit/tests/__init__.py b/sale_product_matrix_secondary_unit/tests/__init__.py new file mode 100644 index 00000000000..1b496aadcae --- /dev/null +++ b/sale_product_matrix_secondary_unit/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_product_matrix_secondary_unit diff --git a/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py b/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py new file mode 100644 index 00000000000..b1f2b1b1f19 --- /dev/null +++ b/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py @@ -0,0 +1,107 @@ +from odoo.tests import Form, RecordCapturer, tagged +from odoo.tests.common import HttpCase + + +@tagged("post_install", "-at_install") +class TestSaleProductMatrixSecondaryUnit(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Ensure the UoM group is enabled + config = Form(cls.env["res.config.settings"]) + config.group_uom = True + config = config.save() + config.execute() + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.attribute1 = cls.env["product.attribute"].create( + {"name": "SecUnit 1", "create_variant": "always", "sequence": 1} + ) + cls.attribute2 = cls.env["product.attribute"].create( + {"name": "SecUnit 2", "create_variant": "always", "sequence": 2} + ) + cls.attribute_value1 = cls.env["product.attribute.value"].create( + [{"name": "SecUnitVal 1", "attribute_id": cls.attribute1.id}] + ) + cls.attribute_value2 = cls.env["product.attribute.value"].create( + [{"name": "SecUnitVal 2", "attribute_id": cls.attribute1.id}] + ) + cls.attribute_value3 = cls.env["product.attribute.value"].create( + [{"name": "SecUnitVal 3", "attribute_id": cls.attribute2.id}] + ) + cls.attribute_value4 = cls.env["product.attribute.value"].create( + [{"name": "SecUnitVal 4", "attribute_id": cls.attribute2.id}] + ) + cls.matrix_template = cls.env["product.template"].create( + { + "name": "SecondaryUnitMatrix", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + "product_add_mode": "matrix", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.attribute1.id, + "value_ids": [(6, 0, cls.attribute1.value_ids.ids)], + }, + ), + ( + 0, + 0, + { + "attribute_id": cls.attribute2.id, + "value_ids": [(6, 0, cls.attribute2.value_ids.ids)], + }, + ), + ], + } + ) + + def test_sale_matrix_with_secondary_unit(self): + # Set the template as configurable by matrix. + SecondaryUnit = self.env["product.secondary.unit"] + secondary_unit_1 = SecondaryUnit.create( + { + "name": "Unit 1", + "product_tmpl_id": self.matrix_template.id, + "uom_id": self.uom_unit.id, + "factor": 12.0, + } + ) + with RecordCapturer(self.env["sale.order"], []) as capture: + self.start_tour("/web", "sale_matrix_with_secondary_unit", login="admin") + new_sale = capture.records + # Ensures a SO has been created with exactly 4 lines + self.assertEqual(len(new_sale.order_line), 4) + self.assertEqual(new_sale.order_line[0].secondary_uom_id, secondary_unit_1) + self.assertEqual(new_sale.order_line[0].secondary_uom_qty, 1) + self.assertEqual(new_sale.order_line[0].product_uom_qty, 12) + self.assertEqual(new_sale.order_line[1].secondary_uom_id, secondary_unit_1) + self.assertEqual(new_sale.order_line[1].secondary_uom_qty, 1) + self.assertEqual(new_sale.order_line[1].product_uom_qty, 12) + self.assertEqual(new_sale.order_line[2].secondary_uom_id, secondary_unit_1) + self.assertEqual(new_sale.order_line[2].secondary_uom_qty, 1) + self.assertEqual(new_sale.order_line[2].product_uom_qty, 12) + self.assertEqual(new_sale.order_line[3].secondary_uom_id, secondary_unit_1) + self.assertEqual(new_sale.order_line[3].secondary_uom_qty, 1) + self.assertEqual(new_sale.order_line[3].product_uom_qty, 12) + + def atest_sale_matrix_without_secondary_unit(self): + with RecordCapturer(self.env["sale.order"], []) as capture: + self.start_tour("/web", "sale_matrix_without_secondary_unit", login="admin") + new_sale = capture.records + # Ensures a SO has been created with exactly 4 lines + self.assertEqual(len(new_sale.order_line), 4) + self.assertFalse(new_sale.order_line[0].secondary_uom_id) + self.assertEqual(new_sale.order_line[0].secondary_uom_qty, 0) + self.assertEqual(new_sale.order_line[0].product_uom_qty, 1) + self.assertFalse(new_sale.order_line[1].secondary_uom_id) + self.assertEqual(new_sale.order_line[1].secondary_uom_qty, 0) + self.assertEqual(new_sale.order_line[1].product_uom_qty, 1) + self.assertFalse(new_sale.order_line[2].secondary_uom_id) + self.assertEqual(new_sale.order_line[2].secondary_uom_qty, 0) + self.assertEqual(new_sale.order_line[2].product_uom_qty, 1) + self.assertFalse(new_sale.order_line[3].secondary_uom_id) + self.assertEqual(new_sale.order_line[3].secondary_uom_qty, 0) + self.assertEqual(new_sale.order_line[3].product_uom_qty, 1) diff --git a/sale_product_matrix_secondary_unit/views/sale_order_views.xml b/sale_product_matrix_secondary_unit/views/sale_order_views.xml new file mode 100644 index 00000000000..4d6fdf4b332 --- /dev/null +++ b/sale_product_matrix_secondary_unit/views/sale_order_views.xml @@ -0,0 +1,29 @@ + + + + + sale.order + + + + + + + + + + + + + + From 6826e98aa03dc18e5f3b742e4f51ab2a0682397d Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Wed, 25 Jun 2025 10:40:23 -0500 Subject: [PATCH 2/3] [IMP] sale_product_matrix_secondary_unit: pre-commit auto fixes --- sale_product_matrix_secondary_unit/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 sale_product_matrix_secondary_unit/pyproject.toml diff --git a/sale_product_matrix_secondary_unit/pyproject.toml b/sale_product_matrix_secondary_unit/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_product_matrix_secondary_unit/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" From 2e9c3e3ccf78eb70e9ef5b7058545e4fe39eb373 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 21 Jul 2025 12:37:20 -0500 Subject: [PATCH 3/3] [MIG] sale_product_matrix_secondary_unit: Migration to version 17.0 --- sale_product_matrix_secondary_unit/README.rst | 18 +- .../__manifest__.py | 10 +- .../models/sale_order.py | 60 +++--- .../static/description/index.html | 8 +- .../src/js/product_matrix_configurator.js | 54 ------ .../src/js/product_matrix_dialog.esm.js | 54 ++++++ .../static/src/js/sale_product_field.esm.js | 49 +++++ .../static/src/js/section_and_note_widget.js | 176 ------------------ .../static/src/xml/product_matrix.xml | 17 +- .../static/src/xml/product_matrix_dialog.xml | 15 ++ ..._product_matrix_secondary_unit_tour.esm.js | 86 +++++++++ ...sale_product_matrix_secondary_unit_tour.js | 106 ----------- ...test_sale_product_matrix_secondary_unit.py | 11 +- .../views/sale_order_views.xml | 2 +- 14 files changed, 266 insertions(+), 400 deletions(-) delete mode 100644 sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js create mode 100644 sale_product_matrix_secondary_unit/static/src/js/product_matrix_dialog.esm.js create mode 100644 sale_product_matrix_secondary_unit/static/src/js/sale_product_field.esm.js delete mode 100644 sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js create mode 100644 sale_product_matrix_secondary_unit/static/src/xml/product_matrix_dialog.xml create mode 100644 sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.esm.js delete mode 100644 sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js diff --git a/sale_product_matrix_secondary_unit/README.rst b/sale_product_matrix_secondary_unit/README.rst index 78805c3892c..2149fb69615 100644 --- a/sale_product_matrix_secondary_unit/README.rst +++ b/sale_product_matrix_secondary_unit/README.rst @@ -17,13 +17,13 @@ Secondary unit in product matrix :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--attribute-lightgray.png?logo=github - :target: https://github.com/OCA/product-attribute/tree/15.0/sale_product_matrix_secondary_unit + :target: https://github.com/OCA/product-attribute/tree/17.0/sale_product_matrix_secondary_unit :alt: OCA/product-attribute .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/product-attribute-15-0/product-attribute-15-0-sale_product_matrix_secondary_unit + :target: https://translation.odoo-community.org/projects/product-attribute-17-0/product-attribute-17-0-sale_product_matrix_secondary_unit :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-attribute&target_branch=15.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -77,7 +77,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -109,14 +109,14 @@ 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. -.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px - :target: https://github.com/chienandalu - :alt: chienandalu +.. |maintainer-carlos-lopez-tecnativa| image:: https://github.com/carlos-lopez-tecnativa.png?size=40px + :target: https://github.com/carlos-lopez-tecnativa + :alt: carlos-lopez-tecnativa Current `maintainer `__: -|maintainer-chienandalu| +|maintainer-carlos-lopez-tecnativa| -This module is part of the `OCA/product-attribute `_ project on GitHub. +This module is part of the `OCA/product-attribute `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_product_matrix_secondary_unit/__manifest__.py b/sale_product_matrix_secondary_unit/__manifest__.py index 7f95201349c..caffa8b674c 100644 --- a/sale_product_matrix_secondary_unit/__manifest__.py +++ b/sale_product_matrix_secondary_unit/__manifest__.py @@ -2,12 +2,12 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Secondary unit in product matrix", - "version": "15.0.1.0.0", + "version": "17.0.1.0.0", "author": "Tecnativa, Odoo Community Association (OCA)", "license": "AGPL-3", "website": "https://github.com/OCA/product-attribute", "category": "Sales Management", - "maintainers": ["chienandalu"], + "maintainers": ["carlos-lopez-tecnativa"], "depends": [ "sale_management", "sale_product_matrix", @@ -18,10 +18,8 @@ ], "assets": { "web.assets_backend": [ - "sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js", - "sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js", - ], - "web.assets_qweb": [ + "sale_product_matrix_secondary_unit/static/src/js/sale_product_field.esm.js", + "sale_product_matrix_secondary_unit/static/src/js/product_matrix_dialog.esm.js", "sale_product_matrix_secondary_unit/static/src/xml/**/*", ], "web.assets_tests": [ diff --git a/sale_product_matrix_secondary_unit/models/sale_order.py b/sale_product_matrix_secondary_unit/models/sale_order.py index 5693530aaac..38bd75be775 100644 --- a/sale_product_matrix_secondary_unit/models/sale_order.py +++ b/sale_product_matrix_secondary_unit/models/sale_order.py @@ -10,7 +10,8 @@ class SaleOrder(models.Model): def _get_matrix(self, product_template): order_lines = self.order_line.filtered( - lambda line: line.product_template_id == product_template + lambda line: line.product_id + and line.product_template_id == product_template ) # Check if the secondary_uom_id is the same across all the order lines is_same_secondary_uom = all( @@ -45,17 +46,40 @@ def _apply_grid(self): if "secondary_unit" not in grid: return super()._apply_grid() # In case that only the secondary unit is changed we need to set it manually + secondary_unit = self.env["product.secondary.unit"].browse( + grid["secondary_unit"] + ) if not grid.get("changed"): lines = self.order_line.filtered( lambda x, grid_template=self.grid_product_tmpl_id: grid_template == x.product_template_id ) - lines.secondary_uom_id = self.env["product.secondary.unit"].browse( - grid["secondary_unit"] + lines.secondary_uom_id = secondary_unit + res = super()._apply_grid() + Attrib = self.env["product.template.attribute.value"] + dirty_cells = grid["changes"] + product_template = self.env["product.template"].browse( + grid["product_template_id"] + ) + for cell in dirty_cells: + combination = Attrib.browse(cell["ptav_ids"]) + no_variant_attr_values = ( + combination - combination._without_no_variant_attributes() + ) + # create or find product variant from combination + product = product_template._create_product_variant(combination) + order_lines = self.order_line.filtered( + lambda line, + product=product, + no_variant_attr_values=no_variant_attr_values: line.product_id.id + == product.id + and line.product_no_variant_attribute_value_ids.ids + == no_variant_attr_values.ids ) - return super( - SaleOrder, self.with_context(grid_secondary_unit_id=grid["secondary_unit"]) - )._apply_grid() + order_lines.secondary_uom_id = secondary_unit + order_lines.secondary_uom_qty = cell["qty"] + order_lines._compute_helper_target_field_qty() + return res class SaleOrderLine(models.Model): @@ -76,7 +100,8 @@ def _compute_force_product_configurator(self): ) for product_template in product_templates: order_lines = order.order_line.filtered( - lambda x: x.product_template_id == product_template + lambda x, product_template=product_template: x.product_template_id + == product_template ) if not all( x.secondary_uom_id == order_lines[0].secondary_uom_id @@ -84,27 +109,6 @@ def _compute_force_product_configurator(self): ): self.force_product_configurator = True - @api.onchange("product_id") - def product_id_change(self): - if "grid_secondary_unit_id" not in self.env.context: - return super().product_id_change() - secondary_uom_id = self.env.context.get("grid_secondary_unit_id") - product_uom_qty = self.product_uom_qty - if not secondary_uom_id: - self.secondary_uom_qty = False - res = super( - SaleOrderLine, self.with_context(skip_secondary_uom_default=True) - ).product_id_change() - self.secondary_uom_id = self.env["product.secondary.unit"].browse( - secondary_uom_id - ) - if self.secondary_uom_id: - self.secondary_uom_qty = product_uom_qty - self.onchange_product_uom_for_secondary() - else: - self.product_uom_qty = product_uom_qty - return res - def mapped(self, func): # HACK: Use secondary_uom_qty when needed to avoid reparsing the matrix if ( diff --git a/sale_product_matrix_secondary_unit/static/description/index.html b/sale_product_matrix_secondary_unit/static/description/index.html index 218cf2f8af1..e82704f76d5 100644 --- a/sale_product_matrix_secondary_unit/static/description/index.html +++ b/sale_product_matrix_secondary_unit/static/description/index.html @@ -369,7 +369,7 @@

Secondary unit in product matrix

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:34455330ff1ccd5b50ea1363108cddf7ace04951394d5702f2aadc9c3d9ee471 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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

With this module we’ll be able to set the secondary units in the product matrix for a quick quotation for those products using this kind of configurator.

@@ -428,7 +428,7 @@

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.

+feedback.

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

@@ -458,8 +458,8 @@

Maintainers

mission is to support the collaborative development of Odoo features and promote its widespread use.

Current maintainer:

-

chienandalu

-

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

+

carlos-lopez-tecnativa

+

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

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

diff --git a/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js b/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js deleted file mode 100644 index 84599c84fad..00000000000 --- a/sale_product_matrix_secondary_unit/static/src/js/product_matrix_configurator.js +++ /dev/null @@ -1,54 +0,0 @@ -odoo.define( - "sale_product_matrix_secondary_unit.product_configurator", - function (require) { - "use strict"; - const ProductConfiguratorWidget = require("sale_product_configurator.product_configurator"); - - ProductConfiguratorWidget.include({ - /* - * @override - * Whenever the secondary units differ for the same template, we'll force the - * regular configurator. - */ - _openConfigurator: function (result) { - if (this.recordData.force_product_configurator) { - result.mode = "configurator"; - } - return this._super.apply(this, arguments); - }, - /* - * @override - * Whenever the secondary units differ for the same template, we'll force the - * regular configurator. - */ - _onEditProductConfiguration: function () { - if (!this.recordData.force_product_configurator) { - this._super.apply(this, arguments); - return; - } - this._openProductConfigurator( - { - configuratorMode: "edit", - default_product_template_id: - this.recordData.product_template_id.data.id, - default_pricelist_id: this._getPricelistId(), - default_product_template_attribute_value_ids: - this._convertFromMany2Many( - this.recordData.product_template_attribute_value_ids - ), - default_product_no_variant_attribute_value_ids: - this._convertFromMany2Many( - this.recordData.product_no_variant_attribute_value_ids - ), - default_product_custom_attribute_value_ids: - this._convertFromOne2Many( - this.recordData.product_custom_attribute_value_ids - ), - default_quantity: this.recordData.product_uom_qty, - }, - this.dataPointID - ); - }, - }); - } -); diff --git a/sale_product_matrix_secondary_unit/static/src/js/product_matrix_dialog.esm.js b/sale_product_matrix_secondary_unit/static/src/js/product_matrix_dialog.esm.js new file mode 100644 index 00000000000..977a44c9a96 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/js/product_matrix_dialog.esm.js @@ -0,0 +1,54 @@ +/** @odoo-module **/ + +import {ProductMatrixDialog} from "@product_matrix/js/product_matrix_dialog"; +import {patch} from "@web/core/utils/patch"; + +patch(ProductMatrixDialog.prototype, { + _onConfirm() { + let secondary_unit = document.getElementsByClassName("o_matrix_secondary_unit"); + if (!secondary_unit || !secondary_unit.length) { + return super._onConfirm(...arguments); + } + let secondary_unit_changed = false; + secondary_unit = parseInt(secondary_unit[0].value || 0, 10); + // TODO: enviar datos al server cuando solo cambie la UdM secundaria y no las cantidades + if (secondary_unit !== self.secondary_unit_id) { + secondary_unit_changed = true; + } + // Override the original _onConfirm method to include secondary unit changes + const inputs = document.getElementsByClassName("o_matrix_input"); + const matrixChanges = []; + for (const matrixInput of inputs) { + if ( + (matrixInput.value && + matrixInput.value !== matrixInput.attributes.value.nodeValue) || + matrixInput.attributes.value.nodeValue > 0 + ) { + matrixChanges.push({ + qty: parseFloat(matrixInput.value), + ptav_ids: matrixInput.attributes.ptav_ids.nodeValue + .split(",") + .map((id) => parseInt(id, 10)), + }); + } + } + if (matrixChanges.length > 0 || secondary_unit_changed) { + // NB: server also removes current line opening the matrix + this.props.record.update({ + grid: JSON.stringify({ + changes: matrixChanges, + product_template_id: this.props.product_template_id, + secondary_unit: secondary_unit || false, + }), + grid_update: true, // To say that the changes to grid have to be applied to the SO. + }); + } + this.props.close(); + }, +}); +ProductMatrixDialog.props = { + ...ProductMatrixDialog.props, + secondary_unit_id: {optional: true}, + secondary_units: {type: Array, optional: true}, + uom_name: {type: String, optional: true}, +}; diff --git a/sale_product_matrix_secondary_unit/static/src/js/sale_product_field.esm.js b/sale_product_matrix_secondary_unit/static/src/js/sale_product_field.esm.js new file mode 100644 index 00000000000..6c1ca0d8d1a --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/js/sale_product_field.esm.js @@ -0,0 +1,49 @@ +/** @odoo-module **/ + +import {ProductMatrixDialog} from "@product_matrix/js/product_matrix_dialog"; +import {SaleOrderLineProductField} from "@sale/js/sale_product_field"; +import {patch} from "@web/core/utils/patch"; + +patch(SaleOrderLineProductField.prototype, { + /* + * @override + * Whenever the secondary units differ for the same template, we'll force the + * regular configurator. + */ + async _openGridConfigurator(edit = false) { + if (this.props.record.data.force_product_configurator) { + this._openProductConfigurator(); + return; + } + return super._openGridConfigurator(edit); + }, + /** + * Triggers Matrix Dialog opening + * + * @param {String} jsonInfo matrix dialog content + * @param {integer} productTemplateId product.template id + * @param {Array} editedCellAttributes list of product.template.attribute.value ids + * used to focus on the matrix cell representing the edited line. + * + * @private + * @override + */ + _openMatrixConfigurator(jsonInfo, productTemplateId, editedCellAttributes) { + const infos = JSON.parse(jsonInfo); + if (!infos.secondary_units || !infos.secondary_units.length) { + return super._openMatrixConfigurator(...arguments); + } + this.secondary_unit_id = infos.secondary_unit_id; + this.dialog.add(ProductMatrixDialog, { + header: infos.header, + rows: infos.matrix, + editedCellAttributes: editedCellAttributes.toString(), + product_template_id: productTemplateId, + record: this.props.record.model.root, + // Provide additional properties to the dialog + secondary_unit_id: infos.secondary_unit_id, + secondary_units: infos.secondary_units, + uom_name: infos.uom_name, + }); + }, +}); diff --git a/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js b/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js deleted file mode 100644 index 3ccc79f59f2..00000000000 --- a/sale_product_matrix_secondary_unit/static/src/js/section_and_note_widget.js +++ /dev/null @@ -1,176 +0,0 @@ -odoo.define( - "sale_product_matrix_secondary_unit.section_and_note_widget", - function (require) { - "use strict"; - const Dialog = require("web.Dialog"); - const core = require("web.core"); - const _t = core._t; - const qweb = core.qweb; - const {Markup} = require("web.utils"); - const fieldRegistry = require("web.field_registry"); - const {format} = require("web.field_utils"); - require("account.section_and_note_backend"); - const SectionAndNoteFieldOne2Many = fieldRegistry.get( - "section_and_note_one2many" - ); - - SectionAndNoteFieldOne2Many.include({ - /** - * @override - * - */ - _applyGrid: function ( - changes, - productTemplateId, - secondary_unit_changed, - secondary_unit, - changed - ) { - if (!secondary_unit_changed && !secondary_unit) { - return this._super.apply(this, arguments); - } - var grid = { - changes: changes, - product_template_id: productTemplateId, - changed: changed, - }; - if (secondary_unit_changed || (secondary_unit && changes.length)) { - grid.secondary_unit = secondary_unit || false; - } - this.trigger_up("field_changed", { - dataPointID: this.dataPointID, - changes: { - grid: JSON.stringify(grid), - // To say that the changes to grid have to be applied to the SO. - grid_update: true, - }, - viewType: "form", - }); - }, - - /** - * @override - * - */ - _openMatrixConfigurator: function ( - jsonInfo, - productTemplateId, - editedCellAttributes - ) { - var self = this; - var infos = JSON.parse(jsonInfo); - if (!infos.secondary_units || !infos.secondary_units.length) { - return this._super.apply(this, arguments); - } - self.secondary_unit_id = infos.secondary_unit_id; - var MatrixDialog = new Dialog(this, { - title: _t("Choose Product Variants"), - size: "extra-large", - $content: $( - qweb.render("sale_product_matrix_secondary_unit.matrix", { - header: infos.header, - rows: infos.matrix, - secondary_unit_id: infos.secondary_unit_id, - secondary_units: infos.secondary_units, - uom_name: infos.uom_name, - format({price, currency_id}) { - if (!price) { - return ""; - } - const sign = price < 0 ? "-" : "+"; - const formatted = format.monetary( - Math.abs(price), - null, - {currency_id} - ); - return Markup`${sign} ${formatted}`; - }, - }) - ), - buttons: [ - { - text: _t("Confirm"), - classes: "btn-primary", - close: true, - click: function () { - var $inputs = this.$(".o_matrix_input"); - var matrixChanges = []; - _.each($inputs, function (matrixInput) { - if ( - (matrixInput.value && - matrixInput.value !== - matrixInput.attributes.value - .nodeValue) || - matrixInput.attributes.value.nodeValue > 0 - ) { - matrixChanges.push({ - qty: parseFloat(matrixInput.value), - ptav_ids: - matrixInput.attributes.ptav_ids.nodeValue - .split(",") - .map(function (id) { - return parseInt(id, 10); - }), - changed: - matrixInput.value && - matrixInput.value !== - matrixInput.attributes.value - .nodeValue, - }); - } - }); - var changed = matrixChanges.reduce( - (is_changed, variant) => - is_changed || variant.changed, - false - ); - var $secondary_unit = this.$( - ".o_matrix_secondary_unit" - ); - var secondary_unit_changed = false; - var secondary_unit = parseInt( - $secondary_unit.val() || 0, - 10 - ); - if (secondary_unit !== self.secondary_unit_id) { - secondary_unit_changed = true; - } - if ( - matrixChanges.length > 0 || - secondary_unit_changed - ) { - self._applyGrid( - matrixChanges, - productTemplateId, - secondary_unit_changed, - secondary_unit, - changed - ); - } - }, - }, - {text: _t("Close"), close: true}, - ], - }).open(); - - MatrixDialog.opened(function () { - MatrixDialog.$content - .closest(".o_dialog_container") - .removeClass("d-none"); - if (editedCellAttributes.length > 0) { - var str = editedCellAttributes.toString(); - MatrixDialog.$content - .find(".o_matrix_input") - .filter( - (k, v) => v.attributes.ptav_ids.nodeValue === str - )[0] - .focus(); - } else { - MatrixDialog.$content.find(".o_matrix_input:first()").focus(); - } - }); - }, - }); - return SectionAndNoteFieldOne2Many; - } -); diff --git a/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml index 71cee50e800..5e30062b3c6 100644 --- a/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml +++ b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml @@ -2,31 +2,30 @@ - -
+ +
-
- +
diff --git a/sale_product_matrix_secondary_unit/static/src/xml/product_matrix_dialog.xml b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix_dialog.xml new file mode 100644 index 00000000000..c8d7d5e6d84 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix_dialog.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.esm.js b/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.esm.js new file mode 100644 index 00000000000..53b84cfc226 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.esm.js @@ -0,0 +1,86 @@ +/** @odoo-module */ +/* Copyright 2025 Carlos Lopez - Tecnativa + * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). */ + +import {registry} from "@web/core/registry"; +import {stepUtils} from "@web_tour/tour_service/tour_utils"; + +const common_steps = [ + stepUtils.showAppsMenuItem(), + { + trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", + }, + { + trigger: ".o_list_button_add", + extra_trigger: ".o_sale_order", + }, + { + trigger: "div[name=partner_id] input", + run: "text Deco Addict", + }, + { + trigger: ".ui-menu-item > a:contains('Deco Addict')", + auto: true, + }, + { + trigger: "a:contains('Add a product')", + }, + { + trigger: "div[name='product_template_id'] input", + run: "text SecondaryUnitMatrix", + }, + { + trigger: "ul.ui-autocomplete a:contains('SecondaryUnitMatrix')", + }, +]; +registry.category("web_tour.tours").add("sale_matrix_with_secondary_unit", { + url: "/web", + test: true, + steps: () => [ + ...common_steps, + { + trigger: "#secondary_unit", + content: "Select the secondary unit", + run: function () { + const select = $("select.o_matrix_secondary_unit"); + const option = select.find("option").filter(function () { + return $(this).text().includes("Unit 1 12.0 Units"); + }); + select.val(option.val()).change(); + }, + }, + { + trigger: ".o_matrix_input_table", + run: function () { + // Fill the whole matrix with 1 + $(".o_matrix_input").val(1); + }, + }, + { + trigger: "button:contains('Confirm')", + }, + ...stepUtils.saveForm(), + ], +}); +registry.category("web_tour.tours").add("sale_matrix_without_secondary_unit", { + url: "/web", + test: true, + steps: () => [ + ...common_steps, + { + // This product does not have a secondary unit + trigger: ":not(select#secondary_unit)", + }, + { + trigger: ".o_matrix_input_table", + run: function () { + // Fill the whole matrix with 1 + $(".o_matrix_input").val(1); + }, + }, + { + trigger: "button:contains('Confirm')", + }, + ...stepUtils.saveForm(), + ], +}); diff --git a/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js b/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js deleted file mode 100644 index 0f4095e58a7..00000000000 --- a/sale_product_matrix_secondary_unit/static/tests/tours/sale_product_matrix_secondary_unit_tour.js +++ /dev/null @@ -1,106 +0,0 @@ -odoo.define( - "sale_product_matrix_secondary_unit.sale_matrix_secondary_unit", - function (require) { - "use strict"; - var tour = require("web_tour.tour"); - - const common_steps = [ - tour.stepUtils.showAppsMenuItem(), - { - trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", - }, - { - trigger: ".o_list_button_add", - extra_trigger: ".o_sale_order", - }, - { - trigger: "div[name=partner_id] input", - run: "text Deco Addict", - }, - { - trigger: ".ui-menu-item > a:contains('Deco Addict')", - auto: true, - }, - { - trigger: "a:contains('Add a product')", - }, - { - trigger: "div[name='product_template_id'] input", - run: "text SecondaryUnitMatrix", - }, - { - trigger: "ul.ui-autocomplete a:contains('SecondaryUnitMatrix')", - }, - ]; - - tour.register( - "sale_matrix_with_secondary_unit", - { - url: "/web", - test: true, - }, - [ - ...common_steps, - { - trigger: "#secondary_unit", - content: "Select the secondary unit", - run: function () { - const select = $("select.o_matrix_secondary_unit"); - const option = select.find("option").filter(function () { - return $(this).text().includes("Unit 1 12.0 Units"); - }); - select.val(option.val()).change(); - }, - }, - { - trigger: ".o_product_variant_matrix", - run: function () { - // Fill the whole matrix with 1 - $(".o_matrix_input").val(1); - }, - }, - { - trigger: "span:contains('Confirm')", - }, - { - trigger: '.o_form_button_save:contains("Save")', - }, - { - // Ensure the form is saved before closing the browser - trigger: '.o_form_button_edit:contains("Edit")', - }, - ] - ); - tour.register( - "sale_matrix_without_secondary_unit", - { - url: "/web", - test: true, - }, - [ - ...common_steps, - { - // This product does not have a secondary unit - trigger: ":not(select#secondary_unit)", - }, - { - trigger: ".o_product_variant_matrix", - run: function () { - // Fill the whole matrix with 1 - $(".o_matrix_input").val(1); - }, - }, - { - trigger: "span:contains('Confirm')", - }, - { - trigger: '.o_form_button_save:contains("Save")', - }, - { - // Ensure the form is saved before closing the browser - trigger: '.o_form_button_edit:contains("Edit")', - }, - ] - ); - } -); diff --git a/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py b/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py index b1f2b1b1f19..24fbf0de667 100644 --- a/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py +++ b/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py @@ -1,3 +1,4 @@ +from odoo import Command from odoo.tests import Form, RecordCapturer, tagged from odoo.tests.common import HttpCase @@ -38,17 +39,13 @@ def setUpClass(cls): "uom_po_id": cls.uom_unit.id, "product_add_mode": "matrix", "attribute_line_ids": [ - ( - 0, - 0, + Command.create( { "attribute_id": cls.attribute1.id, "value_ids": [(6, 0, cls.attribute1.value_ids.ids)], }, ), - ( - 0, - 0, + Command.create( { "attribute_id": cls.attribute2.id, "value_ids": [(6, 0, cls.attribute2.value_ids.ids)], @@ -87,7 +84,7 @@ def test_sale_matrix_with_secondary_unit(self): self.assertEqual(new_sale.order_line[3].secondary_uom_qty, 1) self.assertEqual(new_sale.order_line[3].product_uom_qty, 12) - def atest_sale_matrix_without_secondary_unit(self): + def test_sale_matrix_without_secondary_unit(self): with RecordCapturer(self.env["sale.order"], []) as capture: self.start_tour("/web", "sale_matrix_without_secondary_unit", login="admin") new_sale = capture.records diff --git a/sale_product_matrix_secondary_unit/views/sale_order_views.xml b/sale_product_matrix_secondary_unit/views/sale_order_views.xml index 4d6fdf4b332..23cd34975dc 100644 --- a/sale_product_matrix_secondary_unit/views/sale_order_views.xml +++ b/sale_product_matrix_secondary_unit/views/sale_order_views.xml @@ -16,7 +16,7 @@ expr="//field[@name='order_line']/tree//field[@name='secondary_uom_id']" position="before" > - +