diff --git a/product_configurator_mrp_quantity/README.rst b/product_configurator_mrp_quantity/README.rst new file mode 100644 index 0000000000..687d7db21b --- /dev/null +++ b/product_configurator_mrp_quantity/README.rst @@ -0,0 +1,96 @@ +================================= +Product Configurator MRP Quantity +================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:063e1950e625643c35ac391930663d43b87029135206c6de4aff60eec0a9295e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--configurator-lightgray.png?logo=github + :target: https://github.com/OCA/product-configurator/tree/17.0/product_configurator_mrp_quantity + :alt: OCA/product-configurator +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-configurator-17-0/product-configurator-17-0-product_configurator_mrp_quantity + :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-configurator&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module enhances the Product Configurator MRP Quantity functionality +by introducing dynamic quantity adjustments within the Product +Configurator Wizard. Users can now define specific attributes that allow +for quantity variations during the configuration process. + +The quantity field becomes visible only when a specific checkbox is +selected on the corresponding attribute line within the product +template. + +By default, the quantity field displays a value of "1" which can be +modified by the user during configuration. + +This functionality offers greater flexibility and customization for +users by enabling: + +Configurable Quantity Adjustments: Define attributes that directly +impact the quantity of materials required for a product based on user +selection. Streamlined MRP Integration: Dynamic quantity adjustments +automatically reflect in the Material Requirements Planning (MRP) +process, ensuring accurate inventory calculations. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Open Source Integrators + +Contributors +------------ + +- Vandan Pandeji <> +- Patrick Wilson <> + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/product-configurator `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_configurator_mrp_quantity/__init__.py b/product_configurator_mrp_quantity/__init__.py new file mode 100644 index 0000000000..9b4296142f --- /dev/null +++ b/product_configurator_mrp_quantity/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/product_configurator_mrp_quantity/__manifest__.py b/product_configurator_mrp_quantity/__manifest__.py new file mode 100644 index 0000000000..f9cb454a82 --- /dev/null +++ b/product_configurator_mrp_quantity/__manifest__.py @@ -0,0 +1,22 @@ +{ + "name": "Product Configurator MRP Quantity", + "version": "17.0.1.0.0", + "category": "Manufacturing", + "summary": "Configuration for adding quantity in product configurator.", + "author": "Open Source Integrators,Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/product-configurator", + "depends": ["product_configurator", "product_configurator_mrp"], + "data": [ + "security/ir.model.access.csv", + "views/product_view.xml", + "views/product_attribute_view.xml", + "wizard/product_configurator_view.xml", + "views/product_config_view.xml", + ], + "images": ["static/description/cover.png"], + "development_status": "Beta", + "maintainer": "Open Source Integrators", + "installable": True, + "auto_install": False, +} diff --git a/product_configurator_mrp_quantity/models/__init__.py b/product_configurator_mrp_quantity/models/__init__.py new file mode 100644 index 0000000000..5ffd0452d9 --- /dev/null +++ b/product_configurator_mrp_quantity/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_config +from . import product_attribute +from . import product diff --git a/product_configurator_mrp_quantity/models/product.py b/product_configurator_mrp_quantity/models/product.py new file mode 100644 index 0000000000..2d9f54732c --- /dev/null +++ b/product_configurator_mrp_quantity/models/product.py @@ -0,0 +1,54 @@ +from odoo import api, fields, models +from odoo.tools.sql import drop_index, index_exists + + +class ProductProductAttributeValueQty(models.Model): + _name = "product.product.attribute.value.qty" + _description = "A link between variants and attributes and the quantity of that combination Fields" + + product_id = fields.Many2one( + "product.product", string="Product Variant", ondelete="cascade" + ) + attr_value_id = fields.Many2one("product.attribute.value", required=True) + qty = fields.Integer(string="Quantity") + + @api.depends("attr_value_id", "qty") + def _compute_display_name(self): + res = super()._compute_display_name() + for rec in self: + if rec.attr_value_id and rec.qty: + rec.display_name = ( + rec.attr_value_id.display_name + "(" + str(rec.qty) + ")" + ) + return res + + +class ProductProduct(models.Model): + _inherit = "product.product" + _rec_name = "config_name" + + @api.depends("product_attribute_value_qty_ids") + def _compute_qty_combination_indices(self): + for product in self: + qty_combination_indices = product.product_attribute_value_qty_ids.mapped( + "qty" + ) + product.qty_combination_indices = ",".join( + [str(i) for i in sorted(qty_combination_indices)] + ) + + product_attribute_value_qty_ids = fields.One2many( + "product.product.attribute.value.qty", "product_id" + ) + qty_combination_indices = fields.Char( + compute="_compute_qty_combination_indices", store=True, index=True + ) + + def init(self): + if index_exists(self.env.cr, "product_product_combination_unique"): + drop_index(self.env.cr, "product_product_combination_unique", self._table) + + self.env.cr.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_qty_attrs_unique ON %s (product_tmpl_id, combination_indices,qty_combination_indices) WHERE active is true" + % self._table + ) diff --git a/product_configurator_mrp_quantity/models/product_attribute.py b/product_configurator_mrp_quantity/models/product_attribute.py new file mode 100644 index 0000000000..5aff464d41 --- /dev/null +++ b/product_configurator_mrp_quantity/models/product_attribute.py @@ -0,0 +1,29 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductAttributeLine(models.Model): + _inherit = "product.template.attribute.line" + + is_qty_required = fields.Boolean(string="Qty Required", copy=False) + + +class ProductAttributePrice(models.Model): + _inherit = "product.template.attribute.value" + + is_qty_required = fields.Boolean( + related="attribute_line_id.is_qty_required", + store=True, + string="Qty Required", + copy=False, + ) + default_qty = fields.Integer("Default Quantity", default=1) + maximum_qty = fields.Integer("Max Quantity", default=1) + + @api.constrains("default_qty", "maximum_qty") + def _check_default_qty_maximum_qty(self): + for rec in self: + if rec.default_qty > rec.maximum_qty: + raise ValidationError( + _("Maximum Qty can't be smaller then Default Qty") + ) diff --git a/product_configurator_mrp_quantity/models/product_config.py b/product_configurator_mrp_quantity/models/product_config.py new file mode 100644 index 0000000000..769eebfd90 --- /dev/null +++ b/product_configurator_mrp_quantity/models/product_config.py @@ -0,0 +1,462 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ProductConfigSession(models.Model): + _inherit = "product.config.session" + + session_value_quantity_ids = fields.One2many( + "product.config.session.value.qty", "session_id", string="Quantities" + ) + + @api.model + def search_variant(self, value_ids=None, product_tmpl_id=None): + products = super().search_variant( + value_ids=value_ids, product_tmpl_id=product_tmpl_id + ) + session_filter_values = [] + product_attrs_qtys = products.product_attribute_value_qty_ids + session_attrs_qtys = self.session_value_quantity_ids + for attr_qty in session_attrs_qtys: + filter_values = product_attrs_qtys.filtered( + lambda prod_attr: prod_attr.attr_value_id.id + == attr_qty.attr_value_id.id + and prod_attr.qty == int(attr_qty.qty) + ) + session_filter_values.extend(filter_values.mapped("product_id").ids) + session_filter_products = self.env["product.product"].browse( + session_filter_values + ) + if session_filter_products: + return session_filter_products + else: + return self.env["product.product"] + + def create_get_variant(self, value_ids=None, custom_vals=None): + result = super().create_get_variant( + value_ids=value_ids, custom_vals=custom_vals + ) + if self.session_value_quantity_ids and result.id != self.product_id.id: + result.product_attribute_value_qty_ids.unlink() + qty_attr_obj = self.env["product.product.attribute.value.qty"] + qty_list = [] + for qty_value in self.session_value_quantity_ids: + qty_attr_dict = { + "product_id": result.id, + "attr_value_id": qty_value.attr_value_id.id, + "qty": qty_value.qty, + } + qty_list.append(qty_attr_dict) + qty_attr_obj.create(qty_list) + return result + + @api.model + def get_variant_vals(self, value_ids=None, custom_vals=None, **kwargs): + values = super().get_variant_vals(value_ids=value_ids, custom_vals=custom_vals) + attrs_value_qty_list = [] + for attr_qty in self.session_value_quantity_ids: + attrs_value_qty_list.append( + ( + 0, + 0, + { + "attr_value_id": attr_qty.attr_value_id.id, + "qty": int(attr_qty.qty), + }, + ) + ) + values.update({"product_attribute_value_qty_ids": attrs_value_qty_list}) + return values + + # ============================ + # OVERRIDE Methods + # ============================ + + def update_session_configuration_value(self, vals, product_tmpl_id=None): + """Update value of configuration in current session + + :param: vals: Dictionary of fields(of configution wizard) and values + :param: product_tmpl_id: record set of preoduct template + :return: True/False + """ + self.ensure_one() + if not product_tmpl_id: + product_tmpl_id = self.product_tmpl_id + + product_configurator_obj = self.env["product.configurator"] + field_prefix = product_configurator_obj._prefixes.get("field_prefix") + custom_field_prefix = product_configurator_obj._prefixes.get( + "custom_field_prefix" + ) + qty_field_prefix = product_configurator_obj._prefixes.get("qty_field") + custom_val = self.get_custom_value_id() + + attr_val_dict = {} + custom_val_dict = {} + qty_val_dict = {} + for attr_line in product_tmpl_id.attribute_line_ids: + attr_id = attr_line.attribute_id.id + field_name = field_prefix + str(attr_id) + custom_field_name = custom_field_prefix + str(attr_id) + qty_field_name = qty_field_prefix + str(attr_id) + + if ( + field_name not in vals + and custom_field_name not in vals + and qty_field_name not in vals + ): + continue + + # Add attribute values from the client except custom attribute + # If a custom value is being written, but field name is not in + # the write dictionary, then it must be a custom value! + # existing_session_attrs_qtys = self.session_value_quantity_ids.filtered(lambda l:l.attr_value_id.attribute_id.id == attr_id and l.qty == int(vals.get(qty_field_name))) + existing_session_attrs_nonqtys = False + if vals.get(qty_field_name): + existing_session_attrs_nonqtys = self.session_value_quantity_ids.filtered( + lambda session_value_id: session_value_id.attr_value_id.attribute_id.id + == attr_id + and session_value_id.qty != int(vals.get(qty_field_name)) + ) + if vals.get(field_name, custom_val.id) != custom_val.id: + if attr_line.multi and isinstance(vals[field_name], list): + if not vals[field_name]: + field_val = None + else: + field_val = [] + qty_field_val = [] + for field_vals in vals[field_name]: + if field_vals[0] == 6: + field_val += field_vals[2] or [] + elif field_vals[0] == 4: + field_val.append(field_vals[1]) + # field_val = [ + # i[1] for i in vals[field_name] if vals[field_name][0] + # ] or vals[field_name][0][1] + elif not attr_line.multi and isinstance(vals[field_name], int): + field_val = vals[field_name] + + if attr_line.is_qty_required and vals.get(qty_field_name): + qty_field_val = vals[qty_field_name] + if self.session_value_quantity_ids: + update_session_ids = self.session_value_quantity_ids.filtered( + lambda local_session: local_session.attr_value_id.attribute_id.id + == attr_id + ) + for sess in update_session_ids: + sess.attr_value_id = field_val + else: + raise UserError( + _("An error occurred while parsing value for attribute %s") + % attr_line.attribute_id.name + ) + attr_val_dict.update({attr_id: field_val}) + if attr_line.is_qty_required and vals.get(qty_field_name): + qty_val_dict.update({attr_id: qty_field_val}) + + # Ensure there is no custom value stored if we have switched + # from custom value to selected attribute value. + if attr_line.custom: + custom_val_dict.update({attr_id: False}) + elif attr_line.custom: + val = vals.get(custom_field_name, False) + if attr_line.attribute_id.custom_type == "binary": + # TODO: Add widget that enables multiple file uploads + val = [{"name": "custom", "datas": vals[custom_field_name]}] + custom_val_dict.update({attr_id: val}) + # Ensure there is no standard value stored if we have switched + # from selected value to custom value. + attr_val_dict.update({attr_id: False}) + + elif ( + vals.get(qty_field_name) + and not qty_val_dict + and attr_line.is_qty_required + ): + existing_session_attrs_nonqtys.write({"qty": vals.get(qty_field_name)}) + self.update_config(attr_val_dict, custom_val_dict, qty_val_dict) + + def update_config( + self, attr_val_dict=None, custom_val_dict=None, qty_val_dict=None + ): + """Update the session object with the given value_ids and custom values. + + Use this method instead of write in order to prevent incompatible + configurations as this removed duplicate values for the same attribute. + + :param attr_val_dict: Dictionary of the form { + int (attribute_id): attribute_value_id OR [attribute_value_ids] + } + + :custom_val_dict: Dictionary of the form { + int (attribute_id): { + 'value': 'custom val', + OR + 'attachment_ids': { + [{ + 'name': 'attachment name', + 'datas': base64_encoded_string + }] + } + } + } + + + """ + if attr_val_dict is None: + attr_val_dict = {} + if custom_val_dict is None: + custom_val_dict = {} + if qty_val_dict is None: + qty_val_dict = {} + update_vals = {} + + value_ids = self.value_ids.ids + for attr_id, vals in attr_val_dict.items(): + attr_val_ids = self.value_ids.filtered( + lambda value_id: value_id.attribute_id.id == int(attr_id) + ).ids + # Remove all values for this attribute and add vals from dict + value_ids = list(set(value_ids) - set(attr_val_ids)) + if not vals: + continue + if isinstance(vals, list): + value_ids += vals + elif isinstance(vals, int): + value_ids.append(vals) + + if value_ids != self.value_ids.ids: + update_vals.update({"value_ids": [(6, 0, value_ids)]}) + if qty_val_dict: + if self.session_value_quantity_ids: + self.session_value_quantity_ids.unlink() + session_qty_list = [] + for k, v in qty_val_dict.items(): + attr_value_id = ( + attr_val_dict + and attr_val_dict.get(k) + or self.value_ids.filtered( + lambda value_id: value_id.attribute_id.id == k + ).id + ) + session_qty_list.append( + (0, 0, {"attr_value_id": attr_value_id, "qty": v}) + ) + update_vals.update({"session_value_quantity_ids": session_qty_list}) + # Remove all custom values included in the custom_vals dict + self.custom_value_ids.filtered( + lambda custom_value_id: custom_value_id.attribute_id.id + in custom_val_dict.keys() + ).unlink() + + if custom_val_dict: + binary_field_ids = ( + self.env["product.attribute"] + .search( + [ + ("id", "in", list(custom_val_dict.keys())), + ("custom_type", "=", "binary"), + ] + ) + .ids + ) + else: + binary_field_ids = [] + + for attr_id, vals in custom_val_dict.items(): + if not vals: + continue + + if "custom_value_ids" not in update_vals: + update_vals["custom_value_ids"] = [] + + custom_vals = {"attribute_id": attr_id} + + if attr_id in binary_field_ids: + attachments = [ + ( + 0, + 0, + {"name": val.get("name"), "datas": val.get("datas")}, + ) + for val in vals + ] + custom_vals.update({"attachment_ids": attachments}) + else: + custom_vals.update({"value": vals}) + + update_vals["custom_value_ids"].append((0, 0, custom_vals)) + self.write(update_vals) + + def create_get_bom(self, variant, product_tmpl_id=None, values=None): + # default_type is set as 'product' when the user navigates + # through menu item "Products". This conflicts + # with the type for mrp.bom when mrpBom.onchange() is executed. + ctx = self.env.context.copy() + if ctx.get("default_type"): + ctx.pop("default_type") + self.env.context = ctx + + if values is None: + values = {} + if product_tmpl_id is None or variant.product_tmpl_id != product_tmpl_id: + product_tmpl_id = variant.product_tmpl_id + + mrpBom = self.env["mrp.bom"] + mrpBomLine = self.env["mrp.bom.line"] + attr_products = variant.product_template_attribute_value_ids.mapped( + "product_attribute_value_id.product_id" + ) + + attr_values = variant.product_template_attribute_value_ids.mapped( + "product_attribute_value_id" + ) + existing_bom = self.env["mrp.bom"].search( + [ + ("product_tmpl_id", "=", product_tmpl_id.id), + ("product_id", "=", variant.id), + ] + ) + session_attr_qty_values = self.session_value_quantity_ids + if existing_bom: + return existing_bom[:1] + + parent_bom = self.env["mrp.bom"].search( + [ + ("product_tmpl_id", "=", product_tmpl_id.id), + ("product_id", "=", False), + ], + order="sequence asc", + limit=1, + ) + bom_type = parent_bom and parent_bom.type or "normal" + bom_lines = [] + if not parent_bom: + # If not Bom, then Cycle through attributes to add their + # related products to the bom lines. + for product in attr_products: + local_session_attr_qty_value = session_attr_qty_values.filtered( + lambda local_session: local_session.attr_value_id.product_id.id + == product.id + ) + bom_line_vals = { + "product_id": product.id, + "product_qty": local_session_attr_qty_value.qty > 0 + and local_session_attr_qty_value.qty + or 1, + } + specs = self.get_onchange_specifications(model="mrp.bom.line") + for key, val in specs.items(): + if val is None: + specs[key] = {} + updates = mrpBomLine.onchange( + bom_line_vals, ["product_id", "product_qty"], specs + ) + values = updates.get("value", {}) + values = self.get_vals_to_write(values=values, model="mrp.bom.line") + values.update(bom_line_vals) + bom_lines.append((0, 0, values)) + else: + # If parent BOM is used, then look through Config Sets + # on parent product's bom to add the products to the bom lines. + for parent_bom_line in parent_bom.bom_line_ids: + if parent_bom_line.config_set_id: + for config in parent_bom_line.config_set_id.configuration_ids: + # Add bom lines if config values are part of attr_values + if set(config.value_ids.ids).issubset(set(attr_values.ids)): + local_session_attr_qty_values = session_attr_qty_values.filtered( + lambda local_session: local_session.attr_value_id.product_id + and local_session.attr_value_id.attribute_id.id + in config.value_ids.mapped("attribute_id").ids + ) + session_attr_qty_values = ( + session_attr_qty_values - local_session_attr_qty_values + ) + if parent_bom_line.bom_id.id == parent_bom.id: + parent_bom_line_vals = { + "product_id": parent_bom_line.product_id.id, + "product_qty": local_session_attr_qty_values.qty > 0 + and parent_bom_line.product_qty + * local_session_attr_qty_values.qty + or parent_bom_line.product_qty, + } + specs = self.get_onchange_specifications( + model="mrp.bom.line" + ) + for key, val in specs.items(): + if val is None: + specs[key] = {} + updates = mrpBomLine.onchange( + parent_bom_line_vals, + ["product_id", "product_qty"], + specs, + ) + values = updates.get("value", {}) + values = self.get_vals_to_write( + values=values, model="mrp.bom.line" + ) + values.update(parent_bom_line_vals) + bom_lines.append((0, 0, parent_bom_line_vals)) + else: + parent_bom_product = parent_bom_line.product_id + local_session_attr_qty_value = session_attr_qty_values.filtered( + lambda local_session: local_session.attr_value_id.product_id.id + == parent_bom_product.id + ) + parent_bom_line_vals = { + "product_id": parent_bom_product.id, + "product_qty": local_session_attr_qty_value.qty > 0 + and local_session_attr_qty_value.qty + * parent_bom_line.product_qty + or parent_bom_line.product_qty, + } + specs = self.get_onchange_specifications(model="mrp.bom.line") + for key, val in specs.items(): + if val is None: + specs[key] = {} + updates = mrpBomLine.onchange( + parent_bom_line_vals, ["product_id", "product_qty"], specs + ) + values = updates.get("value", {}) + values = self.get_vals_to_write(values=values, model="mrp.bom.line") + values.update(parent_bom_line_vals) + bom_lines.append((0, 0, values)) + if bom_lines: + bom_values = { + "product_tmpl_id": self.product_tmpl_id.id, + "product_id": variant.id, + "type": bom_type, + "bom_line_ids": bom_lines, + } + specs = self.get_onchange_specifications(model="mrp.bom") + for key, val in specs.items(): + if val is None: + specs[key] = {} + updates = mrpBom.onchange( + bom_values, + [ + "product_id", + "product_configurator_sale_mrproduct_tmpl_id", + "bom_line_ids", + ], + specs, + ) + values = updates.get("value", {}) + values = self.get_vals_to_write(values=values, model="mrp.bom") + values.update(bom_values) + mrp_bom_id = mrpBom.create(values) + if mrp_bom_id and parent_bom: + for operation_line in parent_bom.operation_ids: + operation_line.copy(default={"bom_id": mrp_bom_id.id}) + return mrp_bom_id + return False + + +class ProductConfigSessionValueQty(models.Model): + _name = "product.config.session.value.qty" + _description = """Helper object to store + the user's choice for any value that has an associated quantity.""" + + session_id = fields.Many2one("product.config.session", ondelete="cascade") + attr_value_id = fields.Many2one("product.attribute.value") + qty = fields.Integer(string="Quantity") diff --git a/product_configurator_mrp_quantity/pyproject.toml b/product_configurator_mrp_quantity/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/product_configurator_mrp_quantity/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_configurator_mrp_quantity/readme/CONTRIBUTORS.md b/product_configurator_mrp_quantity/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..d59a193749 --- /dev/null +++ b/product_configurator_mrp_quantity/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Vandan Pandeji \<\<\>\> +- Patrick Wilson \<\<\>\> diff --git a/product_configurator_mrp_quantity/readme/DESCRIPTION.md b/product_configurator_mrp_quantity/readme/DESCRIPTION.md new file mode 100644 index 0000000000..853e0cae34 --- /dev/null +++ b/product_configurator_mrp_quantity/readme/DESCRIPTION.md @@ -0,0 +1,20 @@ +This module enhances the Product Configurator MRP Quantity functionality +by introducing dynamic quantity adjustments within the Product +Configurator Wizard. Users can now define specific attributes that allow +for quantity variations during the configuration process. + +The quantity field becomes visible only when a specific checkbox is +selected on the corresponding attribute line within the product +template. + +By default, the quantity field displays a value of "1" which can be +modified by the user during configuration. + +This functionality offers greater flexibility and customization for +users by enabling: + +Configurable Quantity Adjustments: Define attributes that directly +impact the quantity of materials required for a product based on user +selection. Streamlined MRP Integration: Dynamic quantity adjustments +automatically reflect in the Material Requirements Planning (MRP) +process, ensuring accurate inventory calculations. diff --git a/product_configurator_mrp_quantity/security/ir.model.access.csv b/product_configurator_mrp_quantity/security/ir.model.access.csv new file mode 100644 index 0000000000..ad970ff866 --- /dev/null +++ b/product_configurator_mrp_quantity/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +product_configurator_mrp_quantity.access_product_config_session_value_qty,access_product_config_session_value_qty,product_configurator_mrp_quantity.model_product_config_session_value_qty,base.group_user,1,1,1,1 + +product_configurator_mrp_quantity.access_product_product_attribute_value_qty,access_product_product_attribute_value_qty,product_configurator_mrp_quantity.model_product_product_attribute_value_qty,base.group_user,1,1,1,1 diff --git a/product_configurator_mrp_quantity/static/description/Odoo-Personal-Computer1.png b/product_configurator_mrp_quantity/static/description/Odoo-Personal-Computer1.png new file mode 100644 index 0000000000..b6b8ad435e Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/Odoo-Personal-Computer1.png differ diff --git a/product_configurator_mrp_quantity/static/description/Odoo-Product-Variant-Values.png b/product_configurator_mrp_quantity/static/description/Odoo-Product-Variant-Values.png new file mode 100644 index 0000000000..baddf50512 Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/Odoo-Product-Variant-Values.png differ diff --git a/product_configurator_mrp_quantity/static/description/Product_Config_qty.png b/product_configurator_mrp_quantity/static/description/Product_Config_qty.png new file mode 100644 index 0000000000..9c52d9fdfe Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/Product_Config_qty.png differ diff --git a/product_configurator_mrp_quantity/static/description/cover.png b/product_configurator_mrp_quantity/static/description/cover.png new file mode 100644 index 0000000000..be5e2578ce Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/cover.png differ diff --git a/product_configurator_mrp_quantity/static/description/icon.png b/product_configurator_mrp_quantity/static/description/icon.png new file mode 100644 index 0000000000..d5d307c648 Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/icon.png differ diff --git a/product_configurator_mrp_quantity/static/description/index.html b/product_configurator_mrp_quantity/static/description/index.html new file mode 100644 index 0000000000..e580f539a7 --- /dev/null +++ b/product_configurator_mrp_quantity/static/description/index.html @@ -0,0 +1,63 @@ +
+
+
+

