diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83b4bb9cd7..7fdebf8f30 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,5 @@ exclude: | (?x) - # NOT INSTALLABLE ADDONS - ^product_configurator/| - # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| # We don't want to mess with tool-generated files diff --git a/product_configurator/README.rst b/product_configurator/README.rst index b6fb7d4e04..7b17a220cd 100644 --- a/product_configurator/README.rst +++ b/product_configurator/README.rst @@ -17,19 +17,19 @@ 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/16.0/product_configurator + :target: https://github.com/OCA/product-configurator/tree/17.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-16-0/product-configurator-16-0-product_configurator + :target: https://translation.odoo-community.org/projects/product-configurator-17-0/product-configurator-17-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=16.0 + :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 has all the mechanics to support product configuration. It serves as a base -dependency for configuration interfaces. +This module has all the mechanics to support product configuration. It +serves as a base dependency for configuration interfaces. **Table of contents** @@ -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. @@ -50,19 +50,19 @@ Credits ======= Authors -~~~~~~~ +------- * Pledra Contributors -~~~~~~~~~~~~ +------------ -* `Aion Tech `_: +- `Aion Tech `__: - * Simone Rubino + - Simone Rubino Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -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 55e4e0cfa7..5f18e8ee0a 100644 --- a/product_configurator/__manifest__.py +++ b/product_configurator/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Product Configurator", - "version": "16.0.1.1.1", + "version": "17.0.1.0.0", "category": "Generic Modules/Base", "summary": "Base for product configuration interface modules", "author": "Pledra, Odoo Community Association (OCA)", @@ -28,10 +28,11 @@ "assets": { "web.assets_backend": [ "/product_configurator/static/src/scss/form_widget.scss", - "/product_configurator/static/src/js/form_widgets.js", + "/product_configurator/static/src/js/form_widgets.esm.js", "/product_configurator/static/src/js/boolean_button_widget.esm.js", "/product_configurator/static/src/js/boolean_button_widget.xml", - "/product_configurator/static/src/js/relational_fields.js", + "/product_configurator/static/src/js/kanban_widgets.esm.js", + "/product_configurator/static/src/js/list_widgest.esm.js", ] }, "demo": [ @@ -44,10 +45,9 @@ ], "images": ["static/description/cover.png"], "post_init_hook": "post_init_hook", - "qweb": ["static/xml/create_button.xml"], "development_status": "Beta", "maintainers": ["PCatinean"], - "installable": False, + "installable": True, "application": True, "auto_install": False, } diff --git a/product_configurator/init_hook.py b/product_configurator/init_hook.py index 5e18812a13..59e41bdf18 100644 --- a/product_configurator/init_hook.py +++ b/product_configurator/init_hook.py @@ -3,8 +3,8 @@ logger = logging.getLogger(__name__) -def post_init_hook(cr, registry): +def post_init_hook(env): """Transfer existing weight values to weight_dummy after installation since now the weight field is computed """ - cr.execute("UPDATE product_product SET weight_dummy = weight") + env.cr.execute("UPDATE product_product SET weight_dummy = weight") diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py index 9e604eb79a..b3f821c879 100644 --- a/product_configurator/models/product.py +++ b/product_configurator/models/product.py @@ -177,7 +177,7 @@ def _check_default_value_domains(self): "generate an invalid configuration.\ \n%s" ) - % (exc.name) + % (exc.args[0]) ) from exc def toggle_config(self): diff --git a/product_configurator/models/product_attribute.py b/product_configurator/models/product_attribute.py index c1491849cc..0f408a7315 100644 --- a/product_configurator/models/product_attribute.py +++ b/product_configurator/models/product_attribute.py @@ -302,30 +302,21 @@ def get_attribute_value_extra_prices( extra_prices[attr_val_id.id] += line.price_extra return extra_prices - def name_get(self): - res = super().name_get() + def _compute_display_name(self): + # useless return to make pylint happy + res = super()._compute_display_name() if not self.env.context.get("show_price_extra"): return res product_template_id = self.env.context.get("active_id", False) - price_precision = self.env["decimal.precision"].precision_get("Product Price") - extra_prices = self.get_attribute_value_extra_prices( - product_tmpl_id=product_template_id, pt_attr_value_ids=self - ) - - res_prices = [] - for val in res: - price_extra = extra_prices.get(val[0]) + for attribute in self: + extra_prices = attribute.get_attribute_value_extra_prices( + product_tmpl_id=product_template_id, pt_attr_value_ids=attribute + ) + price_extra = extra_prices.get(attribute.id) if price_extra: - val = ( - val[0], - "{} ( +{} )".format( - val[1], - ("{0:,.%sf}" % (price_precision)).format(price_extra), - ), - ) - res_prices.append(val) - return res_prices + name = f"{attribute.name} ( +{price_extra:.{price_precision}f} )" + attribute.display_name = name @api.model def name_search(self, name="", args=None, operator="ilike", limit=100): diff --git a/product_configurator/models/product_config.py b/product_configurator/models/product_config.py index 02565a4884..0c79fbcd52 100644 --- a/product_configurator/models/product_config.py +++ b/product_configurator/models/product_config.py @@ -3,6 +3,7 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command from odoo.tools.misc import flatten, formatLang _logger = logging.getLogger(__name__) @@ -540,9 +541,9 @@ def update_session_configuration_value(self, vals, product_tmpl_id=None): ) custom_val = self.get_custom_value_id() - attr_val_dict = {} custom_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) @@ -553,13 +554,10 @@ def update_session_configuration_value(self, vals, product_tmpl_id=None): # 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! + # the write dictionary, then it must be a custom value! 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 = vals[field_name][0][2] + field_val = self._update_field_values(vals, field_name, attr_line) elif not attr_line.multi and isinstance(vals[field_name], int): field_val = vals[field_name] else: @@ -582,10 +580,36 @@ def update_session_configuration_value(self, vals, product_tmpl_id=None): 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}) + attr_val_dict.update({attr_id: custom_val.id}) self.update_config(attr_val_dict, custom_val_dict) + def _update_field_values(self, vals, field_name, attr_line): + """New method for update field values for a given attribute.""" + final_val = None + if not vals[field_name]: + return final_val + # Retrieve existing values for the attribute + value_ids = self.value_ids.filtered( + lambda value, attr_line=attr_line: value.attribute_id.id + == attr_line.attribute_id.id + ) + # Initialize `final_val` with IDs + final_val = value_ids.ids or [] + + # Process each `field_vals` operation for the current attribute + for field_vals in vals.get(field_name, []): + if field_vals and field_vals[0] == Command.SET: + final_val = list(set(field_vals[2] or [])) + elif field_vals and field_vals[0] == Command.LINK: + if field_vals[1] not in final_val: + final_val.append(field_vals[1]) + elif field_vals and field_vals[0] in (Command.UNLINK, Command.DELETE): + if field_vals[1] in final_val: + final_val.remove(field_vals[1]) + + return final_val + def update_config(self, attr_val_dict=None, custom_val_dict=None): """Update the session object with the given value_ids and custom values. diff --git a/product_configurator/pyproject.toml b/product_configurator/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/product_configurator/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_configurator/readme/CONTRIBUTORS.md b/product_configurator/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..9b6c1c6fd2 --- /dev/null +++ b/product_configurator/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Aion Tech](https://aiontech.company/): + - Simone Rubino \<\> diff --git a/product_configurator/readme/CONTRIBUTORS.rst b/product_configurator/readme/CONTRIBUTORS.rst deleted file mode 100644 index 6afa1541b4..0000000000 --- a/product_configurator/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,3 +0,0 @@ -* `Aion Tech `_: - - * Simone Rubino diff --git a/product_configurator/readme/DESCRIPTION.md b/product_configurator/readme/DESCRIPTION.md new file mode 100644 index 0000000000..d3dc3ea6eb --- /dev/null +++ b/product_configurator/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module has all the mechanics to support product configuration. It +serves as a base dependency for configuration interfaces. diff --git a/product_configurator/readme/DESCRIPTION.rst b/product_configurator/readme/DESCRIPTION.rst deleted file mode 100644 index a0b4c65152..0000000000 --- a/product_configurator/readme/DESCRIPTION.rst +++ /dev/null @@ -1,2 +0,0 @@ -This module has all the mechanics to support product configuration. It serves as a base -dependency for configuration interfaces. 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 0973764baf..952c22542c 100644 --- a/product_configurator/static/src/js/boolean_button_widget.esm.js +++ b/product_configurator/static/src/js/boolean_button_widget.esm.js @@ -1,58 +1,64 @@ /** @odoo-module **/ -const {onMounted, onRendered, useRef, useState} = owl; -import {BooleanField} from "@web/views/fields/boolean/boolean_field"; +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"; -export class BooleanButtonField extends BooleanField { +export class BooleanButton extends BooleanField { + static template = "product_configurator.BooleanButtonField"; + setup() { super.setup(); - this.state1 = useState({value: 0}); this.root = useRef("root"); + onMounted(() => { this.updateConfigurableButton(); }); + onRendered(() => { this.updateConfigurableButton(); }); } - onChange() { - this.state1.value++; - } - updateConfigurableButton() { - this.text = this.props.value + this.text = this.state.value ? this.props.activeString : this.props.inactiveString; - this.hover = this.props.value + this.hover = this.state.value ? this.props.inactiveString : this.props.activeString; - var val_color = this.props.value ? "text-success" : "text-danger"; - var hover_color = this.props.value ? "text-danger" : "text-success"; + + 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); + $(this.root.el).empty(); $(this.root.el).append($val).append($hover); } } -BooleanButtonField.props = { +BooleanButton.props = { ...standardFieldProps, - activeString: {type: String, optional: true}, + activeString: {type: String}, inactiveString: {type: String, optional: true}, }; -BooleanButtonField.extractProps = ({attrs}) => { - return { - activeString: attrs.options.active, - inactiveString: attrs.options.inactive, - }; +export const BooleanButtonField = { + ...booleanField, + component: BooleanButton, + extractProps: ({options}) => { + return { + activeString: options.active, + inactiveString: options.inactive, + }; + }, }; -BooleanButtonField.template = "product_configurator.BooleanButtonField"; +// Register the field component registry.category("fields").add("boolean_button", BooleanButtonField); diff --git a/product_configurator/static/src/js/boolean_button_widget.xml b/product_configurator/static/src/js/boolean_button_widget.xml index b1d6447d50..66f314d0c1 100644 --- a/product_configurator/static/src/js/boolean_button_widget.xml +++ b/product_configurator/static/src/js/boolean_button_widget.xml @@ -2,13 +2,12 @@ -
+
diff --git a/product_configurator/static/src/js/form_widgets.esm.js b/product_configurator/static/src/js/form_widgets.esm.js new file mode 100644 index 0000000000..46c9ed1471 --- /dev/null +++ b/product_configurator/static/src/js/form_widgets.esm.js @@ -0,0 +1,22 @@ +/* @odoo-module */ +import {FormController} from "@web/views/form/form_controller"; +import {patch} from "@web/core/utils/patch"; + +patch(FormController.prototype, { + setup() { + super.setup(...arguments); + if ( + this.props.resModel === "product.product" && + this.props.context.custom_create_variant + ) { + this.canCreate = false; + } + }, + async beforeExecuteActionButton(clickParams) { + if (clickParams.special === "no_save") { + delete clickParams.special; + return true; + } + return super.beforeExecuteActionButton(...arguments); + }, +}); diff --git a/product_configurator/static/src/js/form_widgets.js b/product_configurator/static/src/js/form_widgets.js deleted file mode 100644 index b45e17f26f..0000000000 --- a/product_configurator/static/src/js/form_widgets.js +++ /dev/null @@ -1,79 +0,0 @@ -odoo.define("product_configurator.FieldBooleanButton", function (require) { - "use strict"; - - var FormController = require("web.FormController"); - var ListController = require("web.ListController"); - var KanbanController = require("web.KanbanController"); - - var pyUtils = require("web.py_utils"); - - FormController.include({ - /* eslint-disable no-unused-vars*/ - renderButtons: function ($node) { - var self = this; - this._super.apply(this, arguments); - if ( - self.modelName === "product.product" && - self.initialState.context.custom_create_variant - ) { - this.$buttons.find(".o_form_button_create").css("display", "none"); - } - }, - /* eslint-disable no-unused-vars*/ - - _onButtonClicked: function (event) { - var self = this; - var attrs = event.data.attrs; - if (event.data.attrs.context) { - var record_ctx = self.model.get(event.data.record.id).context; - var btn_ctx = pyUtils.eval( - "context", - record_ctx, - event.data.attrs.context - ); - self.model.localData[event.data.record.id].context = _.extend( - {}, - btn_ctx, - record_ctx - ); - } - if (attrs.special === "no_save") { - this.canBeSaved = function () { - return true; - }; - var event_no_save = $.extend(true, {}, event); - event_no_save.data.attrs.special = false; - return this._super(event_no_save); - } - this._super(event); - }, - }); - ListController.include({ - /* eslint-disable no-unused-vars*/ - renderButtons: function ($node) { - var self = this; - this._super.apply(this, arguments); - if ( - self.modelName === "product.product" && - self.initialState.context.custom_create_variant - ) { - this.$buttons.find(".o_list_button_add").css("display", "none"); - } - }, - /* eslint-disable no-unused-vars*/ - }); - KanbanController.include({ - /* eslint-disable no-unused-vars*/ - renderButtons: function ($node) { - var self = this; - this._super.apply(this, arguments); - if ( - self.modelName === "product.product" && - self.initialState.context.custom_create_variant - ) { - this.$buttons.find(".o-kanban-button-new").css("display", "none"); - } - }, - /* eslint-disable no-unused-vars*/ - }); -}); diff --git a/product_configurator/static/src/js/kanban_widgets.esm.js b/product_configurator/static/src/js/kanban_widgets.esm.js new file mode 100644 index 0000000000..e829256ebe --- /dev/null +++ b/product_configurator/static/src/js/kanban_widgets.esm.js @@ -0,0 +1,15 @@ +/* @odoo-module */ +import {KanbanController} from "@web/views/kanban/kanban_controller"; +import {patch} from "@web/core/utils/patch"; + +patch(KanbanController.prototype, { + setup() { + super.setup(...arguments); + if ( + this.props.resModel === "product.product" && + this.props.context.custom_create_variant + ) { + this.props.archInfo.activeActions.create = false; + } + }, +}); diff --git a/product_configurator/static/src/js/list_widgest.esm.js b/product_configurator/static/src/js/list_widgest.esm.js new file mode 100644 index 0000000000..60b242f6d6 --- /dev/null +++ b/product_configurator/static/src/js/list_widgest.esm.js @@ -0,0 +1,15 @@ +/* @odoo-module */ +import {ListController} from "@web/views/list/list_controller"; +import {patch} from "@web/core/utils/patch"; + +patch(ListController.prototype, { + setup() { + super.setup(...arguments); + if ( + this.props.resModel === "product.product" && + this.props.context.custom_create_variant + ) { + this.activeActions.create = false; + } + }, +}); diff --git a/product_configurator/static/src/js/relational_fields.js b/product_configurator/static/src/js/relational_fields.js deleted file mode 100644 index cabb0950e7..0000000000 --- a/product_configurator/static/src/js/relational_fields.js +++ /dev/null @@ -1,26 +0,0 @@ -odoo.define("product_configurator.FieldStatus", function (require) { - "use strict"; - - var fields = require("web.relational_fields"); - var FieldStatus = fields.FieldStatus; - - FieldStatus.include({ - /* Prase input as string in order to have a clickable statusbar*/ - _onClickStage: function (e) { - this._setValue(String($(e.currentTarget).data("value"))); - }, - }); - - /* Bug from odoo: in case of widget many2many_tags $input and $el do not exist - in 'this', so it returns 'undefine', but setIDForLabel(method in AbstractField) - expecting getFocusableElement always return object*/ - fields.FieldMany2One.include({ - getFocusableElement: function () { - var element = this._super.apply(this, arguments); - if (element === undefined) { - return $(); - } - return element; - }, - }); -}); diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py index 7419a3fc5d..58bf1c79cc 100644 --- a/product_configurator/tests/test_configuration_rules.py +++ b/product_configurator/tests/test_configuration_rules.py @@ -304,9 +304,11 @@ def test_fill_restricted_custom_value(self): ], **{wiz_field: False for wiz_field in wizard_values.keys()}, }, - regular_attribute_field_name, + [regular_attribute_field_name], { - regular_attribute_field_name: "1", + regular_attribute_field_name: {"fields": "display_name"}, + custom_attribute_field_name: {"fields": "display_name"}, + other_custom_attribute_field_name: {"fields": "display_name"}, }, ) diff --git a/product_configurator/tests/test_wizard.py b/product_configurator/tests/test_wizard.py index 1a06604860..6a07f434c4 100644 --- a/product_configurator/tests/test_wizard.py +++ b/product_configurator/tests/test_wizard.py @@ -203,17 +203,7 @@ def test_06_onchange_product_tmpl(self): def test_07_get_onchange_domains(self): product_config_wizard = self._check_wizard_nxt_step() - conf = [ - "gasoline", - "228i", - "model_luxury_line", - "silver", - "rims_384", - "tapistry_black", - "steptronic", - "smoker_package", - "tow_hook", - ] + values = [ "gasoline", "228i", @@ -225,7 +215,7 @@ def test_07_get_onchange_domains(self): "smoker_package", "tow_hook", ] - product_config_wizard.get_onchange_domains(values, conf) + product_config_wizard.get_onchange_domains(values) def test_08_onchange_state(self): product_config_wizard = self._check_wizard_nxt_step() @@ -340,12 +330,12 @@ def test_12_fields_get(self): wizard_id=product_config_wizard_1.id ).fields_get() - def test_13_fields_view_get(self): + def test_13_get_view(self): product_config_wizard = self._check_wizard_nxt_step() - product_config_wizard.fields_view_get() + product_config_wizard.get_view() product_config_wizard.with_context( wizard_id=product_config_wizard.id - ).fields_view_get() + ).get_view() # custom value # custom value self.attr_line_fuel.custom = True @@ -408,7 +398,7 @@ def test_13_fields_view_get(self): product_config_wizard_1.action_next_step() product_config_wizard_1.with_context( wizard_id=product_config_wizard_1.id - ).fields_view_get() + ).get_view() def test_14_unlink(self): product_config_wizard = self._check_wizard_nxt_step() @@ -579,17 +569,10 @@ def test_16_get_onchange_domains(self): } ) field_prefix = self.wizard._prefixes.get("field_prefix") - check_available_val_id = { - field_prefix - + "%s" % (self.value_gasoline.attribute_id.id): self.value_gasoline.id, - field_prefix + "%s" % (self.value_218i.attribute_id.id): self.value_218i.id, - field_prefix - + "%s" % (self.value_sport_line.attribute_id.id): self.value_sport_line.id, - } values_ids = self.value_diesel.ids product_tmpl_id = self.config_product domains_available = self.wizard.get_onchange_domains( - check_available_val_id, values_ids, product_tmpl_id, session_id + values_ids, product_tmpl_id, session_id ) rec = domains_available[ field_prefix + str(self.value_sport_line.attribute_id.id) diff --git a/product_configurator/views/product_attribute_view.xml b/product_configurator/views/product_attribute_view.xml index 2316db3acc..5bd466c7a4 100644 --- a/product_configurator/views/product_attribute_view.xml +++ b/product_configurator/views/product_attribute_view.xml @@ -21,15 +21,16 @@ -
-
+
- + @@ -44,16 +45,8 @@ - - + + @@ -63,20 +56,20 @@ + @@ -154,15 +147,14 @@ - - product.attribute.value.tree + + product.attribute.value.list.inherit product.attribute.value + - - + - - +
diff --git a/product_configurator/views/product_config_view.xml b/product_configurator/views/product_config_view.xml index 768d6a4769..80c3dc2faf 100644 --- a/product_configurator/views/product_config_view.xml +++ b/product_configurator/views/product_config_view.xml @@ -55,7 +55,7 @@
@@ -57,10 +57,9 @@ - { - 'show_attribute': False, - 'attribute_line_ids': attribute_line_ids, - } + {'default_config_ok': context.get('default_config_ok', False)} @@ -73,22 +72,22 @@ domain="[('id', 'in', value_ids)]" context="{'show_attribute': False}" options="{'no_create': True, 'no_create_edit': True}" - invisible="not context.get('default_config_ok', False)" + column_invisible="not context.get('default_config_ok', False)" /> @@ -97,7 +96,7 @@ expr="//field[@name='attribute_line_ids']/tree/field[@name='value_ids']" position="attributes" > - {'required': [('custom','!=',True)]} + not custom @@ -145,12 +144,12 @@ - - + + @@ -169,10 +168,7 @@ string="Configuration Steps" name="configurator_steps" /> - + @@ -242,9 +238,7 @@ - {'invisible': [('config_ok','=',True)]} + config_ok @@ -296,7 +290,7 @@
@@ -318,21 +312,15 @@ class="oe_highlight" type="object" string="Reconfigure Product" - attrs="{'invisible': [('config_ok','=',False)]}" + invisible="not config_ok" /> - - @@ -345,15 +333,10 @@ - + - {'readonly': [('config_ok', '=', True)]} + config_ok @@ -366,14 +349,11 @@ - +
- -
+ -
+ + + +
diff --git a/product_configurator/wizard/product_configurator.py b/product_configurator/wizard/product_configurator.py index d8aead78f3..7e5152aba6 100644 --- a/product_configurator/wizard/product_configurator.py +++ b/product_configurator/wizard/product_configurator.py @@ -1,15 +1,15 @@ +import logging + from lxml import etree from odoo import _, api, fields, models, tools from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command from odoo.tools import frozendict from odoo.addons.base.models.ir_model import FIELD_TYPES -from odoo.addons.base.models.ir_ui_view import ( - transfer_field_to_modifiers, - transfer_modifiers_to_node, - transfer_node_to_modifiers, -) + +_logger = logging.getLogger(__name__) class FreeSelection(fields.Selection): @@ -39,6 +39,7 @@ def _prefixes(self): return { "field_prefix": "__attribute_", "custom_field_prefix": "__custom_", + "domain_field_prefix": "__domain_", } # TODO: Remove _prefix suffix as this is implied by the class property name @@ -134,7 +135,6 @@ def onchange_product_tmpl(self): def get_onchange_domains( self, - values, cfg_val_ids, product_tmpl_id=False, config_session_id=False, @@ -155,19 +155,12 @@ def get_onchange_domains( if not config_session_id: config_session_id = self.config_session_id - custom_val = config_session_id.get_custom_value_id() domains = {} check_avail_ids = cfg_val_ids[:] for line in product_tmpl_id.attribute_line_ids.sorted(): field_name = field_prefix + str(line.attribute_id.id) - if field_name not in values: - continue - - vals = values[field_name] - # get available values - attribute_line_values = line._configurator_value_ids() avail_ids = config_session_id.values_available( check_val_ids=attribute_line_values.ids, @@ -179,11 +172,6 @@ def get_onchange_domains( check_avail_ids = list( set(check_avail_ids) - (set(line.value_ids.ids) - set(avail_ids)) ) - # Include custom value in the domain if attr line permits it - if line.custom and custom_val.id in avail_ids: - domains[field_name][0][2].append(custom_val.id) - if line.multi and vals and custom_val.id in vals[0][2]: - continue return domains def get_onchange_vals(self, cfg_val_ids, config_session_id=None): @@ -212,6 +200,7 @@ def get_form_vals( cfg_val_ids=None, product_tmpl_id=None, config_session_id=None, + values=None, ): """Generate a dictionary to return new values via onchange method. Domains hold the values available, this method enforces these values @@ -224,32 +213,73 @@ def get_form_vals( """ vals = {} dynamic_fields = {k: v for k, v in dynamic_fields.items() if v} + # List to store multi-value IDs + available_val_ids_m2m = [] for k, v in dynamic_fields.items(): if not v: continue available_val_ids = domains[k][0][2] + # Get all value_ids linked to the current config session + value_ids = self.config_session_id.value_ids + # Filter attribute lines for multi-select attributes that match IDs + # in value_ids + attribute_line_ids = self.product_tmpl_id.attribute_line_ids.filtered( + lambda line, value_ids=value_ids: line.multi + and line.attribute_id.id in value_ids.mapped("attribute_id").ids + ) + # Get multi-value IDs that match attribute lines + # Filter the `multi_value_ids` associated with attributes in + # `attribute_line_ids` + multi_value_ids = value_ids.filtered( + lambda value, + attribute_line_ids=attribute_line_ids: value.attribute_id.id + in attribute_line_ids.mapped("attribute_id").ids + ) + + # Retrieve IDs of available multi-value options + available_val_ids_m2m = multi_value_ids.ids + + # Process values for the current attribute field if isinstance(v, list): - if any(not isinstance(el, int) for el in v): - v = v[0][2] - value_ids = list(set(v) & set(available_val_ids)) - dynamic_fields.update({k: value_ids}) - vals[k] = [[6, 0, value_ids]] + for sub_value in v: + if sub_value[0] == Command.UNLINK: + if sub_value[1] in available_val_ids_m2m: + available_val_ids_m2m.remove(sub_value[1]) + elif sub_value[0] == Command.LINK: + if sub_value[1] not in available_val_ids_m2m: + available_val_ids_m2m.append(sub_value[1]) + elif sub_value[0] == Command.SET: + available_val_ids_m2m = sub_value[2] + + # Update dynamic fields and set `vals` with modified multi-value IDs + dynamic_fields.update({k: available_val_ids_m2m}) + vals[k] = [[Command.SET, 0, available_val_ids_m2m]] + elif v not in available_val_ids: + # Handle single values not in available IDs dynamic_fields.update({k: None}) vals[k] = None else: + # Use the single value if it exists in available IDs vals[k] = v - final_cfg_val_ids = list(dynamic_fields.values()) - vals.update(self.get_onchange_vals(final_cfg_val_ids, config_session_id)) + field_prefix = self._prefixes.get("field_prefix") + # List of attributes to remove from value_ids as they are currently changed + attributes_to_consider_removal = [ + int(field.split(field_prefix)[1]) for field in vals if field_prefix in field + ] + filtered_value_ids = self.value_ids.filtered( + lambda val: val.attribute_id.id not in attributes_to_consider_removal + ).ids + final_config_values = list(filtered_value_ids + list(dynamic_fields.values())) + vals.update(self.get_onchange_vals(final_config_values, config_session_id)) # 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])]] - return vals - def apply_onchange_values(self, values, field_name, field_onchange): + def apply_onchange_values(self, values, field_names, field_onchange): """Called from web-controller - original onchage return M2o values in formate (attr-value.id, attr-value.name) but on website @@ -269,7 +299,6 @@ def apply_onchange_values(self, values, field_name, field_onchange): state = values.get("state", False) if not state: state = self.state - cfg_vals = self.env["product.attribute.value"] if values.get("value_ids", []): cfg_vals = self.env["product.attribute.value"].browse( @@ -278,16 +307,20 @@ def apply_onchange_values(self, values, field_name, field_onchange): if not cfg_vals: cfg_vals = self.value_ids - field_type = type(field_name) - field_prefix = self._prefixes.get("field_prefix") custom_field_prefix = self._prefixes.get("custom_field_prefix") - if field_type == list or ( - not field_name.startswith(field_prefix) - and not field_name.startswith(custom_field_prefix) - ): + domain_field_prefix = self._prefixes.get("domain_field_prefix") + local_field_name = field_names and field_names[0].startswith(field_prefix) + local_custom_field = field_names and field_names[0].startswith( + custom_field_prefix + ) + local_domain_prefix = field_names and field_names[0].startswith( + domain_field_prefix + ) + if not local_field_name and not local_custom_field and not local_domain_prefix: values = self._remove_dynamic_fields(values) - res = super().onchange(values, field_name, field_onchange) + field_onchange = self._remove_dynamic_fields(field_onchange) + res = super().onchange(values, field_names, field_onchange) return res view_val_ids = set() @@ -308,6 +341,7 @@ def apply_onchange_values(self, values, field_name, field_onchange): 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) @@ -316,7 +350,10 @@ def apply_onchange_values(self, values, field_name, field_onchange): if not v: continue if isinstance(v, list): - view_val_ids |= set(v[0][2]) + if v[0][0] == Command.SET: + view_val_ids |= set(v[0][2]) + else: + view_val_ids |= {a[1] for a in v} elif isinstance(v, int): view_val_ids.add(v) @@ -324,28 +361,36 @@ def apply_onchange_values(self, values, field_name, field_onchange): cfg_vals = cfg_vals.filtered( lambda v: v.attribute_id.id not in view_attribute_ids ) - # Combine database values with wizard values_available cfg_val_ids = cfg_vals.ids + list(view_val_ids) domains = self.get_onchange_domains( - values, cfg_val_ids, product_tmpl_id, config_session_id + cfg_val_ids, product_tmpl_id, config_session_id ) + vals = self.get_form_vals( dynamic_fields=dynamic_fields, domains=domains, product_tmpl_id=product_tmpl_id, config_session_id=config_session_id, + values=values, ) - + vals.update(self._transform_onchange_domain_field_vals(domains)) return {"value": vals, "domain": domains} - def onchange(self, values, field_name, field_onchange): + def _transform_onchange_domain_field_vals(self, domains): + vals = {} + for field, value in domains.items(): + domain_field = field.replace("attribute", "domain") + vals[domain_field] = value + return vals + + def onchange(self, values: dict, field_names: list[str], fields_spec: dict): """Override the onchange wrapper to return domains to dynamic fields as onchange isn't triggered for non-db fields """ onchange_values = self.apply_onchange_values( - values=values, field_name=field_name, field_onchange=field_onchange + values=values, field_names=field_names, field_onchange=fields_spec ) field_prefix = self._prefixes.get("field_prefix") vals = onchange_values.get("value", {}) @@ -404,9 +449,16 @@ def _onchange_state(self): @api.onchange("product_preset_id") def _onchange_product_preset(self): """Set value ids as from product preset""" - pta_value_ids = self.product_preset_id.product_template_attribute_value_ids + preset_id = self.product_preset_id + if not preset_id and self.env.context.get("preset_values"): + preset_id = self.env.context.get("preset_values").get("product_preset_id") + 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.value_ids = attr_value_ids + self._origin.value_ids = attr_value_ids + self._origin.price = ( + preset_id and preset_id.lst_price or self.product_tmpl_id.list_price + ) @api.model def get_field_default_attrs(self): @@ -472,10 +524,7 @@ def fields_get(self, allfields=None, write_access=True, attributes=None): attribute = line.attribute_id value_ids = line.value_ids.ids - value_ids = wiz.config_session_id.values_available( - check_val_ids=value_ids, - product_template_attribute_line_id=line.id, - ) + value_ids = wiz.config_session_id.values_available(check_val_ids=value_ids) # If attribute lines allows custom values add the # generic "Custom" attribute.value to the list of options @@ -503,15 +552,24 @@ def fields_get(self, allfields=None, write_access=True, attributes=None): type=field_type, sequence=line.sequence, ) + domain_field_prefix = self._prefixes.get("domain_field_prefix") + domain_field = domain_field_prefix + str(attribute.id) + res[domain_field] = dict( + default_attrs, + type="binary", + string="Domain %s" % line.attribute_id.name, + change_default=True, + ) # Add the dynamic field to the result set using the convention # "__attribute_DBID" to later identify and extract it res[field_prefix + str(attribute.id)] = dict( default_attrs, type="many2many" if line.multi else "many2one", - domain=[("id", "in", value_ids)], + domain="%s" % domain_field, string=line.attribute_id.name, relation="product.attribute.value", + change_default=True, # sequence=line.sequence, ) return res @@ -541,6 +599,7 @@ def get_view(self, view_id=None, view_type="form", **options): dynamic_fields = { k: v for k, v in fields.items() if k.startswith(dynamic_field_prefixes) } + models = dict(res["models"]) models[wizard_model] = models[wizard_model] + tuple(dynamic_fields.keys()) res["models"] = frozendict(models) @@ -551,33 +610,6 @@ def get_view(self, view_id=None, view_type="form", **options): res.update({"arch": etree.tostring(mod_view)}) return res - @api.model - def setup_modifiers(self, node, field=None): - """Processes node attributes and field descriptors to generate - the ``modifiers`` node attribute and set it on the provided node. - - Alters its first argument in-place. - - :param node: ``field`` node from an OpenERP view - :type node: lxml.etree._Element - :param dict field: field descriptor corresponding to the provided node - :param dict context: execution context used to evaluate node attributes - :param bool current_node_path: triggers the ``column_invisible`` code - path (separate from ``invisible``): in - tree view there are two levels of - invisibility, cell content (a column is - present but the cell itself is not - displayed) with ``invisible`` and column - invisibility (the whole column is - hidden) with ``column_invisible``. - :returns: nothing - """ - modifiers = {} - if field is not None: - transfer_field_to_modifiers(field=field, modifiers=modifiers) - transfer_node_to_modifiers(node=node, modifiers=modifiers) - transfer_modifiers_to_node(modifiers=modifiers, node=node) - def prepare_attrs_initial( self, attr_lines, field_prefix, custom_field_prefix, dynamic_fields, wiz ): @@ -585,9 +617,10 @@ def prepare_attrs_initial( for attr_line in attr_lines: attribute_id = attr_line.attribute_id.id field_name = field_prefix + str(attribute_id) + domain_field_prefix = self._prefixes.get("domain_field_prefix") + domain_field_name = domain_field_prefix + str(attribute_id) custom_field = custom_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 @@ -596,23 +629,24 @@ def prepare_attrs_initial( ) # attrs property for dynamic fields - attrs = {"readonly": [], "required": [], "invisible": []} + attrs = {"readonly": "", "required": "", "invisible": ""} + invisible_str = "" + readonly_str = "" + required_str = "" if config_steps: cfg_step_ids = [str(id) for id in config_steps.ids] - attrs["invisible"].append(("state", "not in", cfg_step_ids)) - attrs["readonly"].append(("state", "not in", cfg_step_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: - attrs["required"].append(("state", "in", cfg_step_ids)) + required_str = f"state in {cfg_step_ids}" else: - attrs["invisible"].append(("state", "not in", ["configure"])) - attrs["readonly"].append(("state", "not in", ["configure"])) - + 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: - attrs["required"].append(("state", "in", ["configure"])) + required_str = "state in {}".format(["configure"]) if attr_line.custom: pass @@ -627,40 +661,73 @@ def prepare_attrs_initial( # configuration step then we must use attrs to enable/disable the # required and readonly depending on the value entered in the # dependee + # Create a dictionary of attribute dependencies + 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, attr_id=attr_id: line.attribute_id.id + == attr_id + ).value_ids + val_ids = val_ids - domain_line.value_ids + attr_depends[attr_field] |= set(val_ids.ids) + + # Apply dependency conditions + readonly_str, required_str = self._generate_dependency_attributes( + attr_line, attr_depends, dynamic_fields, readonly_str, required_str + ) + + attrs = { + "readonly": readonly_str, + "required": required_str, + "invisible": invisible_str, + } - if attr_line.value_ids > dependencies.mapped("value_ids"): + return ( + attrs, + field_name, + custom_field, + config_steps, + cfg_step_ids, + domain_field_name, + ) + + def _generate_dependency_attributes( + self, attr_line, attr_depends, dynamic_fields, readonly_str, required_str + ): + """ + Applies conditions based on attribute dependencies to readonly and required + strings.""" + if attr_line.custom: + return readonly_str, required_str + for dependee_field, val_ids in attr_depends.items(): + if not val_ids: continue - 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, attr_id=attr_id: 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: - attrs["readonly"].append((dependee_field, "not in", list(val_ids))) + field_type = dynamic_fields.get(dependee_field, {}).get("type") + if field_type != "many2many": + readonly_str += f" and {dependee_field} not in {str(list(val_ids))}" + if ( + attr_line.required + and not attr_line.custom + and field_type != "many2many" + ): + required_str += f" and {dependee_field} in {str(list(val_ids))}" - if attr_line.required and not attr_line.custom: - attrs["required"].append((dependee_field, "in", list(val_ids))) - return attrs, field_name, custom_field, config_steps, cfg_step_ids + return readonly_str, required_str @api.model def add_dynamic_fields(self, res, dynamic_fields, wiz): @@ -695,27 +762,27 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz): custom_field, config_steps, cfg_step_ids, + domain_field_name, ) = self.prepare_attrs_initial( attr_line, field_prefix, custom_field_prefix, dynamic_fields, wiz ) - if len(attrs["readonly"]) > 1 and attrs["readonly"][0] != "|": - attrs["readonly"].insert(0, "|") - if len(attrs["invisible"]) > 1 and attrs["invisible"][0] != "|": - attrs["invisible"].insert(0, "|") - # 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", - attrs=str(attrs), + 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( @@ -727,14 +794,21 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz): ), ) + xml_dynamic_form.append(node) + domain_node = etree.Element( + "field", + name=domain_field_name, + on_change="1", + readonly="1", + invisible="1", + ) + xml_dynamic_form.append(domain_node) + 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) - xml_dynamic_form.append(node) - if attr_line.custom and custom_field in dynamic_fields: widget = "" config_session_obj = self.env["product.config.session"] @@ -745,26 +819,40 @@ def add_dynamic_fields(self, res, dynamic_fields, wiz): else: field_val = custom_option_id - attrs["readonly"] += [(field_name, "!=", field_val)] - attrs["invisible"] += [(field_name, "!=", field_val)] - attrs["required"] += [(field_name, "=", field_val)] + attrs.update( + { + "readonly": attrs.get("readonly") + + f" or {field_name} != {field_val}" + } + ) + attrs.update( + { + "invisible": attrs.get("invisible") + + f" or {field_name} != {field_val}" + } + ) + attrs.update( + { + "required": attrs.get("required") + + f" or {field_name} != {field_val}" + } + ) if config_steps: - attrs["required"] += [("state", "in", cfg_step_ids)] + attrs.update( + { + "required": attrs.get("required") + + f" or 'state' in {cfg_step_ids}" + } + ) # TODO: Add a field2widget mapper if attr_line.attribute_id.custom_type == "color": widget = "color" - if len(attrs["invisible"]) > 1 and attrs["invisible"][0] != "|": - attrs["invisible"].insert(0, "|") - if len(attrs["readonly"]) > 1 and attrs["readonly"][0] != "|": - attrs["readonly"].insert(0, "|") - node = etree.Element( - "field", name=custom_field, attrs=str(attrs), widget=widget + "field", name=custom_field, attrib=attrs, widget=widget ) - self.setup_modifiers(node) xml_dynamic_form.append(node) return xml_view @@ -802,11 +890,11 @@ def read(self, fields=None, load="_classic_read"): field_prefix = self._prefixes.get("field_prefix") custom_field_prefix = self._prefixes.get("custom_field_prefix") - + domain_field_prefix = self._prefixes.get("domain_field_prefix") 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)] - - dynamic_fields = attr_vals + custom_attr_vals + domain_attr_vals = [f for f in fields if f.startswith(domain_field_prefix)] + dynamic_fields = attr_vals + custom_attr_vals + domain_attr_vals fields = self._remove_dynamic_fields(fields) custom_val = self.env["product.config.session"].get_custom_value_id() @@ -827,10 +915,23 @@ def read(self, fields=None, load="_classic_read"): continue custom_field_name = custom_field_prefix + str(attr_id) - + domain_field_name = domain_field_prefix + str(attr_id) + available_value_ids = self.config_session_id.values_available( + check_val_ids=attr_line.value_ids.ids, + product_template_attribute_line_id=attr_line.id, + ) + if attr_line.custom: + config_session_obj = self.env["product.config.session"] + custom_val = config_session_obj.get_custom_value_id() + available_value_ids.append(custom_val.id) # Handle default values for dynamic fields on Odoo frontend - res[0].update({field_name: False, custom_field_name: False}) - + res[0].update( + { + field_name: [] if attr_line.multi else False, + custom_field_name: False, + domain_field_name: [("id", "in", available_value_ids)], + } + ) custom_vals = self.custom_value_ids.filtered( lambda x, attr_id=attr_id: x.attribute_id.id == attr_id ).with_context(show_attribute=False) @@ -848,7 +949,7 @@ def read(self, fields=None, load="_classic_read"): 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, @@ -857,12 +958,17 @@ def read(self, fields=None, load="_classic_read"): ) elif attr_line.multi: dynamic_vals = {field_name: vals.ids} + dynamic_vals = { + field_name: [ + {"id": v.id, "display_name": v.display_name} for v in vals + ] + } 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 @@ -917,7 +1023,6 @@ def action_previous_step(self): wizard_action = self.with_context( wizard_id=self.id, view_cache=False, allow_preset_selection=False ).get_wizard_action(wizard=self) - cfg_step_lines = self.product_tmpl_id.config_step_line_ids if not cfg_step_lines: self.state = "select" @@ -946,17 +1051,10 @@ def action_reset(self): """Delete wizard and configuration session then create a new wizard+session and return an action for the new wizard object""" try: - session = self.config_session_id - # Field parent_id(of session) defind in - # product_configurator_subconfig, so this code should - # be moved in product_configurator_subconfig - # while session.parent_id: - # session = session.parent_id - session_product_tmpl_id = session.product_tmpl_id - session.unlink() - except Exception: - session = self.env["product.config.step"] - + session_product_tmpl_id = self.config_session_id.product_tmpl_id + self.config_session_id.unlink() + except Exception as e: + _logger.error("Error while resetting configuration session: %s", e) action = self.with_context( wizard_id=None, allow_preset_selection=False, diff --git a/product_configurator/wizard/product_configurator_view.xml b/product_configurator/wizard/product_configurator_view.xml index 6114d0e83c..485bd22869 100644 --- a/product_configurator/wizard/product_configurator_view.xml +++ b/product_configurator/wizard/product_configurator_view.xml @@ -15,30 +15,32 @@ confirm="Are you sure? This will remove your current configuration for this template!" special="no_save" class="oe_highlight" - attrs="{'invisible': ['|', ('product_tmpl_id', '=', False), ('state', '=', 'select')]}" + invisible="not product_tmpl_id or state == 'select'" /> - + + @@ -49,11 +51,11 @@ /> @@ -78,7 +80,7 @@