diff --git a/product_configurator/README.md b/product_configurator/README.md index affc7d903..41b51cf01 100644 --- a/product_configurator/README.md +++ b/product_configurator/README.md @@ -1,4 +1,4 @@ -#Odoo Product Configurator +# Odoo Product Configurator This module is Dynamic configuration wizard for Odoo back-end and the foundation for external configuration interfaces such 'website_product_configurator'. diff --git a/product_configurator/README.rst b/product_configurator/README.rst index 284d996f1..85eb1450e 100644 --- a/product_configurator/README.rst +++ b/product_configurator/README.rst @@ -17,13 +17,13 @@ Product Configurator :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 + :target: https://github.com/OCA/product-configurator/tree/18.0/product_configurator :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 + :target: https://translation.odoo-community.org/projects/product-configurator-18-0/product-configurator-18-0-product_configurator :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-configurator&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -42,7 +42,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. @@ -82,6 +82,6 @@ Current `maintainer `__: |maintainer-PCatinean| -This module is part of the `OCA/product-configurator `_ project on GitHub. +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/__manifest__.py b/product_configurator/__manifest__.py index 6daf59370..52a389dd9 100644 --- a/product_configurator/__manifest__.py +++ b/product_configurator/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Product Configurator", - "version": "17.0.1.0.1", + "version": "18.0.1.0.0", "category": "Generic Modules/Base", "summary": "Base for product configuration interface modules", "author": "Pledra, Odoo Community Association (OCA)", diff --git a/product_configurator/data/menu_configurable_product.xml b/product_configurator/data/menu_configurable_product.xml index 3823aa8ca..eb36ee49d 100644 --- a/product_configurator/data/menu_configurable_product.xml +++ b/product_configurator/data/menu_configurable_product.xml @@ -24,7 +24,7 @@ Configurable Templates ir.actions.act_window product.template - kanban,tree,form + kanban,list,form Configured Variants ir.actions.act_window product.product - kanban,form,tree + kanban,form,list @@ -88,7 +88,7 @@ Configuration Steps ir.actions.act_window product.config.step - tree,form + list,form Configuration Restrictions ir.actions.act_window product.config.domain - tree,form + list,form Configuration Sessions ir.actions.act_window product.config.session - tree,form + list,form maxv): raise ValidationError( - _( + self.env._( "Selected custom value '%(name)s' must be " "between %(min_val)s and %(max_val)s", **{ @@ -122,14 +122,14 @@ def validate_custom_val(self, val): ) elif minv and val < minv: raise ValidationError( - _( + self.env._( "Selected custom value '%(name)s' must be at least %(min_val)s", **{"name": self.name, "min_val": self.min_val}, ) ) elif maxv and val > maxv: raise ValidationError( - _( + self.env._( "Selected custom value '%(name)s' " "must be lower than %(max_value)s", **{"name": self.name, "max_value": self.max_val + 1}, @@ -146,7 +146,7 @@ def _check_constraint_min_max_value(self): maxv = attribute.max_val if maxv and minv and maxv < minv: raise ValidationError( - _("Maximum value must be greater than Minimum value") + self.env._("Maximum value must be greater than Minimum value") ) def _configurator_value_ids(self): @@ -194,7 +194,7 @@ def _check_default_values(self): for line in self.filtered(lambda line: line.default_val): if line.default_val not in line.value_ids: raise ValidationError( - _( + self.env._( "Default values for each attribute line must exist in " "the attribute values (%(attr_name)s: %(default_val)s)", **{ @@ -215,7 +215,7 @@ def _check_valid_values(self): # if ptal.active and not ptal.value_ids: # Customization End raise ValidationError( - _( + self.env._( "The attribute %(attr)s must have at least one value for " "the product %(product)s.", **{ @@ -227,7 +227,7 @@ def _check_valid_values(self): for pav in ptal.value_ids: if pav.attribute_id != ptal.attribute_id: raise ValidationError( - _( + self.env._( "On the product %(product)s you cannot associate the " "value %(value)s with the attribute %(attr)s because they " "do not match.", @@ -438,7 +438,7 @@ def _validate_configuration(self): ) if not valid: raise ValidationError( - _( + self.env._( "Values provided to the attribute value line are " "incompatible with the current rules" ) diff --git a/product_configurator/models/product_config.py b/product_configurator/models/product_config.py index 0c79fbcd5..a278ae842 100644 --- a/product_configurator/models/product_config.py +++ b/product_configurator/models/product_config.py @@ -1,10 +1,12 @@ import logging from ast import literal_eval +from collections.abc import Iterable +from itertools import chain -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.fields import Command -from odoo.tools.misc import flatten, formatLang +from odoo.tools.misc import formatLang _logger = logging.getLogger(__name__) @@ -230,7 +232,7 @@ def check_value_attributes(self): forbidden_values = line.value_ids - line.attr_line_val_ids if forbidden_values: raise ValidationError( - _( + self.env._( "Values must belong to the attribute of the " "corresponding attribute_line set on the " "configuration line" @@ -280,7 +282,7 @@ def _check_value_ids(self): ) except ValidationError as exc: raise ValidationError( - _( + self.env._( "Values entered for line '%s' generate " "a incompatible configuration", cfg_img.name, @@ -334,7 +336,7 @@ def _check_config_step(self): ).mapped("config_step_id") if config_step.config_step_id in cfg_steps: raise ValidationError( - _("Cannot have a configuration step defined twice.") + self.env._("Cannot have a configuration step defined twice.") ) @@ -402,6 +404,11 @@ def _compute_config_step_name(self): if not session.config_step_name: session.config_step_name = session.config_step + def flatten_attribute_value_ids(self, value_ids): + return chain.from_iterable( + v if isinstance(v, Iterable) else [v] for v in value_ids + ) + @api.model def get_cfg_weight(self, value_ids=None, custom_vals=None): """Computes the weight of the configured product based on the @@ -421,7 +428,7 @@ def get_cfg_weight(self, value_ids=None, custom_vals=None): self = self.with_context(active_id=product_tmpl.id) - value_ids = flatten(value_ids) + value_ids = list(self.flatten_attribute_value_ids(value_ids)) weight_extra = 0.0 product_attr_val_obj = self.env["product.template.attribute.value"] @@ -504,8 +511,6 @@ def _compute_currency_id(self): product_preset_id = fields.Many2one( comodel_name="product.product", string="Preset", - domain="[('product_tmpl_id', '=', product_tmpl_id),\ - ('config_preset_ok', '=', True)]", ) def action_confirm(self, product_id=None): @@ -520,7 +525,10 @@ def _check_product_id(self): for session in self.filtered(lambda s: s.state == "done"): if not session.product_id: raise ValidationError( - _("Finished configuration session must have a " "product_id linked") + self.env._( + "Finished configuration session must have a " + "product_id linked" + ) ) def update_session_configuration_value(self, vals, product_tmpl_id=None): @@ -562,7 +570,7 @@ def update_session_configuration_value(self, vals, product_tmpl_id=None): field_val = vals[field_name] else: raise UserError( - _( + self.env._( "An error occurred while parsing value for attribute %s", attr_line.attribute_id.name, ) @@ -714,9 +722,9 @@ def write(self, vals): try: self.validate_configuration(final=False) except ValidationError as exc: - raise ValidationError(_(f"{exc}")) from exc + raise ValidationError(self.env._(f"{exc}")) from exc except Exception as exc: - raise ValidationError(_("Invalid Configuration")) from exc + raise ValidationError(self.env._("Invalid Configuration")) from exc return res @api.model_create_multi @@ -724,7 +732,7 @@ def create(self, vals_list): for vals in vals_list: vals["name"] = self.env["ir.sequence"].next_by_code( "product.config.session" - ) or _("New") + ) or self.env._("New") product_tmpl = ( self.env["product.template"] .browse(vals.get("product_tmpl_id")) @@ -750,10 +758,10 @@ def create(self, vals_list): # TODO: Remove if cond when PR with # raise error on github is merged except ValidationError as exc: - raise ValidationError(_("%s") % exc.name) from exc + raise ValidationError(self.env._("%s") % exc.name) from exc except Exception as exc: raise ValidationError( - _( + self.env._( "Default values provided generate an invalid " "configuration" ) @@ -783,9 +791,9 @@ def create_get_variant(self, value_ids=None, custom_vals=None): try: self.validate_configuration() except ValidationError as exc: - raise ValidationError(_("%s") % exc.name) from exc + raise ValidationError(self.env._("%s") % exc.name) from exc except Exception as exc: - raise ValidationError(_("Invalid Configuration")) from exc + raise ValidationError(self.env._("Invalid Configuration")) from exc duplicates = self.search_variant( value_ids=value_ids, product_tmpl_id=self.product_tmpl_id @@ -800,7 +808,7 @@ def create_get_variant(self, value_ids=None, custom_vals=None): variant = product_obj.sudo().create(vals) variant.message_post( - body=_("Product created via configuration wizard"), + body=self.env._("Product created via configuration wizard"), author_id=self.env.user.partner_id.id, ) @@ -999,7 +1007,7 @@ def get_next_step( return False elif not (value_ids or custom_value_ids) and state != "select": raise UserError( - _( + self.env._( "You must select at least one " "attribute in order to configure a product" ) @@ -1009,9 +1017,7 @@ def get_next_step( adjacent_steps = self.get_adjacent_steps() next_step = adjacent_steps.get("next_step") - open_step_lines = list( - map(lambda x: "%s" % (x), self.get_open_step_lines().ids) - ) + open_step_lines = list(map(str, self.get_open_step_lines().ids)) session_config_step = self.config_step if ( @@ -1026,7 +1032,7 @@ def get_next_step( pass elif not (value_ids or custom_value_ids): raise UserError( - _( + self.env._( "You must select at least one " "attribute in order to configure a product" ) @@ -1173,7 +1179,7 @@ def check_and_open_incomplete_step(self, value_ids=None, custom_value_ids=None): step_to_open = step break if step_to_open: - return "%s" % (step_to_open.id) + return str(step_to_open.id) return False @api.model @@ -1336,7 +1342,7 @@ def check_attributes_configuration( ): # TODO: Verify custom value type to be correct raise ValidationError( - _("Required attribute '%s' is empty", attr.name) + self.env._("Required attribute '%s' is empty", attr.name) ) @api.model @@ -1397,7 +1403,7 @@ def validate_configuration( else: group_by_attr[val.attribute_id] = val - message = _("The following values are not available:") + message = self.env._("The following values are not available:") for attr, val in group_by_attr.items(): message += "\n {}: {}".format(attr.name, ", ".join(val.mapped("name"))) raise ValidationError(message) @@ -1415,7 +1421,7 @@ def validate_configuration( custom_attrs_with_error = self.env["product.attribute"].browse( custom_attrs_with_error ) - error_message = _( + error_message = self.env._( "The following custom values are not permitted " "according to the product template - %s.\n\nIt is possible " "that a change has been made to allowed custom values " @@ -1440,7 +1446,7 @@ def validate_configuration( ) attrs_with_error[line.attribute_id] = wrong_vals if attrs_with_error: - error_message = _( + error_message = self.env._( "The following multi values are not permitted " "according to the product template - %s.\n\nIt is possible " "that a change has been made to allowed multi values " @@ -1476,7 +1482,7 @@ def search_variant(self, value_ids=None, product_tmpl_id=None): product_tmpl_id = self.product_tmpl_id if not product_tmpl_id: raise ValidationError( - _( + self.env._( "Cannot conduct search on an empty config session " "without product_tmpl_id kwarg" ) @@ -1521,7 +1527,6 @@ def create_get_session( return self.create(vals) # TODO: Disallow duplicates - def flatten_val_ids(self, value_ids): """Return a list of value_ids from a list with a mix of ids and list of ids (multiselection) @@ -1529,7 +1534,10 @@ def flatten_val_ids(self, value_ids): :param value_ids: list of value ids or mix of ids and list of ids (e.g: [1, 2, 3, [4, 5, 6]]) :returns: flattened list of ids ([1, 2, 3, 4, 5, 6])""" - flat_val_ids = set(flatten(value_ids)) + if not isinstance(value_ids, list | tuple): + return [value_ids] # Ensure single values are wrapped in a list + + flat_val_ids = set(self.flatten_attribute_value_ids(value_ids)) return list(flat_val_ids) def formatPrices(self, prices=None, dp="Product Price"): @@ -1632,7 +1640,7 @@ def _compute_val_name(self): uom = attr_val_custom.attribute_id.uom_id.name attr_val_custom.name = "{}{}".format( attr_val_custom.value, - (" %s" % uom) or "", + f" {uom}" if uom else "", ) name = fields.Char(readonly=True, compute="_compute_val_name", store=True) @@ -1682,7 +1690,9 @@ def unique_attribute(self): > 1 ): raise ValidationError( - _("Configuration cannot have the " "same value inserted twice") + self.env._( + "Configuration cannot have the " "same value inserted twice" + ) ) # @api.constrains('cfg_session_id.value_ids') @@ -1702,14 +1712,14 @@ def check_custom_type(self): custom_type = custom_val.attribute_id.custom_type if custom_val.value and custom_type == "binary": raise ValidationError( - _( + self.env._( "Attribute custom type is binary, attachments are the " "only accepted values with this custom field type" ) ) if custom_val.attachment_ids and custom_type != "binary": raise ValidationError( - _( + self.env._( "Attribute custom type must be 'binary' for saving " "attachments to custom value" ) diff --git a/product_configurator/static/description/configurable-template.png b/product_configurator/static/description/configurable-template.png index 860c818ea..bdbd3408a 100644 Binary files a/product_configurator/static/description/configurable-template.png and b/product_configurator/static/description/configurable-template.png differ diff --git a/product_configurator/static/description/quotation-updated.png b/product_configurator/static/description/quotation-updated.png index 8b0f5aafb..4b40af20d 100644 Binary files a/product_configurator/static/description/quotation-updated.png and b/product_configurator/static/description/quotation-updated.png differ diff --git a/product_configurator/static/description/quotation.png b/product_configurator/static/description/quotation.png index 8ed801383..987e1ba87 100644 Binary files a/product_configurator/static/description/quotation.png and b/product_configurator/static/description/quotation.png differ diff --git a/product_configurator/static/description/wizard-color.png b/product_configurator/static/description/wizard-color.png index 396e046b7..add31d25d 100644 Binary files a/product_configurator/static/description/wizard-color.png and b/product_configurator/static/description/wizard-color.png differ diff --git a/product_configurator/static/description/wizard-last-step.png b/product_configurator/static/description/wizard-last-step.png index 3d89020dc..6f2fd2f85 100644 Binary files a/product_configurator/static/description/wizard-last-step.png and b/product_configurator/static/description/wizard-last-step.png differ diff --git a/product_configurator/static/description/wizard-template.png b/product_configurator/static/description/wizard-template.png index 948ce549b..708c3ede7 100644 Binary files a/product_configurator/static/description/wizard-template.png and b/product_configurator/static/description/wizard-template.png differ diff --git a/product_configurator/static/src/js/boolean_button_widget.esm.js b/product_configurator/static/src/js/boolean_button_widget.esm.js index 952c22542..fb655a425 100644 --- a/product_configurator/static/src/js/boolean_button_widget.esm.js +++ b/product_configurator/static/src/js/boolean_button_widget.esm.js @@ -1,8 +1,8 @@ -/** @odoo-module **/ import {BooleanField, booleanField} from "@web/views/fields/boolean/boolean_field"; import {onMounted, onRendered, useRef} from "@odoo/owl"; import {registry} from "@web/core/registry"; import {standardFieldProps} from "@web/views/fields/standard_field_props"; +const {document} = globalThis; export class BooleanButton extends BooleanField { static template = "product_configurator.BooleanButtonField"; @@ -21,25 +21,29 @@ export class BooleanButton extends BooleanField { } updateConfigurableButton() { + if (!this.root.el) { + return; + } this.text = this.state.value ? this.props.activeString : this.props.inactiveString; this.hover = this.state.value ? this.props.inactiveString : this.props.activeString; - var val_color = this.state.value ? "text-success" : "text-danger"; var hover_color = this.state.value ? "text-danger" : "text-success"; - var $val = $("") - .addClass("o_stat_text o_boolean_button o_not_hover " + val_color) - .text(this.text); - var $hover = $("") - .addClass("o_stat_text o_boolean_button o_hover d-none " + hover_color) - .text(this.hover); + const valSpan = document.createElement("span"); + valSpan.className = `o_stat_text o_boolean_button o_not_hover ${val_color}`; + valSpan.textContent = this.text; + + const hoverSpan = document.createElement("span"); + hoverSpan.className = `o_stat_text o_boolean_button o_hover d-none ${hover_color}`; + hoverSpan.textContent = this.hover; - $(this.root.el).empty(); - $(this.root.el).append($val).append($hover); + this.root.el.innerHTML = ""; + this.root.el.appendChild(valSpan); + this.root.el.appendChild(hoverSpan); } } diff --git a/product_configurator/static/src/scss/form_widget.scss b/product_configurator/static/src/scss/form_widget.scss index 7a9323bc1..d3cbbf54d 100644 --- a/product_configurator/static/src/scss/form_widget.scss +++ b/product_configurator/static/src/scss/form_widget.scss @@ -14,7 +14,3 @@ min-height: 30px; } } - -.pull-right { - float: right; -} diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py index 58bf1c79c..a6aee9499 100644 --- a/product_configurator/tests/test_configuration_rules.py +++ b/product_configurator/tests/test_configuration_rules.py @@ -4,7 +4,7 @@ from odoo import SUPERUSER_ID, Command from odoo.exceptions import ValidationError from odoo.fields import first -from odoo.tests.common import Form, TransactionCase +from odoo.tests import Form, TransactionCase from odoo.tools.safe_eval import safe_eval diff --git a/product_configurator/tests/test_product_attribute.py b/product_configurator/tests/test_product_attribute.py index 453d6e59a..132ab83ac 100644 --- a/product_configurator/tests/test_product_attribute.py +++ b/product_configurator/tests/test_product_attribute.py @@ -34,6 +34,40 @@ def setUpClass(cls): ) cls.ProductAttributeValueFuel = cls.value_gasoline.attribute_id.id + cls.product_template = cls.env["product.template"].create( + { + "name": "Test Product", + } + ) + cls.attribute = cls.env["product.attribute"].create( + { + "name": "Color", + } + ) + + cls.value_red = cls.env["product.attribute.value"].create( + { + "name": "Red", + "attribute_id": cls.attribute.id, + } + ) + + cls.value_blue = cls.env["product.attribute.value"].create( + { + "name": "Blue", + "attribute_id": cls.attribute.id, + } + ) + + # Create attribute line and assign values + cls.product_template.attribute_line_ids.create( + { + "product_tmpl_id": cls.product_template.id, + "attribute_id": cls.attribute.id, + "value_ids": [(6, 0, [cls.value_red.id, cls.value_blue.id])], + } + ) + def test_01_onchange_custome_type(self): self.ProductAttributeFuel.min_val = 20 self.ProductAttributeFuel.max_val = 30 @@ -190,3 +224,45 @@ def test_12_onchange_values(self): "Error: If default_val not exists\ Method: onchange_values()", ) + + def test_13_name_search_without_product_template(self): + res = self.env["product.attribute.value"].with_context().name_search("Red") + self.assertTrue( + res, + "Expected name_search to return results without product template context", + ) + + def test_14_name_search_exclude_values(self): + self.assertTrue(self.value_blue, "Attribute value 'Blue' must be initialized.") + res = self.env["product.attribute.value"].name_search("Blue") + self.assertTrue( + any(r[0] == self.value_blue.id for r in res), + "Expected Blue to be in search results", + ) + + def test_15_name_search_with_invalid_template(self): + self.env.context = dict(self.env.context, _cfg_product_tmpl_id=999999) + result = self.env["product.attribute.value"].name_search( + name="Red", args=[], operator="ilike", limit=10 + ) + self.assertEqual( + result, [], "Expected no results when using an invalid product template ID" + ) + + def test_16_name_search_with_no_attributes(self): + empty_product_template = self.env["product.template"].create( + { + "name": "Empty Product", + } + ) + self.env.context = dict( + self.env.context, _cfg_product_tmpl_id=empty_product_template.id + ) + result = self.env["product.attribute.value"].name_search( + name="Red", args=[], operator="ilike", limit=10 + ) + self.assertEqual( + result, + [], + "Expected no results when the product template has no attributes", + ) diff --git a/product_configurator/views/product_attribute_view.xml b/product_configurator/views/product_attribute_view.xml index 99c7a1b54..eaf9128ed 100644 --- a/product_configurator/views/product_attribute_view.xml +++ b/product_configurator/views/product_attribute_view.xml @@ -1,7 +1,6 @@ - product.config.product.attribute.tree product.attribute @@ -83,7 +82,7 @@ Attributes ir.actions.act_window product.attribute - tree,form + list,form {'flag_config_ok': True} @@ -96,7 +95,6 @@ /> - - tree + list - product.attribute.value.list.inherit product.attribute.value @@ -190,7 +187,7 @@ Attribute Values ir.actions.act_window product.attribute.value - tree,form + list,form - product.configurator.config.step.form product.config.step @@ -23,15 +22,14 @@ product.config.step
- + - +
- product.configurator.domain.form product.config.domain @@ -45,7 +43,7 @@ - + - + @@ -89,7 +87,7 @@ - + - + @@ -125,7 +123,7 @@ product.config.session.tree product.config.session - + @@ -140,7 +138,7 @@ - + @@ -174,14 +172,14 @@ - + - + diff --git a/product_configurator/views/product_view.xml b/product_configurator/views/product_view.xml index ef35cf6be..fa41b7a93 100644 --- a/product_configurator/views/product_view.xml +++ b/product_configurator/views/product_view.xml @@ -63,7 +63,7 @@ not custom {'flag_config_ok': context.get('default_config_ok', False)} - - - - - + - + - + - +
- + - + - @@ -224,7 +217,7 @@ ]" widget="many2many_tags" /> - +
- +
@@ -349,9 +342,9 @@ - +
@@ -359,6 +352,7 @@ name="reconfigure_product" type="object" class="fa fa-repeat fa-lg" + title="Reconfigure Product" />
diff --git a/product_configurator/wizard/product_configurator.py b/product_configurator/wizard/product_configurator.py index 7e5152aba..d8f4f4bf1 100644 --- a/product_configurator/wizard/product_configurator.py +++ b/product_configurator/wizard/product_configurator.py @@ -1,8 +1,10 @@ import logging +from collections.abc import Iterable +from itertools import chain from lxml import etree -from odoo import _, api, fields, models, tools +from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.fields import Command from odoo.tools import frozendict @@ -53,19 +55,17 @@ def _remove_dynamic_fields(self, fields): prefixes = self._prefixes.values() - field_type = type(fields) - - if field_type == list: + if isinstance(fields, list): static_fields = [] - elif field_type == dict: + elif isinstance(fields, dict): static_fields = {} for field_name in fields: if any(prefix in field_name for prefix in prefixes): continue - if field_type == list: + if isinstance(fields, list): static_fields.append(field_name) - elif field_type == dict: + elif isinstance(fields, dict): static_fields[field_name] = fields[field_name] return static_fields @@ -127,7 +127,7 @@ def onchange_product_tmpl(self): if self.value_ids: # TODO: Add confirmation button an delete cfg session raise UserError( - _( + self.env._( "Changing the product template while having an active " "configuration will erase reset/clear all values" ) @@ -276,12 +276,21 @@ def get_form_vals( # To solve the Multi selection problem removing extra [] if "value_ids" in vals: val_ids = vals["value_ids"][0] - vals["value_ids"] = [[val_ids[0], val_ids[1], tools.flatten(val_ids[2])]] + value_data = val_ids[2] + if not isinstance(value_data, list | tuple): + flattened_values = [value_data] + else: + flattened_values = list( + chain.from_iterable( + i if isinstance(i, Iterable) else [i] for i in value_data + ) + ) + vals["value_ids"] = [Command.set(flattened_values)] return vals def apply_onchange_values(self, values, field_names, field_onchange): """Called from web-controller - - original onchage return M2o values in formate + - original onchange returns M2o values in format (attr-value.id, attr-value.name) but on website we need only attr-value.id""" product_tmpl_id = self.env["product.template"].browse( @@ -300,9 +309,10 @@ def apply_onchange_values(self, values, field_names, field_onchange): if not state: state = self.state cfg_vals = self.env["product.attribute.value"] - if values.get("value_ids", []): + value_ids = values.get("value_ids", []) + if value_ids and isinstance(value_ids, list) and value_ids[0]: cfg_vals = self.env["product.attribute.value"].browse( - values.get("value_ids", [])[0][2] + value_ids[0][2] if len(value_ids[0]) > 2 else [] ) if not cfg_vals: cfg_vals = self.value_ids @@ -339,9 +349,6 @@ def apply_onchange_values(self, values, field_names, field_onchange): # Get the unstored values from the client view for k, v in dynamic_fields.items(): attr_id = int(k.split(field_prefix)[1]) - # if isinstance(v, list): - # dynamic_fields[k] = v[0][2] - line_attributes = cfg_step.attribute_line_ids.mapped("attribute_id") if not cfg_step or attr_id in line_attributes.ids: view_attribute_ids.add(attr_id) @@ -455,8 +462,8 @@ def _onchange_product_preset(self): preset_id = self.env["product.product"].browse(preset_id) pta_value_ids = preset_id.product_template_attribute_value_ids attr_value_ids = pta_value_ids.mapped("product_attribute_value_id") - self._origin.value_ids = attr_value_ids - self._origin.price = ( + self.value_ids = attr_value_ids + self.price = ( preset_id and preset_id.lst_price or self.product_tmpl_id.list_price ) @@ -557,7 +564,7 @@ def fields_get(self, allfields=None, write_access=True, attributes=None): res[domain_field] = dict( default_attrs, type="binary", - string="Domain %s" % line.attribute_id.name, + string=f"Domain {line.attribute_id.name}", change_default=True, ) @@ -566,7 +573,7 @@ def fields_get(self, allfields=None, write_access=True, attributes=None): res[field_prefix + str(attribute.id)] = dict( default_attrs, type="many2many" if line.multi else "many2one", - domain="%s" % domain_field, + domain=f"{domain_field}", string=line.attribute_id.name, relation="product.attribute.value", change_default=True, @@ -748,7 +755,9 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz): 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)") + self.env._( + "There was a problem rendering the view " "(dynamic_form not found)" + ) ) from exc # Get all dynamic fields inserted via fields_get method @@ -878,8 +887,10 @@ def create(self, vals_list): ) vals.update({"user_id": self.env.uid, "config_session_id": session.id}) wz_value_ids = vals.get("value_ids", []) + # Check if wz_value_ids is not empty and has a valid structure. if session.value_ids and ( - (wz_value_ids and not wz_value_ids[0][2]) or not wz_value_ids + not wz_value_ids + or (len(wz_value_ids[0]) > 2 and not wz_value_ids[0][2]) ): vals.update({"value_ids": [(6, 0, session.value_ids.ids)]}) return super().create(vals_list) @@ -1004,7 +1015,7 @@ def action_next_step(self): if not self.product_tmpl_id.attribute_line_ids: raise ValidationError( - _("Product Template does not have any attribute lines defined") + self.env._("Product Template does not have any attribute lines defined") ) next_step = self.config_session_id.get_next_step( state=self.state, @@ -1106,7 +1117,7 @@ def open_step(self, step): if not step: return wizard_action if isinstance(step, type(self.env["product.config.step.line"])): - step = "%s" % (step.id) + step = str(step.id) self.state = step self.config_session_id.config_step = step return wizard_action diff --git a/product_configurator/wizard/product_configurator_view.xml b/product_configurator/wizard/product_configurator_view.xml index cae08cbf7..ec1156ffe 100644 --- a/product_configurator/wizard/product_configurator_view.xml +++ b/product_configurator/wizard/product_configurator_view.xml @@ -47,6 +47,7 @@ name="product_preset_id" invisible="not context.get('allow_preset_selection')" options="{'no_create': True}" + domain="[('product_tmpl_id', '=', product_tmpl_id),('config_preset_ok', '=', True)]" /> - + - +