diff --git a/sale_product_matrix_secondary_unit/README.rst b/sale_product_matrix_secondary_unit/README.rst new file mode 100644 index 00000000000..2149fb69615 --- /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/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-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=17.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-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-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/__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..caffa8b674c --- /dev/null +++ b/sale_product_matrix_secondary_unit/__manifest__.py @@ -0,0 +1,29 @@ +# 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": "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": ["carlos-lopez-tecnativa"], + "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/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": [ + "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..38bd75be775 --- /dev/null +++ b/sale_product_matrix_secondary_unit/models/sale_order.py @@ -0,0 +1,121 @@ +# 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_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( + 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 + 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 = 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 + ) + 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): + _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, product_template=product_template: 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 + + 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/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" 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 00000000000..1dcc49c24f3 Binary files /dev/null and b/sale_product_matrix_secondary_unit/static/description/icon.png differ diff --git a/sale_product_matrix_secondary_unit/static/description/index.html b/sale_product_matrix_secondary_unit/static/description/index.html new file mode 100644 index 00000000000..e82704f76d5 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/description/index.html @@ -0,0 +1,468 @@ + + + + + +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:

+

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_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/xml/product_matrix.xml b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml new file mode 100644 index 00000000000..5e30062b3c6 --- /dev/null +++ b/sale_product_matrix_secondary_unit/static/src/xml/product_matrix.xml @@ -0,0 +1,38 @@ + + + + + +
+
+ +
+ +
+
+
+
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/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..24fbf0de667 --- /dev/null +++ b/sale_product_matrix_secondary_unit/tests/test_sale_product_matrix_secondary_unit.py @@ -0,0 +1,104 @@ +from odoo import Command +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": [ + Command.create( + { + "attribute_id": cls.attribute1.id, + "value_ids": [(6, 0, cls.attribute1.value_ids.ids)], + }, + ), + Command.create( + { + "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 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 + # 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..23cd34975dc --- /dev/null +++ b/sale_product_matrix_secondary_unit/views/sale_order_views.xml @@ -0,0 +1,29 @@ + + + + + sale.order + + + + + + + + + + + + + +