Odoo Product Configurator MRP Quantity

+

Introducing dynamic quantity adjustments within the Product Configurator Wizard on-demand, easy and error-free

+
+
+
+ Pledra +
+
+
+
+ +
+
+

Qty Required checkbox on the product template attribute line acts as a trigger for the dynamic quantity field in the wizard

+
+
+ +
+
+
+
+ +
+
+

By default, the quantity field displays a value of "1" which can be modified by the user during configuration

+
+
+ +
+
+
+
+ +
+
+

Select Product Template for Variant Configuration

+
+
+ +
+
+
+
+ +
+
+

Compatible with Odoo Enterprise and Community

+

Odoo versions supported: 8 / 9 / 10

+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/product_configurator_mrp_quantity/static/description/odoo-community-interface.png b/product_configurator_mrp_quantity/static/description/odoo-community-interface.png new file mode 100644 index 0000000000..e741d61531 Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/odoo-community-interface.png differ diff --git a/product_configurator_mrp_quantity/static/description/odoo-enterprise-interface.png b/product_configurator_mrp_quantity/static/description/odoo-enterprise-interface.png new file mode 100644 index 0000000000..5b13b3dd19 Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/odoo-enterprise-interface.png differ diff --git a/product_configurator_mrp_quantity/static/description/pledra-logo.png b/product_configurator_mrp_quantity/static/description/pledra-logo.png new file mode 100644 index 0000000000..b82d4ab00d Binary files /dev/null and b/product_configurator_mrp_quantity/static/description/pledra-logo.png differ diff --git a/product_configurator_mrp_quantity/tests/__init__.py b/product_configurator_mrp_quantity/tests/__init__.py new file mode 100644 index 0000000000..a75d29e7ac --- /dev/null +++ b/product_configurator_mrp_quantity/tests/__init__.py @@ -0,0 +1 @@ +from . import test_configurator_quantity diff --git a/product_configurator_mrp_quantity/tests/test_configurator_quantity.py b/product_configurator_mrp_quantity/tests/test_configurator_quantity.py new file mode 100644 index 0000000000..69d0acc7bd --- /dev/null +++ b/product_configurator_mrp_quantity/tests/test_configurator_quantity.py @@ -0,0 +1,235 @@ +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("-at_install", "post_install") +class ConfigurationQtyCreate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.test_i7_pro = cls.env["product.template"].create( + { + "name": "i7 Processor", + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) + cls.attr_ssd = cls.env["product.attribute"].create( + { + "name": "SSD", + "value_ids": [ + (0, 0, {"name": "SSD-512GB"}), + (0, 0, {"name": "SSD-1TB"}), + ], + } + ) + cls.value_ssd256 = cls.env["product.attribute.value"].create( + {"name": "SSD-256GB", "attribute_id": cls.attr_ssd.id} + ) + + cls.attr_ram = cls.env["product.attribute"].create( + { + "name": "RAM", + "value_ids": [ + (0, 0, {"name": "RAM-16GB"}), + (0, 0, {"name": "RAM-32GB"}), + ], + } + ) + cls.value_ram8gb = cls.env["product.attribute.value"].create( + {"name": "RAM-8GB", "attribute_id": cls.attr_ram.id} + ) + + cls.attr_processor = cls.env["product.attribute"].create( + { + "name": "Processor", + "value_ids": [ + (0, 0, {"name": "i3"}), + (0, 0, {"name": "i5"}), + ], + } + ) + + cls.attr_i7_value = cls.env["product.attribute.value"].create( + { + "name": "i7", + "attribute_id": cls.attr_processor.id, + "product_id": cls.test_i7_pro.product_variant_id.id, + } + ) + cls.test_product_tmpl = cls.env["product.template"].create( + { + "name": "Test Computer System", + "config_ok": True, + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.attr_ssd.id, + "value_ids": [ + (6, 0, cls.attr_ssd.value_ids.ids), + ], + "required": True, + "is_qty_required": True, + }, + ), + ( + 0, + 0, + { + "attribute_id": cls.attr_ram.id, + "value_ids": [ + (6, 0, cls.attr_ram.value_ids.ids), + ], + "required": True, + }, + ), + ( + 0, + 0, + { + "attribute_id": cls.attr_processor.id, + "value_ids": [ + (6, 0, cls.attr_processor.value_ids.ids), + ], + "required": True, + "is_qty_required": True, + }, + ), + ], + } + ) + + cls.component_ram = cls.env["product.template"].create( + { + "name": "Component-RAM", + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) + + cls.component_ssd = cls.env["product.template"].create( + { + "name": "Component-SSD", + "type": "consu", + "categ_id": cls.env.ref("product.product_category_all").id, + } + ) + cls.ssd_config_set = cls.env["mrp.bom.line.configuration.set"].create( + { + "name": "SSD-Config Set", + "configuration_ids": [ + (0, 0, {"value_ids": [(6, 0, cls.value_ssd256.ids)]}) + ], + } + ) + + def test_01_check_default_qty_maximum_qty(self): + attr_ssd_val = self.env["product.template.attribute.value"].search( + [("attribute_id", "=", self.attr_ssd.id)] + ) + with self.assertRaises(ValidationError): + attr_ssd_val.write({"default_qty": 2, "maximum_qty": 1}) + attr_ssd_val.write( + { + "default_qty": 1, + "maximum_qty": 5, + } + ) + + def test_02_create_product_config_wizard(self): + ProductConfWizard = self.env["product.configurator"] + product_config_wizard = ProductConfWizard.create( + { + "product_tmpl_id": self.test_product_tmpl.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_ssd.id}": self.value_ssd256.id, + f"__qty_{self.attr_ssd.id}": 2, + f"__attribute_{self.attr_ram.id}": self.value_ram8gb.id, + f"__attribute_{self.attr_processor.id}": self.attr_i7_value.id, + f"__qty_{self.attr_processor.id}": 2, + } + ) + product_config_wizard.action_next_step() + + # For Duplicate Product Variant Createion + product_config_wizard2 = ProductConfWizard.create( + { + "product_tmpl_id": self.test_product_tmpl.id, + } + ) + product_config_wizard2.action_next_step() + product_config_wizard2.write( + { + f"__attribute_{self.attr_ssd.id}": self.value_ssd256.id, + f"__qty_{self.attr_ssd.id}": 2, + f"__attribute_{self.attr_ram.id}": self.value_ram8gb.id, + f"__attribute_{self.attr_processor.id}": self.attr_i7_value.id, + f"__qty_{self.attr_processor.id}": 2, + } + ) + product_config_wizard2.action_next_step() + + def test_03_create_mrp_order(self): + MRPBom = self.env["mrp.bom"] + self.value_ssd256.write( + {"product_id": self.component_ssd.product_variant_id.id} + ) + computer_system_bom = MRPBom.create( + { + "product_tmpl_id": self.test_product_tmpl.id, + "config_ok": True, + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component_ram.product_variant_id.id, + "product_tmpl_id": self.component_ram.id, + "product_qty": 2, + "product_uom_id": self.env.ref("uom.product_uom_unit").id, + "config_set_id": self.ssd_config_set.id, + }, + ), + ( + 0, + 0, + { + "product_id": self.component_ssd.product_variant_id.id, + "product_tmpl_id": self.component_ssd.id, + "product_qty": 1, + "product_uom_id": self.env.ref("uom.product_uom_unit").id, + }, + ), + ], + } + ) + ProductConfWizard = self.env["product.configurator"] + product_config_wizard = ProductConfWizard.create( + { + "product_tmpl_id": self.test_product_tmpl.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_ssd.id}": self.value_ssd256.id, + f"__qty_{self.attr_ssd.id}": 2, + f"__attribute_{self.attr_ram.id}": self.value_ram8gb.id, + f"__attribute_{self.attr_processor.id}": self.attr_i7_value.id, + f"__qty_{self.attr_processor.id}": 2, + } + ) + product_config_wizard.action_next_step() + + self.test_product_tmpl.product_variant_id.mapped( + "product_attribute_value_qty_ids" + )._compute_display_name() diff --git a/product_configurator_mrp_quantity/views/product_attribute_view.xml b/product_configurator_mrp_quantity/views/product_attribute_view.xml new file mode 100644 index 0000000000..295ae71fe4 --- /dev/null +++ b/product_configurator_mrp_quantity/views/product_attribute_view.xml @@ -0,0 +1,63 @@ + + + + + + product.template.attribute.value.view.form.weight.extra + product.template.attribute.value + + form + + + + + + + + + + + product.template.attribute.value.view.tree.weight.extra + product.template.attribute.value + + tree + + + + + + + + + + diff --git a/product_configurator_mrp_quantity/views/product_config_view.xml b/product_configurator_mrp_quantity/views/product_config_view.xml new file mode 100644 index 0000000000..9b160e42a8 --- /dev/null +++ b/product_configurator_mrp_quantity/views/product_config_view.xml @@ -0,0 +1,27 @@ + + + + product.config.session.form + product.config.session + + + + + + + + + + + + + + + + diff --git a/product_configurator_mrp_quantity/views/product_view.xml b/product_configurator_mrp_quantity/views/product_view.xml new file mode 100644 index 0000000000..6d0eb920ae --- /dev/null +++ b/product_configurator_mrp_quantity/views/product_view.xml @@ -0,0 +1,35 @@ + + + + + product.configurator.product.template.form + product.template + + + + + + + + + + product.product.form + product.product + + + + + + + + + diff --git a/product_configurator_mrp_quantity/wizard/__init__.py b/product_configurator_mrp_quantity/wizard/__init__.py new file mode 100644 index 0000000000..3c76586e5c --- /dev/null +++ b/product_configurator_mrp_quantity/wizard/__init__.py @@ -0,0 +1 @@ +from . import product_configurator diff --git a/product_configurator_mrp_quantity/wizard/product_configurator.py b/product_configurator_mrp_quantity/wizard/product_configurator.py new file mode 100644 index 0000000000..d4a047f075 --- /dev/null +++ b/product_configurator_mrp_quantity/wizard/product_configurator.py @@ -0,0 +1,412 @@ +from lxml import etree + +from odoo import _, api, models +from odoo.exceptions import UserError + + +class ProductConfigurator(models.TransientModel): + _inherit = "product.configurator" + + @property + def _prefixes(self): + """Oerride this method to add all extra prefixes""" + return { + "field_prefix": "__attribute_", + "custom_field_prefix": "__custom_", + "qty_field": "__qty_", + } + + @api.model + def fields_get(self, allfields=None, write_access=True, attributes=None): + qty_field_prefix = self._prefixes.get("qty_field") + res = super().fields_get(allfields=allfields, attributes=attributes) + + wizard_id = self._find_wizard_context() + # If wizard_id is not defined in the context then the wizard was just + # launched and is not stored in the database yet + if not wizard_id: + return res + + # Get the wizard object from the database + wiz = self.browse(wizard_id) + active_step_id = wiz.state + + # If the product template is not set it is still at the 1st step + if not wiz.product_tmpl_id: + return res + # Default field attributes + default_attrs = self.get_field_default_attrs() + + attribute_lines = wiz.product_tmpl_id.attribute_line_ids + for line in attribute_lines: + attribute = line.attribute_id + value_ids = line.value_ids.ids + if line.is_qty_required: + selection_vals = [(False, "")] + attribute_value_obj = self.env["product.template.attribute.value"] + atrr_values = attribute_value_obj.search( + [("attribute_line_id", "=", line.id)] + ) + default_qty = min(atrr_values.mapped("default_qty")) + maximum_qty = max(atrr_values.mapped("maximum_qty")) + for i in range(default_qty, maximum_qty + 1): + selection_vals.append((str(i), i)) + res[qty_field_prefix + str(attribute.id)] = dict( + default_attrs, + type="selection", + string="Qty", + selection=selection_vals, + ) + return res + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if "product_id" in vals: + product = self.env["product.product"].browse(vals["product_id"]) + product_attr_qty = product.product_attribute_value_qty_ids + attr_qty_list = [] + session = self.env["product.config.session"].create_get_session( + product_tmpl_id=int(vals.get("product_tmpl_id")) + ) + flag = False + for attr in product_attr_qty: + session_attr = session.session_value_quantity_ids.filtered( + lambda l: l.attr_value_id.id == attr.attr_value_id.id + and l.qty == int(attr.qty) + ) + if not session_attr: + attr_qty_list.append( + ( + 0, + 0, + { + "attr_value_id": attr.attr_value_id.id, + "qty": int(attr.qty), + }, + ) + ) + if attr_qty_list: + vals.update({"session_value_quantity_ids": attr_qty_list}) + return super().create(vals_list) + + # ============================ + # OVERRIDE Methods + # ============================ + + def prepare_attrs_initial( + self, + attr_lines, + field_prefix, + custom_field_prefix, + qty_field_prefix, + dynamic_fields, + wiz, + ): + cfg_step_ids = [] + for attr_line in attr_lines: + attribute_id = attr_line.attribute_id.id + field_name = field_prefix + str(attribute_id) + custom_field = custom_field_prefix + str(attribute_id) + qty_field = qty_field_prefix + str(attribute_id) + + # Check if the attribute line has been added to the db fields + if field_name not in dynamic_fields: + continue + + config_steps = wiz.product_tmpl_id.config_step_line_ids.filtered( + lambda x: attr_line in x.attribute_line_ids + ) + + # attrs property for dynamic fields + attrs = {"readonly": "", "required": "", "invisible": ""} + invisible_str = "" + readonly_str = "" + required_str = "" + + if config_steps: + cfg_step_ids = [str(id) for id in config_steps.ids] + invisible_str = f"state not in {cfg_step_ids}" + readonly_str = f"state not in {cfg_step_ids}" + # If attribute is required make it so only in the proper step + if attr_line.required: + required_str = f"state in {cfg_step_ids}" + else: + invisible_str = "state not in {}".format(["configure"]) + readonly_str = "state not in {}".format(["configure"]) + # If attribute is required make it so only in the proper step + if attr_line.required: + required_str = "state in {}".format(["configure"]) + + if attr_line.custom: + pass + # TODO: Implement restrictions for ranges + + config_lines = wiz.product_tmpl_id.config_line_ids + dependencies = config_lines.filtered( + lambda cl: cl.attribute_line_id == attr_line + ) + + # If an attribute field depends on another field from the same + # configuration step then we must use attrs to enable/disable the + # required and readonly depending on the value entered in the + # dependee + + if attr_line.value_ids <= dependencies.mapped("value_ids"): + attr_depends = {} + domain_lines = dependencies.mapped("domain_id.domain_line_ids") + for domain_line in domain_lines: + attr_id = domain_line.attribute_id.id + attr_field = field_prefix + str(attr_id) + attr_lines = wiz.product_tmpl_id.attribute_line_ids + # If the fields it depends on are not in the config step + # allow to update attrs for all attribute.\ otherwise + # required will not work with stepchange using statusbar. + # if config_steps and wiz.state not in cfg_step_ids: + # continue + if attr_field not in attr_depends: + attr_depends[attr_field] = set() + if domain_line.condition == "in": + attr_depends[attr_field] |= set(domain_line.value_ids.ids) + elif domain_line.condition == "not in": + val_ids = attr_lines.filtered( + lambda line: line.attribute_id.id == attr_id + ).value_ids + val_ids = val_ids - domain_line.value_ids + attr_depends[attr_field] |= set(val_ids.ids) + + for dependee_field, val_ids in attr_depends.items(): + if not val_ids: + continue + + # if not attr_line.custom: + # readonly_str = f"{dependee_field} not in {list(val_ids)}" + if attr_line.required and not attr_line.custom: + required_str += f" and {dependee_field} in {list(val_ids)}" + + attrs.update( + { + "readonly": readonly_str, + "required": required_str, + "invisible": invisible_str, + } + ) + return attrs, field_name, custom_field, qty_field, config_steps, cfg_step_ids + + @api.model + def add_dynamic_fields(self, res, dynamic_fields, wiz): + """Create the configuration view using the dynamically generated + fields in fields_get() + """ + + field_prefix = self._prefixes.get("field_prefix") + custom_field_prefix = self._prefixes.get("custom_field_prefix") + qty_field_prefix = self._prefixes.get("qty_field") + + try: + # Search for view container hook and add dynamic view and fields + xml_view = etree.fromstring(res["arch"]) + xml_static_form = xml_view.xpath("//group[@name='static_form']")[0] + xml_dynamic_form = etree.Element("group", colspan="2", name="dynamic_form") + xml_parent = xml_static_form.getparent() + xml_parent.insert(xml_parent.index(xml_static_form) + 1, xml_dynamic_form) + xml_dynamic_form = xml_view.xpath("//group[@name='dynamic_form']")[0] + except Exception as exc: + raise UserError( + _("There was a problem rendering the view " "(dynamic_form not found)") + ) from exc + + # Get all dynamic fields inserted via fields_get method + attr_lines = wiz.product_tmpl_id.attribute_line_ids.sorted() + + # Loop over the dynamic fields and add them to the view one by one + for attr_line in attr_lines: # TODO: NC: Added a filter for multi + ( + attrs, + field_name, + custom_field, + qty_field, + config_steps, + cfg_step_ids, + ) = self.prepare_attrs_initial( + attr_line, + field_prefix, + custom_field_prefix, + qty_field_prefix, + dynamic_fields, + wiz, + ) + + # Create the new field in the view + node = etree.Element( + "field", + name=field_name, + on_change="1", + default_focus="1" if attr_line == attr_lines[0] else "0", + attrib=attrs, + context=str( + { + "show_attribute": False, + "show_price_extra": True, + "active_id": wiz.product_tmpl_id.id, + "wizard_id": wiz.id, + "field_name": field_name, + "is_m2m": attr_line.multi, + "value_ids": attr_line.value_ids.ids, + } + ), + options=str( + { + "no_create": True, + "no_create_edit": True, + "no_open": True, + } + ), + ) + + field_type = dynamic_fields[field_name].get("type") + if field_type == "many2many": + node.attrib["widget"] = "many2many_tags" + # Apply the modifiers (attrs) on the newly inserted field in the + # arch and add it to the view + # self.setup_modifiers(node) # TODO: NC: Need to improve this method + xml_dynamic_form.append(node) + + if attr_line.custom and custom_field in dynamic_fields: + widget = "" + config_session_obj = self.env["product.config.session"] + custom_option_id = config_session_obj.get_custom_value_id().id + + if field_type == "many2many": + field_val = [(6, False, [custom_option_id])] + else: + field_val = custom_option_id + + attrs.update( + { + "readonly": attrs.get("readonly") + + f" and {field_name} != {field_val}" + } + ) + attrs.update( + { + "invisible": attrs.get("invisible") + + f" and {field_name} != {field_val}" + } + ) + attrs.update( + { + "required": attrs.get("required") + + f" and {field_name} != {field_val}" + } + ) + + if config_steps: + attrs.update( + { + "required": attrs.get("required") + + f" and 'state' in {cfg_step_ids}" + } + ) + + # TODO: Add a field2widget mapper + if attr_line.attribute_id.custom_type == "color": + widget = "color" + + node = etree.Element( + "field", name=custom_field, attrib=attrs, widget=widget + ) + # self.setup_modifiers(node) # TODO: NC: Need to improve this method + xml_dynamic_form.append(node) + if attr_line.is_qty_required and qty_field in dynamic_fields: + node = etree.Element( + "field", name=qty_field, on_change="1", attrib=attrs + ) + # self.setup_modifiers(node) # TODO: NC: Need to improve this method + xml_dynamic_form.append(node) + return xml_view + + def read(self, fields=None, load="_classic_read"): + """Remove dynamic fields from the fields list and update the + returned values with the dynamic data stored in value_ids""" + field_prefix = self._prefixes.get("field_prefix") + custom_field_prefix = self._prefixes.get("custom_field_prefix") + qty_field_prefix = self._prefixes.get("qty_field") + + attr_vals = [f for f in fields if f.startswith(field_prefix)] + custom_attr_vals = [f for f in fields if f.startswith(custom_field_prefix)] + qty_attr_vals = [f for f in fields if f.startswith(qty_field_prefix)] + + dynamic_fields = attr_vals + custom_attr_vals + qty_attr_vals + fields = self._remove_dynamic_fields(fields) + + custom_val = self.env["product.config.session"].get_custom_value_id() + dynamic_vals = {} + + res = super().read(fields=fields, load=load) + + if not load: + load = "_classic_read" + + if not dynamic_fields: + return res + + for attr_line in self.product_tmpl_id.attribute_line_ids: + attr_id = attr_line.attribute_id.id + field_name = field_prefix + str(attr_id) + if field_name not in dynamic_fields: + continue + + custom_field_name = custom_field_prefix + str(attr_id) + qty_field_name = qty_field_prefix + str(attr_id) + + # Handle default values for dynamic fields on Odoo frontend + res[0].update( + {field_name: False, custom_field_name: False, qty_field_name: False} + ) + + custom_vals = self.custom_value_ids.filtered( + lambda x: x.attribute_id.id == attr_id + ).with_context(show_attribute=False) + vals = attr_line.value_ids.filtered( + lambda v: v in self.value_ids + ).with_context( + show_attribute=False, + show_price_extra=True, + active_id=self.product_tmpl_id.id, + ) + qty_field_values = self.session_value_quantity_ids.filtered( + lambda l: l.attr_value_id.attribute_id.id == attr_id + ) + if not attr_line.custom and not vals: + continue + + if attr_line.custom and custom_vals: + custom_field_val = custom_val.id + if load == "_classic_read": + # custom_field_val = custom_val.name_get()[0] + custom_field_val = (custom_val.id, custom_val.display_name or "") + dynamic_vals.update( + { + field_name: custom_field_val, + custom_field_name: custom_vals.eval(), + } + ) + elif attr_line.multi: + dynamic_vals = {field_name: vals.ids} + else: + try: + vals.ensure_one() + field_value = vals.id + if load == "_classic_read": + # field_value = vals.name_get()[0] + field_value = (vals.id, vals.display_name or "") + dynamic_vals = {field_name: field_value} + except Exception: + continue + + if qty_field_values: + for attr_qty in qty_field_values: + dynamic_vals.update({qty_field_name: str(attr_qty.qty)}) + res[0].update(dynamic_vals) + return res diff --git a/product_configurator_mrp_quantity/wizard/product_configurator_view.xml b/product_configurator_mrp_quantity/wizard/product_configurator_view.xml new file mode 100644 index 0000000000..a85f89a614 --- /dev/null +++ b/product_configurator_mrp_quantity/wizard/product_configurator_view.xml @@ -0,0 +1,19 @@ + + + + + + product.configurator.product.template.form + product.configurator + + + + + + + + +