diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 634b97d5f..23ae79a74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,7 @@ 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$| diff --git a/product_configurator/README.md b/product_configurator/README.md new file mode 100644 index 000000000..affc7d903 --- /dev/null +++ b/product_configurator/README.md @@ -0,0 +1,25 @@ +#Odoo Product Configurator + +This module is Dynamic configuration wizard for Odoo back-end and the foundation for +external configuration interfaces such 'website_product_configurator'. + +By itself this module does not configure custom products but offers the basis for +generating, validating, updating configurable products using configuration interfaces. + +# Features + +- Inhibition of automatically created variants. +- Extension of attribute lines to offer required, custom and multiple selection. +- Configuration / Compatibility rules between attributes. +- Separation of attributes in different steps. +- Images for intermediate and final configurations. +- Managing active configuration sessions for external configurators +- Set of helper methods required for any Odoo configuration module. + +# Usage + +This module is Dynamic configuration wizard for Odoo back-end and the foundation for +external configuration interfaces such 'website_product_configurator'. + +By itself this module does not configure custom products but offers the basis for +generating, validating, updating configurable products using configuration interfaces. diff --git a/product_configurator/README.rst b/product_configurator/README.rst new file mode 100644 index 000000000..b6fb7d4e0 --- /dev/null +++ b/product_configurator/README.rst @@ -0,0 +1,87 @@ +==================== +Product Configurator +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9ddc483727ae5c864626d3f7df20a5648deffc29d784bd82b5fd2a3eccee0f03 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.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 + :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 + :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. + +**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 +~~~~~~~ + +* Pledra + +Contributors +~~~~~~~~~~~~ + +* `Aion Tech `_: + + * Simone Rubino + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-PCatinean| image:: https://github.com/PCatinean.png?size=40px + :target: https://github.com/PCatinean + :alt: PCatinean + +Current `maintainer `__: + +|maintainer-PCatinean| + +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/__init__.py b/product_configurator/__init__.py new file mode 100644 index 000000000..ad2c86ac0 --- /dev/null +++ b/product_configurator/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import wizard + +from .init_hook import post_init_hook diff --git a/product_configurator/__manifest__.py b/product_configurator/__manifest__.py new file mode 100644 index 000000000..55e4e0cfa --- /dev/null +++ b/product_configurator/__manifest__.py @@ -0,0 +1,53 @@ +{ + "name": "Product Configurator", + "version": "16.0.1.1.1", + "category": "Generic Modules/Base", + "summary": "Base for product configuration interface modules", + "author": "Pledra, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/product-configurator", + "external_dependencies": { + "python": [ + "mako", + ] + }, + "depends": ["account"], + "data": [ + "security/configurator_security.xml", + "security/ir.model.access.csv", + "views/res_config_settings_view.xml", + "data/menu_configurable_product.xml", + "data/product_attribute.xml", + "data/ir_sequence_data.xml", + "data/ir_config_parameter_data.xml", + "views/product_view.xml", + "views/product_attribute_view.xml", + "views/product_config_view.xml", + "wizard/product_configurator_view.xml", + ], + "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/boolean_button_widget.esm.js", + "/product_configurator/static/src/js/boolean_button_widget.xml", + "/product_configurator/static/src/js/relational_fields.js", + ] + }, + "demo": [ + "demo/product_template.xml", + "demo/product_attribute.xml", + "demo/product_config_domain.xml", + "demo/product_config_lines.xml", + "demo/product_config_step.xml", + "demo/config_image_ids.xml", + ], + "images": ["static/description/cover.png"], + "post_init_hook": "post_init_hook", + "qweb": ["static/xml/create_button.xml"], + "development_status": "Beta", + "maintainers": ["PCatinean"], + "installable": False, + "application": True, + "auto_install": False, +} diff --git a/product_configurator/data/ir_config_parameter_data.xml b/product_configurator/data/ir_config_parameter_data.xml new file mode 100644 index 000000000..f68621178 --- /dev/null +++ b/product_configurator/data/ir_config_parameter_data.xml @@ -0,0 +1,9 @@ + + + + product_configurator.manager_product_configuration_settings + True + + diff --git a/product_configurator/data/ir_sequence_data.xml b/product_configurator/data/ir_sequence_data.xml new file mode 100644 index 000000000..805d5c689 --- /dev/null +++ b/product_configurator/data/ir_sequence_data.xml @@ -0,0 +1,9 @@ + + + + Configuration Session + product.config.session + CS + 4 + + diff --git a/product_configurator/data/menu_configurable_product.xml b/product_configurator/data/menu_configurable_product.xml new file mode 100644 index 000000000..63e21736c --- /dev/null +++ b/product_configurator/data/menu_configurable_product.xml @@ -0,0 +1,132 @@ + + + + + + + + + Configurable Templates + ir.actions.act_window + product.template + kanban,tree,form + + {'default_config_ok': True, 'custom_create_variant': True, 'search_default_filter_config_ok': 1} + + + + + + Configured Variants + ir.actions.act_window + product.product + kanban,form,tree + + + {'default_config_ok': True, 'custom_create_variant': True, 'search_default_filter_config_ok': 1} + + + + + + Settings + ir.actions.act_window + res.config.settings + form + inline + + {'module' : 'product_configurator'} + + + + + + Configuration Steps + ir.actions.act_window + product.config.step + tree,form + + + + + + Configuration Restrictions + ir.actions.act_window + product.config.domain + tree,form + + + + + + Configuration Sessions + ir.actions.act_window + product.config.session + tree,form + + + + + diff --git a/product_configurator/data/product_attribute.xml b/product_configurator/data/product_attribute.xml new file mode 100644 index 000000000..5b7df2a41 --- /dev/null +++ b/product_configurator/data/product_attribute.xml @@ -0,0 +1,15 @@ + + + + + Custom + + + + + Custom + + + + + diff --git a/product_configurator/demo/config_image_ids.xml b/product_configurator/demo/config_image_ids.xml new file mode 100644 index 000000000..47d6d2f0b --- /dev/null +++ b/product_configurator/demo/config_image_ids.xml @@ -0,0 +1,156 @@ + + + + + + + Coupé Red + + + + + + + Coupé Silver + + + + + + + Coupé Black + + + + + + + Coupé Red Rims 384 + + + + + + + Coupé Red Rims 387 + + + + + + + Coupé Silver Rims 384 + + + + + + + Coupé Silver Rims 387 + + + + + + + Coupé Black Rims 384 + + + + + + + Coupé Black Rims 387 + + + + + + diff --git a/product_configurator/demo/product_attribute.xml b/product_configurator/demo/product_attribute.xml new file mode 100644 index 000000000..ded2454eb --- /dev/null +++ b/product_configurator/demo/product_attribute.xml @@ -0,0 +1,408 @@ + + + + + + + Fuel + + + + Gasoline + + + + + Diesel + + + + + + + Engine + + + + + + 218i + + + + + + 220i + + + + + + 228i + + + + + + M235i + + + + + + M235i xDrive + + + + + + + + 218d + + + + + + 220d + + + + + + 220d xDrive + + + + + + 225d + + + + + + + + Lines + + + + Sport Line + + + + + + Model Sport Line + + + + + + Luxury Line + + + + + + Model Luxury Line + + + + + + Model M Sport + + + + + + Model Advantage + + + + + + + + Paint Color + + + + Red + + + + + Silver + + + + + + Black + + + + + + + Rims + + + + V-spoke 16" + + + + + V-spoke 18" + + + + + Double-spoke 18" + + + + + + + Tapistry + + + + Black + + + + + Oyster/Black + + + + + Coral Red/Black + + + + + + + Transmission + + + + Automatic (Steptronic) + + + + + + Automatic Sport (Steptronic) + + + + + + + + Options + + + + Armrest + + + + + + Smoker Package + + + + + + Sunroof + + + + + + Tow hook + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/product_configurator/demo/product_config_domain.xml b/product_configurator/demo/product_config_domain.xml new file mode 100644 index 000000000..e57d75f2a --- /dev/null +++ b/product_configurator/demo/product_config_domain.xml @@ -0,0 +1,84 @@ + + + + + + + + + Gasoline + + + + + + in + and + + + + + + + Diesel + + + + + + in + and + + + + + + + + 218i Engine + + + + + + in + and + + + + + + + + Luxury Lines + + + + + + in + and + + + + diff --git a/product_configurator/demo/product_config_lines.xml b/product_configurator/demo/product_config_lines.xml new file mode 100644 index 000000000..616bb7225 --- /dev/null +++ b/product_configurator/demo/product_config_lines.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/product_configurator/demo/product_config_step.xml b/product_configurator/demo/product_config_step.xml new file mode 100644 index 000000000..797b7ebdc --- /dev/null +++ b/product_configurator/demo/product_config_step.xml @@ -0,0 +1,81 @@ + + + + + + + Engine + + + + Body + + + + Lines + + + + Interior + + + + Extras + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/product_configurator/demo/product_template.xml b/product_configurator/demo/product_template.xml new file mode 100644 index 000000000..e97284215 --- /dev/null +++ b/product_configurator/demo/product_template.xml @@ -0,0 +1,270 @@ + + + + + + + + BMW + + + + + + 2 Series + + consu + + + + + + + + + Sport Line + consu + + + + + + Luxury Line + consu + + + + + + Model Sport Line + consu + + + + + + Model Luxury Line + consu + + + + + + Model M Sport + consu + + + + + + Model Advantage + consu + + + + + + Automatic Transmission Steptronic + consu + + + + + + Sport Automatic Transmission Steptronic + consu + + + + + + Sunroof + consu + + + + + + Armrest + consu + + + + + + Towhook + consu + + + + + + Smoker Package + consu + + + + + + 218i Coupé + consu + + + + + + 220i Coupé + consu + + + + + + 228i Coupé + consu + + + + + + M235i Coupé + consu + + + + + + M235i xDrive Coupe + consu + + + + + + 218d Coupé + consu + + + + + + 220d Coupé + consu + + + + + + 220d xDrive Coupé + consu + + + + + + 225d Coupé + consu + + + + + + Silver Paint + consu + + + + + diff --git a/product_configurator/i18n/it.po b/product_configurator/i18n/it.po new file mode 100644 index 000000000..c1bb19174 --- /dev/null +++ b/product_configurator/i18n/it.po @@ -0,0 +1,1562 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_configurator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-04-03 09:36+0000\n" +"Last-Translator: Simone Rubino \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Attribute/s: %s" +msgstr "" +"\n" +"Attributo/i: %s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Restriction: %s" +msgstr "" +"\n" +"Limitazione: %s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Value/s: %s\n" +msgstr "" +"\n" +"Valore/i: %s\n" + +#. module: product_configurator +#: model:product.template,name:product_configurator.bmw_2_series +msgid "2 Series" +msgstr "2 serie" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218d +msgid "218d" +msgstr "218d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218d_coupe_product_template +msgid "218d Coupé" +msgstr "218d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218i +msgid "218i" +msgstr "218i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218i_coupe_product_template +msgid "218i Coupé" +msgstr "218i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d +msgid "220d" +msgstr "220d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_coupe_product_template +msgid "220d Coupé" +msgstr "220d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d_xdrive +msgid "220d xDrive" +msgstr "220d xDrive" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_xdrive_coupe_product_template +msgid "220d xDrive Coupé" +msgstr "220d xDrive Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220i +msgid "220i" +msgstr "220i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220i_coupe_product_template +msgid "220i Coupé" +msgstr "220i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_225d +msgid "225d" +msgstr "225d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_225d_coupe_product_template +msgid "225d Coupé" +msgstr "225d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_228i +msgid "228i" +msgstr "228i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_228i_coupe_product_template +msgid "228i Coupé" +msgstr "228i Coupé" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_kanban_view_inherited +msgid "" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__active +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__active +msgid "Active" +msgstr "Attivo" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"All attribute values used in attribute value lines must be defined in the " +"attribute lines of the template" +msgstr "" +"Tutti i valori attributo utilizzati nelle righe valore attributo nelle righe " +"attributo del modello" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__val_custom +msgid "Allow custom value for this attribute?" +msgstr "Consentire valore personalizzato per questo attributo?" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__custom +msgid "Allow custom values for this attribute?" +msgstr "Consentire valori personalizzati per questo attributo?" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__multi +msgid "Allow selection of multiple values for this attribute?" +msgstr "Consentire selezione di valori multipli per questo attributo?" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attr_line_val_ids +msgid "Allowed Attribute Values" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "An error occurred while parsing value for attribute %s" +msgstr "Si è verificato un errore nell'analisi del valore per l'attributo %s" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"Are you sure? This will remove your current configuration for this template!" +msgstr "" +"Si è sicuri? Questo rimuoverà la configurazione attuale per questo modello!" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_armrest +#: model:product.template,name:product_configurator.product_2_series_armrest_product_template +msgid "Armrest" +msgstr "Bracciolo" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__binary +msgid "Attachment" +msgstr "Allegato" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attachment_ids +msgid "Attachments" +msgstr "Allegati" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attribute_id +msgid "Attribute" +msgstr "Attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_line_ids +msgid "Attribute Dependencies" +msgstr "Dipendenze attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attribute_line_id +msgid "Attribute Line" +msgstr "Riga attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_line_val_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_line_val_ids +msgid "Attribute Line Val" +msgstr "Valore riga attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__attribute_line_ids +msgid "Attribute Lines" +msgstr "Righe attributo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_id +msgid "Attribute Value" +msgstr "Valore attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_value_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_value_line_ids +msgid "Attribute Value Lines" +msgstr "Righe valore attributo" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.variants_action +#: model:ir.ui.menu,name:product_configurator.menu_variants_action_configuration +msgid "Attribute Values" +msgstr "Valori attributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_value__weight_extra +msgid "Attribute Weight Extra" +msgstr "Extra peso attributo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__image +msgid "Attribute value image (Display on website for radio buttons)" +msgstr "" +"Immagine valore attributo (visualizzata nel sito web per selettore di tipo " +"radio)" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_attributes_view +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__attribute_line_ids +#: model:ir.ui.menu,name:product_configurator.menu_attribute_action_configuration +msgid "Attributes" +msgstr "Attributi" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic +msgid "Automatic (Steptronic)" +msgstr "Automatico (Steptronic)" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic_sport +msgid "Automatic Sport (Steptronic)" +msgstr "Automatico sport (Steptronic)" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_product_template +msgid "Automatic Transmission Steptronic" +msgstr "Trasmissione automatica Steptronic" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Back" +msgstr "Indietro" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_black +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_black +msgid "Black" +msgstr "Nero" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_body +msgid "Body" +msgstr "Corpo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__active +msgid "" +"By unchecking the active field you can disable a attribute value without " +"deleting it" +msgstr "" +"Deselezionando il campo attivo si può disabilitare il valore di un attributo " +"senza cancellarlo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__active +msgid "" +"By unchecking the active field you can disable a attribute without deleting " +"it" +msgstr "" +"Deselezionando il campo attivo si può disabilitare un attributo senza " +"cancellarlo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_ok +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_ok +msgid "Can be Configured" +msgstr "Può essere configurato" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Cannot have a configuration step defined twice." +msgstr "Non si può avere un passo di configurazione definito due volte." + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "" +"Changing the product template while having an active configuration will " +"erase reset/clear all values" +msgstr "" +"Modificare un modello prodotto con una configurazione attiva cancellerà il " +"ripristino/pulizia di tutti i valori" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__char +msgid "Char" +msgstr "Char" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__color +msgid "Color" +msgstr "Colore" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__condition +msgid "Condition" +msgstr "Condizione" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurable" +msgstr "Configurabile" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_product_main +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Configurable Products" +msgstr "Prodotti configurabili" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Configurable Products cannot have duplicates (identical attribute values)" +msgstr "" +"I prodotti configurabili non possono avere duplicati (valori attributi " +"identici)" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_tmpl_id +msgid "Configurable Template" +msgstr "Modello configurabile" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_template_action +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_template_action +msgid "Configurable Templates" +msgstr "Modelli configurabili" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_variants_action +msgid "Configurable Variants" +msgstr "Varianti configurabili" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__value_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_settings +msgid "Configuration" +msgstr "Configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_image_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_image_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Images" +msgstr "Immagini configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_step_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_step_line_ids +msgid "Configuration Lines" +msgstr "Righe configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_name +msgid "Configuration Name" +msgstr "Nome configurazione" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_domain_action +#: model:ir.ui.menu,name:product_configurator.menu_product_config_domain_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Restrictions" +msgstr "Restrizioni configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_session_id +msgid "Configuration Session" +msgstr "Sessione configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__name +msgid "Configuration Session Number" +msgstr "Numero sessione configurazione" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_session +#: model:ir.ui.menu,name:product_configurator.menu_product_config_session +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Configuration Sessions" +msgstr "Sessioni configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__config_step_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_name +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_tree_view +msgid "Configuration Step" +msgstr "Passo configurazione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step +msgid "Configuration Step ID" +msgstr "ID passo configurazione" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_steps_action +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_config_steps_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Steps" +msgstr "Passi configurazione" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurator" +msgstr "Configuratore" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configure Product" +msgstr "Configurazione prodotto" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_variant_action +msgid "Configured Variants" +msgstr "Varianti configurate" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_coral_red_black +msgid "Coral Red/Black" +msgstr "Corallo rosso/bianco" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_3 +msgid "Coupé Black" +msgstr "Coupé Black" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_9 +msgid "Coupé Black Rims 384" +msgstr "Coupé Black Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_10 +msgid "Coupé Black Rims 387" +msgstr "Coupé Black Rims 387" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_1 +msgid "Coupé Red" +msgstr "Coupé Red" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_5 +msgid "Coupé Red Rims 384" +msgstr "Coupé Red Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_6 +msgid "Coupé Red Rims 387" +msgstr "Coupé Red Rims 387" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_2 +msgid "Coupé Silver" +msgstr "Coupé Silver" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_7 +msgid "Coupé Silver Rims 384" +msgstr "Coupé Silver Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_8 +msgid "Coupé Silver Rims 387" +msgstr "Coupé Silver Rims 387" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__currency_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__currency_id +msgid "Currency" +msgstr "Valuta" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__custom +#: model:product.attribute,name:product_configurator.custom_attribute +#: model:product.attribute.value,name:product_configurator.custom_attribute_value +msgid "Custom" +msgstr "Personalizzato" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__val_custom +msgid "Custom Value" +msgstr "Valore personalizzato" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__custom_value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__custom_value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Custom Values" +msgstr "Valori personalizzati" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_session_custom_value__value +msgid "Custom value held as string" +msgstr "Valore personalizzato gestito come stringa" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__date +msgid "Date" +msgstr "Data" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__datetime +msgid "DateTime" +msgstr "Datetime" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__default_val +msgid "Default Val" +msgstr "Valore predefinito" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Default values for each attribute line must exist in the attribute values " +"(%(attr_name)s: %(default_val)s)" +msgstr "" +"I valori predefiniti per ogni riga attributo devono esistere nei valori " +"attributo (%(attr_name)s: %(default_val)s)" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Default values provided generate an invalid configuration" +msgstr "I valori predefiniti forniti generano una configurazione non valida" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__description +msgid "Description" +msgstr "Descrizione" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__required +msgid "" +"Determines the required value of this attribute though it can be change on " +"the template level" +msgstr "" +"Determina il valore richiesto di questo attributo che può essere modificato " +"a livello modello" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_diesel +msgid "Diesel" +msgstr "Diesel" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain_line +msgid "Domain Line for Config Restrictions" +msgstr "Riga dominio per restrizioni configurazione" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain +msgid "Domain for Config Restrictions" +msgstr "Dominio per restrizioni configurazione" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__done +msgid "Done" +msgstr "Eseguita" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_384 +msgid "Double-spoke 18\"" +msgstr "Double-spoke 18\"" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__draft +msgid "Draft" +msgstr "Bozza" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_engine +#: model:product.config.step,name:product_configurator.config_step_engine +msgid "Engine" +msgstr "Motore" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Error while calculating mako product name: %s" +msgstr "Errore nel calcolo del nome modello Mako: %s" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_extras +msgid "Extras" +msgstr "Aggiuntivi" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__custom_type +msgid "Field Type" +msgstr "Tipo campo" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Finished configuration session must have a product_id linked" +msgstr "" +"Una sessione configurazione completata deve avere un product_id collegato" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__float +msgid "Float" +msgstr "Float" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Following Attribute/Value from restriction are not present in template " +"attributes/values. Please make sure you are adding right restriction" +msgstr "" +"Il seguente attributo/valore della restrizione non è presente negli " +"attributi/valori del modello. Verificare di aggiungere le restrizioni " +"corrette" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_line__attr_line_val_ids +msgid "" +"For normal attributes the values configured for the product can be " +"selected.\n" +"For custom attributes the 'Custom' value can also be selected." +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_fuel +msgid "Fuel" +msgstr "Carburante" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_gasoline +msgid "Gasoline" +msgstr "Benzina" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,help:product_configurator.field_product_template__mako_tmpl_name +msgid "Generate Name based on Mako Template" +msgstr "Nome generato in mase al modello Mako" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__id +msgid "ID" +msgstr "ID" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__image +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__image +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1920 +msgid "Image" +msgstr "Immagine" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1024 +msgid "Image 1024" +msgstr "Immagine 1024" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_128 +msgid "Image 128" +msgstr "Immagine 128" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_256 +msgid "Image 256" +msgstr "Immagine 256" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_512 +msgid "Image 512" +msgstr "Immagine 512" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__implied_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Inherited" +msgstr "Ereditato" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__integer +msgid "Integer" +msgstr "Integer" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_interior +msgid "Interior" +msgstr "Interno" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_preset_ok +msgid "Is Preset" +msgstr "È preimpostato" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__required +msgid "Is this attribute required?" +msgstr "Questo attributo è richiesto?" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_model_line +#: model:product.config.step,name:product_configurator.config_step_lines +msgid "Lines" +msgstr "Righe" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_luxury_line +#: model:product.template,name:product_configurator.product_bmw_luxury_line_product_template +msgid "Luxury Line" +msgstr "Luxury Line" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i +msgid "M235i" +msgstr "M235i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m235i_coupe_product_template +msgid "M235i Coupé" +msgstr "M235i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i_xdrive +msgid "M235i xDrive" +msgstr "M235i xDrive" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m2351_xdrive_coupe_product_template +msgid "M235i xDrive Coupe" +msgstr "M235i xDrive Coupe" + +#. module: product_configurator +#: model:res.groups,name:product_configurator.group_product_configurator_manager +msgid "Manager" +msgstr "Responsabile" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_dummy +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight_dummy +msgid "Manual Weight" +msgstr "Peso manuale" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template__weight_dummy +msgid "Manual setting of product template weight" +msgstr "Impostazione manuale del peso modello prodotto" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__max_val +msgid "Max Value" +msgstr "Valore massimo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__max_val +msgid "Maximum value allowed" +msgstr "Massimo valore consentito" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Maximum value must be greater than Minimum value" +msgstr "Il valore massimo deve essere superiore al valore minimo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__min_val +msgid "Min Value" +msgstr "Valore minimo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__min_val +msgid "Minimum value allowed" +msgstr "Minimo valore consentito" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_advantage +#: model:product.template,name:product_configurator.product_bmw_model_advantage_product_template +msgid "Model Advantage" +msgstr "Model Advantage" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_luxury_line +#: model:product.template,name:product_configurator.product_bmw_model_luxury_line_product_template +msgid "Model Luxury Line" +msgstr "Model Luxury Line" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_m_sport +#: model:product.template,name:product_configurator.product_bmw_model_m_sport_product_template +msgid "Model M Sport" +msgstr "Model M Sport" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_sport_line +#: model:product.template,name:product_configurator.product_bmw_model_sport_line_product_template +msgid "Model Sport Line" +msgstr "Model Sport Line" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__multi +msgid "Multi" +msgstr "Multi" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__name +msgid "Name" +msgstr "Nome" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Next" +msgstr "Successiva" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"On the product %(product)s you cannot associate the value %(value)s with the " +"attribute %(attr)s because they do not match." +msgstr "" +"Nel prodotto %(product)s non si può associare il valore %(value)s per " +"l'attributo %(attr)s perché non corrispondono." + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__operator +msgid "Operators" +msgstr "Operatori" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_options +msgid "Options" +msgstr "Opzioni" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_oyster_black +msgid "Oyster/Black" +msgstr "Oyster/Black" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_color +msgid "Paint Color" +msgstr "Colore vernice" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_preset_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_preset_id +msgid "Preset" +msgstr "Preimpostato" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__price +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__price +msgid "Price" +msgstr "Prezzo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__product_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_id +msgid "Product" +msgstr "Prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute +msgid "Product Attribute" +msgstr "Attributo prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value_line +msgid "Product Attribute Value Line" +msgstr "Riga valore attributo prodotto" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Product Attribute Values" +msgstr "Valori attributo prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_image +msgid "Product Config Image" +msgstr "Immagine configurazione prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_line +msgid "Product Config Restrictions" +msgstr "Restrizioni configurazione prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session +msgid "Product Config Session" +msgstr "Sessione configurazione prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session_custom_value +msgid "Product Config Session Custom Value" +msgstr "Valore predefinito sessione configurazione prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step_line +msgid "Product Config Step Lines" +msgstr "Righe passo configurazione prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step +msgid "Product Config Steps" +msgstr "Passi configurazione prodotto" + +#. module: product_configurator +#: model:ir.module.category,name:product_configurator.product_config_category +#: model_terms:ir.ui.view,arch_db:product_configurator.configurator_settings_view_form +msgid "Product Configurator" +msgstr "Configuratore prodotto" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_img +msgid "Product Img" +msgstr "Immagine prodotto" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__product_tmpl_id +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Riga attributo modello prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_value +msgid "Product Template Attribute Value" +msgstr "Valore attributo modello prodotto" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "Product Template does not have any attribute lines defined" +msgstr "Il modello prodotto non ha nessuna riga attributo definita" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_value_ids +msgid "Product Value" +msgstr "Valore prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_product +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_id +msgid "Product Variant" +msgstr "Variante prodotto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_configurator +msgid "Product configuration Wizard" +msgstr "Procedura guidata configurazione prodotto" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_form_view_custom_vals_inherit +msgid "Reconfigure Product" +msgstr "Riconfigura prodotto" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_red +msgid "Red" +msgstr "Rossa" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__required +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__required +msgid "Required" +msgstr "Richiesto" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Reset" +msgstr "Resetta" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__domain_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__domain_id +msgid "Restrictions" +msgstr "Restrizioni" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Restrictions added make the current default values generate an invalid " +"configuration. \n" +"%s" +msgstr "" +"Le restrizioni aggiunte rendono i valori attuali predefiniti una " +"configurazione non valida. \n" +"%s" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_rims +msgid "Rims" +msgstr "Cerchioni" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__domain_id +msgid "Rule" +msgstr "Regola" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Rules" +msgstr "Regole" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__search_ok +msgid "Searchable" +msgstr "Ricercabile" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom field type '%s' is not searchable" +msgstr "Il tipo campo personalizzato selezionato '%s' non è ricercabile" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be at least %(min_val)s" +msgstr "" +"Il valore personalizzato selezionato '%(name)s' deve essere almeno " +"%(min_val)s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Selected custom value '%(name)s' must be between %(min_val)s and %(max_val)s" +msgstr "" +"Il calore personalizzato selezionato '%(name)s' deve essere tra %(min_val)s " +"e %(max_val)s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be lower than %(max_value)s" +msgstr "" +"Il valore personalizzato selezionato '%(name)s' deve essere inferiore a " +"%(max_value)s" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__cfg_session_id +msgid "Session" +msgstr "Sessione" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_configurator__product_id +msgid "Set only when re-configuring a existing variant" +msgstr "Impostare solo quando si riconfigura una variante esistente" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_domain_line__sequence +msgid "Set the order of operations for evaluation domain lines" +msgstr "" +"Imposta l'ordine delle operazioni per la valutazione delle righe dominio" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_product_configurator_configuration +#: model:ir.ui.menu,name:product_configurator.menu_action_product_configurator_configuration +msgid "Settings" +msgstr "Impostazioni" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_silver +msgid "Silver" +msgstr "Argento" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_paint_silver_product_template +msgid "Silver Paint" +msgstr "Vernice argento" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_smoker_package +#: model:product.template,name:product_configurator.product_2_series_smoker_package_product_template +msgid "Smoker Package" +msgstr "Pacchetto fumatore" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Sorry, you are not allowed to create/change this kind of document. For more " +"information please contact your manager." +msgstr "" +"Spiacenti, non si è autorizzati a creare/modificare questo tipo di " +"documento. Per ulteriori informazioni contattare il responsabile." + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_sport_product_template +msgid "Sport Automatic Transmission Steptronic" +msgstr "Sport Automatic Transmission Steptronic" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sport_line +#: model:product.template,name:product_configurator.product_bmw_sport_line_product_template +msgid "Sport Line" +msgstr "Sport Line" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Standard Products" +msgstr "Prodotti standard" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__state +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__state +msgid "State" +msgstr "Stato" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sunroof +#: model:product.template,name:product_configurator.product_2_series_sunroof_product_template +msgid "Sunroof" +msgstr "Tettuccio apribile" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_tapistry +msgid "Tapistry" +msgstr "Tappezzeria" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__template_attribute_value_ids +msgid "Template Attribute Values" +msgstr "Valori atributo modello" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__template_attribute_ids +msgid "Template Attributes" +msgstr "Attributi modello" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__text +msgid "Textarea" +msgstr "Textarea" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"The attribute %(attr)s must have at least one value for the product " +"%(product)s." +msgstr "" +"L'attributo %(attr)s deve avere almeno un valore per il prodotto %(product)s." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value_line__attribute_id +msgid "" +"The attribute cannot be changed once the value is used on at least one " +"product." +msgstr "" +"L'attributo non può essere modificato una volta che è utilizzato almeno in " +"un prodotto." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__custom_type +msgid "The type of the custom field generated in the frontend" +msgstr "Tipo del campo personalizzato generato nel frontend" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "There was a problem rendering the view (dynamic_form not found)" +msgstr "" +"Si è verificato un problema nella visualizzazione della vista (dynamic_form " +"non trovato)" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"To reset/change the Preset Please close and start the configuration again" +msgstr "" +"Per resettare/modificare i valori preimpostati, chiudere e riaprire la " +"configurazione" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tow_hook +msgid "Tow hook" +msgstr "Gancio di traino" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_towhook_product_template +msgid "Towhook" +msgstr "Gancio di traino" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__trans_implied_ids +msgid "Transitively inherits" +msgstr "Eredita transitivamente" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_transmission +msgid "Transmission" +msgstr "Trasmissione" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__uom_id +msgid "Unit of Measure" +msgstr "Unità di misura" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__user_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__user_id +#: model:res.groups,name:product_configurator.group_product_configurator +msgid "User" +msgstr "Utente" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_378 +msgid "V-spoke 16\"" +msgstr "V-spoke 16\"" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_387 +msgid "V-spoke 18\"" +msgstr "V-spoke 18\"" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__value +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Value" +msgstr "Valore" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__value_ids +msgid "Values" +msgstr "Valori" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_ids +msgid "Values Configuration" +msgstr "Configurazione valori" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Values entered for line '%s' generate a incompatible configuration" +msgstr "" +"I valori inseriti per la riga '%s' generano una configurazione non " +"compatibile" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "" +"Values must belong to the attribute of the corresponding attribute_line set " +"on the configuration line" +msgstr "" +"I valori devono appartenere all'attributo del corrispondente attribute_line " +"nella riga di configurazione" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Values provided to the attribute value line are incompatible with the " +"current rules" +msgstr "" +"I valori forniti alla riga valore attributo sono incompatibili con le regole " +"attuali" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Variant Name" +msgstr "Nome variante" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,field_description:product_configurator.field_product_template__mako_tmpl_name +msgid "Variant name" +msgstr "Nome variante" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_ir_ui_view +msgid "View" +msgstr "Vista" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight +msgid "Weight" +msgstr "Peso" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_extra +msgid "Weight Extra" +msgstr "Peso aggiuntivo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__search_ok +msgid "" +"When checking for variants with the same configuration, do we include this " +"field in the search?" +msgstr "" +"Quando si cercano varianti con la stessa confiugrazione, si include anche " +"questo campo nella ricerca?" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "You cannot have a duplicate configuration for the same value" +msgstr "Non si può avere una configurazione duplicata per lo stesso valore" + +#, python-format +#~ msgid "" +#~ "Attribute custom type is binary, attachments are the only accepted values " +#~ "with this custom field type" +#~ msgstr "" +#~ "Il tipo attributo personalizzato è binario, sono consentiti solo allegati " +#~ "con questo tipo di campo personalizzato" + +#, python-format +#~ msgid "" +#~ "Attribute custom type must be 'binary' for saving attachments to custom " +#~ "value" +#~ msgstr "" +#~ "Il tipo attributo personalizzato deve essere 'binary' per salvare " +#~ "allegati nel valore personalizzato" + +#, python-format +#~ msgid "" +#~ "Cannot conduct search on an empty config session without product_tmpl_id " +#~ "kwarg" +#~ msgstr "" +#~ "Non si può eseguire una ricerca in una sessione con configurazione vuota " +#~ "senza kwarg product_tmpl_id" + +#, python-format +#~ msgid "Configuration cannot have the same value inserted twice" +#~ msgstr "La configurazione non può avere lo stesso valore inserito due volte" + +#, python-format +#~ msgid "Invalid Configuration" +#~ msgstr "Configurazione non valida" + +#, python-format +#~ msgid "New" +#~ msgstr "Nuova" + +#, python-format +#~ msgid "Product created via configuration wizard" +#~ msgstr "Prodotto creato attraverso la procedura guidata configurazione" + +#, python-format +#~ msgid "Required attribute '%s' is empty" +#~ msgstr "L'attributo richiesto '%s' è vuoto" + +#, python-format +#~ msgid "" +#~ "You must select at least one attribute in order to configure a product" +#~ msgstr "Bisogna selezionare almeno un attributo per confiugrare un prodotto" + +#~ msgid "Attribute Line Values" +#~ msgstr "Valori riga attributo" diff --git a/product_configurator/i18n/product_configurator.pot b/product_configurator/i18n/product_configurator.pot new file mode 100644 index 000000000..9d19e07c6 --- /dev/null +++ b/product_configurator/i18n/product_configurator.pot @@ -0,0 +1,1450 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_configurator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Attribute/s: %s" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Restriction: %s" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Value/s: %s\n" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.bmw_2_series +msgid "2 Series" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218d +msgid "218d" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218d_coupe_product_template +msgid "218d Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218i +msgid "218i" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218i_coupe_product_template +msgid "218i Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d +msgid "220d" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_coupe_product_template +msgid "220d Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d_xdrive +msgid "220d xDrive" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_xdrive_coupe_product_template +msgid "220d xDrive Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220i +msgid "220i" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220i_coupe_product_template +msgid "220i Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_225d +msgid "225d" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_225d_coupe_product_template +msgid "225d Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_228i +msgid "228i" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_228i_coupe_product_template +msgid "228i Coupé" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_kanban_view_inherited +msgid "" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__active +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__active +msgid "Active" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"All attribute values used in attribute value lines must be defined in the " +"attribute lines of the template" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__val_custom +msgid "Allow custom value for this attribute?" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__custom +msgid "Allow custom values for this attribute?" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__multi +msgid "Allow selection of multiple values for this attribute?" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attr_line_val_ids +msgid "Allowed Attribute Values" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "An error occurred while parsing value for attribute %s" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"Are you sure? This will remove your current configuration for this template!" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_armrest +#: model:product.template,name:product_configurator.product_2_series_armrest_product_template +msgid "Armrest" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__binary +msgid "Attachment" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attachment_ids +msgid "Attachments" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attribute_id +msgid "Attribute" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_line_ids +msgid "Attribute Dependencies" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attribute_line_id +msgid "Attribute Line" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_line_val_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_line_val_ids +msgid "Attribute Line Val" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__attribute_line_ids +msgid "Attribute Lines" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_id +msgid "Attribute Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_value_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_value_line_ids +msgid "Attribute Value Lines" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.variants_action +#: model:ir.ui.menu,name:product_configurator.menu_variants_action_configuration +msgid "Attribute Values" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_value__weight_extra +msgid "Attribute Weight Extra" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__image +msgid "Attribute value image (Display on website for radio buttons)" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_attributes_view +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__attribute_line_ids +#: model:ir.ui.menu,name:product_configurator.menu_attribute_action_configuration +msgid "Attributes" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic +msgid "Automatic (Steptronic)" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic_sport +msgid "Automatic Sport (Steptronic)" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_product_template +msgid "Automatic Transmission Steptronic" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Back" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_black +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_black +msgid "Black" +msgstr "" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_body +msgid "Body" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__active +msgid "" +"By unchecking the active field you can disable a attribute value without " +"deleting it" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__active +msgid "" +"By unchecking the active field you can disable a attribute without deleting " +"it" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_ok +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_ok +msgid "Can be Configured" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Cannot have a configuration step defined twice." +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "" +"Changing the product template while having an active configuration will " +"erase reset/clear all values" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__char +msgid "Char" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__color +msgid "Color" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__condition +msgid "Condition" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurable" +msgstr "" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_product_main +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Configurable Products" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Configurable Products cannot have duplicates (identical attribute values)" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_tmpl_id +msgid "Configurable Template" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_template_action +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_template_action +msgid "Configurable Templates" +msgstr "" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_variants_action +msgid "Configurable Variants" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__value_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_settings +msgid "Configuration" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_image_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_image_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Images" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_step_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_step_line_ids +msgid "Configuration Lines" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_name +msgid "Configuration Name" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_domain_action +#: model:ir.ui.menu,name:product_configurator.menu_product_config_domain_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Restrictions" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_session_id +msgid "Configuration Session" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__name +msgid "Configuration Session Number" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_session +#: model:ir.ui.menu,name:product_configurator.menu_product_config_session +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Configuration Sessions" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__config_step_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_name +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_tree_view +msgid "Configuration Step" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step +msgid "Configuration Step ID" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_steps_action +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_config_steps_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Steps" +msgstr "" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurator" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configure Product" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_variant_action +msgid "Configured Variants" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_coral_red_black +msgid "Coral Red/Black" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_3 +msgid "Coupé Black" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_9 +msgid "Coupé Black Rims 384" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_10 +msgid "Coupé Black Rims 387" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_1 +msgid "Coupé Red" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_5 +msgid "Coupé Red Rims 384" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_6 +msgid "Coupé Red Rims 387" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_2 +msgid "Coupé Silver" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_7 +msgid "Coupé Silver Rims 384" +msgstr "" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_8 +msgid "Coupé Silver Rims 387" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_uid +msgid "Created by" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_date +msgid "Created on" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__currency_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__currency_id +msgid "Currency" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__custom +#: model:product.attribute,name:product_configurator.custom_attribute +#: model:product.attribute.value,name:product_configurator.custom_attribute_value +msgid "Custom" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__val_custom +msgid "Custom Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__custom_value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__custom_value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Custom Values" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_session_custom_value__value +msgid "Custom value held as string" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__date +msgid "Date" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__datetime +msgid "DateTime" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__default_val +msgid "Default Val" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Default values for each attribute line must exist in the attribute values " +"(%(attr_name)s: %(default_val)s)" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Default values provided generate an invalid configuration" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__description +msgid "Description" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__required +msgid "" +"Determines the required value of this attribute though it can be change on " +"the template level" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_diesel +msgid "Diesel" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__display_name +msgid "Display Name" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain_line +msgid "Domain Line for Config Restrictions" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain +msgid "Domain for Config Restrictions" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__done +msgid "Done" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_384 +msgid "Double-spoke 18\"" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__draft +msgid "Draft" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_engine +#: model:product.config.step,name:product_configurator.config_step_engine +msgid "Engine" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Error while calculating mako product name: %s" +msgstr "" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_extras +msgid "Extras" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__custom_type +msgid "Field Type" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Finished configuration session must have a product_id linked" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__float +msgid "Float" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Following Attribute/Value from restriction are not present in template " +"attributes/values. Please make sure you are adding right restriction" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_line__attr_line_val_ids +msgid "" +"For normal attributes the values configured for the product can be selected.\n" +"For custom attributes the 'Custom' value can also be selected." +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_fuel +msgid "Fuel" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_gasoline +msgid "Gasoline" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,help:product_configurator.field_product_template__mako_tmpl_name +msgid "Generate Name based on Mako Template" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__id +msgid "ID" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__image +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__image +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1920 +msgid "Image" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1024 +msgid "Image 1024" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_128 +msgid "Image 128" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_256 +msgid "Image 256" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_512 +msgid "Image 512" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__implied_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Inherited" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__integer +msgid "Integer" +msgstr "" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_interior +msgid "Interior" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_preset_ok +msgid "Is Preset" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__required +msgid "Is this attribute required?" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator____last_update +msgid "Last Modified on" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_date +msgid "Last Updated on" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_model_line +#: model:product.config.step,name:product_configurator.config_step_lines +msgid "Lines" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_luxury_line +#: model:product.template,name:product_configurator.product_bmw_luxury_line_product_template +msgid "Luxury Line" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i +msgid "M235i" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m235i_coupe_product_template +msgid "M235i Coupé" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i_xdrive +msgid "M235i xDrive" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m2351_xdrive_coupe_product_template +msgid "M235i xDrive Coupe" +msgstr "" + +#. module: product_configurator +#: model:res.groups,name:product_configurator.group_product_configurator_manager +msgid "Manager" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_dummy +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight_dummy +msgid "Manual Weight" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template__weight_dummy +msgid "Manual setting of product template weight" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__max_val +msgid "Max Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__max_val +msgid "Maximum value allowed" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Maximum value must be greater than Minimum value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__min_val +msgid "Min Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__min_val +msgid "Minimum value allowed" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_advantage +#: model:product.template,name:product_configurator.product_bmw_model_advantage_product_template +msgid "Model Advantage" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_luxury_line +#: model:product.template,name:product_configurator.product_bmw_model_luxury_line_product_template +msgid "Model Luxury Line" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_m_sport +#: model:product.template,name:product_configurator.product_bmw_model_m_sport_product_template +msgid "Model M Sport" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_sport_line +#: model:product.template,name:product_configurator.product_bmw_model_sport_line_product_template +msgid "Model Sport Line" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__multi +msgid "Multi" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__name +msgid "Name" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Next" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"On the product %(product)s you cannot associate the value %(value)s with the" +" attribute %(attr)s because they do not match." +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__operator +msgid "Operators" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_options +msgid "Options" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_oyster_black +msgid "Oyster/Black" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_color +msgid "Paint Color" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_preset_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_preset_id +msgid "Preset" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__price +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__price +msgid "Price" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__product_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_id +msgid "Product" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute +msgid "Product Attribute" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value_line +msgid "Product Attribute Value Line" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Product Attribute Values" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_image +msgid "Product Config Image" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_line +msgid "Product Config Restrictions" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session +msgid "Product Config Session" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session_custom_value +msgid "Product Config Session Custom Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step_line +msgid "Product Config Step Lines" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step +msgid "Product Config Steps" +msgstr "" + +#. module: product_configurator +#: model:ir.module.category,name:product_configurator.product_config_category +#: model_terms:ir.ui.view,arch_db:product_configurator.configurator_settings_view_form +msgid "Product Configurator" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_img +msgid "Product Img" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__product_tmpl_id +msgid "Product Template" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_value +msgid "Product Template Attribute Value" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "Product Template does not have any attribute lines defined" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_value_ids +msgid "Product Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_product +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_id +msgid "Product Variant" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_configurator +msgid "Product configuration Wizard" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_form_view_custom_vals_inherit +msgid "Reconfigure Product" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_red +msgid "Red" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__required +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__required +msgid "Required" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Reset" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__domain_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__domain_id +msgid "Restrictions" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Restrictions added make the current default values generate an invalid configuration. \n" +"%s" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_rims +msgid "Rims" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__domain_id +msgid "Rule" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Rules" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__search_ok +msgid "Searchable" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom field type '%s' is not searchable" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be at least %(min_val)s" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Selected custom value '%(name)s' must be between %(min_val)s and %(max_val)s" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be lower than %(max_value)s" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__sequence +msgid "Sequence" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__cfg_session_id +msgid "Session" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_configurator__product_id +msgid "Set only when re-configuring a existing variant" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_domain_line__sequence +msgid "Set the order of operations for evaluation domain lines" +msgstr "" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_product_configurator_configuration +#: model:ir.ui.menu,name:product_configurator.menu_action_product_configurator_configuration +msgid "Settings" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_silver +msgid "Silver" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_paint_silver_product_template +msgid "Silver Paint" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_smoker_package +#: model:product.template,name:product_configurator.product_2_series_smoker_package_product_template +msgid "Smoker Package" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Sorry, you are not allowed to create/change this kind of document. For more " +"information please contact your manager." +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_sport_product_template +msgid "Sport Automatic Transmission Steptronic" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sport_line +#: model:product.template,name:product_configurator.product_bmw_sport_line_product_template +msgid "Sport Line" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Standard Products" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__state +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__state +msgid "State" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sunroof +#: model:product.template,name:product_configurator.product_2_series_sunroof_product_template +msgid "Sunroof" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_tapistry +msgid "Tapistry" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__template_attribute_value_ids +msgid "Template Attribute Values" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__template_attribute_ids +msgid "Template Attributes" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__text +msgid "Textarea" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"The attribute %(attr)s must have at least one value for the product " +"%(product)s." +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value_line__attribute_id +msgid "" +"The attribute cannot be changed once the value is used on at least one " +"product." +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__custom_type +msgid "The type of the custom field generated in the frontend" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "There was a problem rendering the view (dynamic_form not found)" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"To reset/change the Preset Please close and start the configuration again" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tow_hook +msgid "Tow hook" +msgstr "" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_towhook_product_template +msgid "Towhook" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__trans_implied_ids +msgid "Transitively inherits" +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_transmission +msgid "Transmission" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__uom_id +msgid "Unit of Measure" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__user_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__user_id +#: model:res.groups,name:product_configurator.group_product_configurator +msgid "User" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_378 +msgid "V-spoke 16\"" +msgstr "" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_387 +msgid "V-spoke 18\"" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__value +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Value" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__value_ids +msgid "Values" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_ids +msgid "Values Configuration" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Values entered for line '%s' generate a incompatible configuration" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "" +"Values must belong to the attribute of the corresponding attribute_line set " +"on the configuration line" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Values provided to the attribute value line are incompatible with the " +"current rules" +msgstr "" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Variant Name" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,field_description:product_configurator.field_product_template__mako_tmpl_name +msgid "Variant name" +msgstr "" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight +msgid "Weight" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_extra +msgid "Weight Extra" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__search_ok +msgid "" +"When checking for variants with the same configuration, do we include this " +"field in the search?" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "You cannot have a duplicate configuration for the same value" +msgstr "" diff --git a/product_configurator/i18n/pt.po b/product_configurator/i18n/pt.po new file mode 100644 index 000000000..c4572b8f9 --- /dev/null +++ b/product_configurator/i18n/pt.po @@ -0,0 +1,1552 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_configurator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-09-15 18:02+0000\n" +"Last-Translator: Peter Romão \n" +"Language-Team: none\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Attribute/s: %s" +msgstr "" +"\n" +"Atributo(s): %s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Restriction: %s" +msgstr "" +"\n" +"Restrição: %s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"\n" +"Value/s: %s\n" +msgstr "" +"\n" +"Valor(es): %s\n" + +#. module: product_configurator +#: model:product.template,name:product_configurator.bmw_2_series +msgid "2 Series" +msgstr "2 Series" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218d +msgid "218d" +msgstr "218d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218d_coupe_product_template +msgid "218d Coupé" +msgstr "218d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_218i +msgid "218i" +msgstr "218i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_218i_coupe_product_template +msgid "218i Coupé" +msgstr "218i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d +msgid "220d" +msgstr "220d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_coupe_product_template +msgid "220d Coupé" +msgstr "220d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d_xdrive +msgid "220d xDrive" +msgstr "220d xDrive" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220d_xdrive_coupe_product_template +msgid "220d xDrive Coupé" +msgstr "220d xDrive Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_220i +msgid "220i" +msgstr "220i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_220i_coupe_product_template +msgid "220i Coupé" +msgstr "220i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_225d +msgid "225d" +msgstr "225d" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_225d_coupe_product_template +msgid "225d Coupé" +msgstr "225d Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_228i +msgid "228i" +msgstr "228i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_228i_coupe_product_template +msgid "228i Coupé" +msgstr "228i Coupé" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_kanban_view_inherited +msgid "" +msgstr "" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__active +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__active +msgid "Active" +msgstr "Activo" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"All attribute values used in attribute value lines must be defined in the " +"attribute lines of the template" +msgstr "" +"Todos os valores de atributo usados nas linhas de valor de atributo devem " +"ser definidos nas linhas de atributo do modelo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__val_custom +msgid "Allow custom value for this attribute?" +msgstr "Permitir valor personalizado para este atributo?" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__custom +msgid "Allow custom values for this attribute?" +msgstr "Permitir valores personalizados para este atributo?" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__multi +msgid "Allow selection of multiple values for this attribute?" +msgstr "Permitir a seleção de vários valores para este atributo?" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attr_line_val_ids +msgid "Allowed Attribute Values" +msgstr "" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "An error occurred while parsing value for attribute %s" +msgstr "Ocorreu um erro ao analisar o valor do atributo %s" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"Are you sure? This will remove your current configuration for this template!" +msgstr "" +"Tem a certeza? Isto irá remover a sua configuração atual para este modelo!" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_armrest +#: model:product.template,name:product_configurator.product_2_series_armrest_product_template +msgid "Armrest" +msgstr "Descanso de Braço" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__binary +msgid "Attachment" +msgstr "Anexo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attachment_ids +msgid "Attachments" +msgstr "Anexos" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__attribute_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attribute_id +msgid "Attribute" +msgstr "Atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_line_ids +msgid "Attribute Dependencies" +msgstr "Dependências de Atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attribute_line_id +msgid "Attribute Line" +msgstr "Linha de Atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_line_val_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_line_val_ids +msgid "Attribute Line Val" +msgstr "Valor da Linha de Atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__attribute_line_ids +msgid "Attribute Lines" +msgstr "Linhas de Atributo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_id +msgid "Attribute Value" +msgstr "Valor do atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_value_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_value_line_ids +msgid "Attribute Value Lines" +msgstr "Linhas de Valor de Atributo" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.variants_action +#: model:ir.ui.menu,name:product_configurator.menu_variants_action_configuration +msgid "Attribute Values" +msgstr "Valores de Atributo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_value__weight_extra +msgid "Attribute Weight Extra" +msgstr "Atributo Peso Extra" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__image +msgid "Attribute value image (Display on website for radio buttons)" +msgstr "Imagem do valor do atributo (Exibir no website para botões de opção)" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_attributes_view +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__attribute_line_ids +#: model:ir.ui.menu,name:product_configurator.menu_attribute_action_configuration +msgid "Attributes" +msgstr "Atributos" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic +msgid "Automatic (Steptronic)" +msgstr "Automatic (Steptronic)" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic_sport +msgid "Automatic Sport (Steptronic)" +msgstr "Automatic Sport (Steptronic)" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_product_template +msgid "Automatic Transmission Steptronic" +msgstr "Transmissão Automática Steptronic" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Back" +msgstr "Anterior" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_black +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_black +msgid "Black" +msgstr "Preto" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_body +msgid "Body" +msgstr "Corpo" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__active +msgid "" +"By unchecking the active field you can disable a attribute value without " +"deleting it" +msgstr "" +"Ao desmarcar o campo ativo, pode desativar um valor de atributo sem o " +"eliminar" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__active +msgid "" +"By unchecking the active field you can disable a attribute without deleting " +"it" +msgstr "Ao desmarcar o campo ativo, pode desativar um atributo sem o eliminar" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_ok +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_ok +msgid "Can be Configured" +msgstr "Pode ser Configurado" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Cannot have a configuration step defined twice." +msgstr "Não pode ter uma etapa de configuração definida duas vezes." + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "" +"Changing the product template while having an active configuration will " +"erase reset/clear all values" +msgstr "" +"Alterar o modelo do artigo enquanto tiver uma configuração ativa apagará " +"redefinirá/limpará todos os valores" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__char +msgid "Char" +msgstr "Char" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__color +msgid "Color" +msgstr "Cor" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__condition +msgid "Condition" +msgstr "Condição" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurable" +msgstr "Configurável" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_product_main +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Configurable Products" +msgstr "Artigos Configuráveis" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Configurable Products cannot have duplicates (identical attribute values)" +msgstr "" +"Artigos configuráveis não podem ter duplicados (valores de atributo " +"idênticos)" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_tmpl_id +msgid "Configurable Template" +msgstr "Modelo Configurável" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_template_action +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_template_action +msgid "Configurable Templates" +msgstr "Modelos Configuráveis" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_variants_action +msgid "Configurable Variants" +msgstr "Variantes Configuráveis" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__value_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_settings +msgid "Configuration" +msgstr "Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_image_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_image_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Images" +msgstr "Imagens de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_step_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_step_line_ids +msgid "Configuration Lines" +msgstr "Linhas de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_name +msgid "Configuration Name" +msgstr "Nome da Configuração" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_domain_action +#: model:ir.ui.menu,name:product_configurator.menu_product_config_domain_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Restrictions" +msgstr "Restrições de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_session_id +msgid "Configuration Session" +msgstr "Sessão de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__name +msgid "Configuration Session Number" +msgstr "Número da Sessão de Configuração" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_session +#: model:ir.ui.menu,name:product_configurator.menu_product_config_session +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Configuration Sessions" +msgstr "Sessões de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__config_step_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_name +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_tree_view +msgid "Configuration Step" +msgstr "Etapa de Configuração" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step +msgid "Configuration Step ID" +msgstr "ID da Etapa de Configuração" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_config_steps_action +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_ids +#: model:ir.ui.menu,name:product_configurator.menu_product_config_steps_action +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configuration Steps" +msgstr "Etapas de Configuração" + +#. module: product_configurator +#: model:ir.ui.menu,name:product_configurator.menu_product_configurable +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configurator" +msgstr "Configurador" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Configure Product" +msgstr "Configurar Artigo" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.product_configurable_variant_action +msgid "Configured Variants" +msgstr "Variantes Configuradas" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_coral_red_black +msgid "Coral Red/Black" +msgstr "Vermelho Coral/Preto" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_3 +msgid "Coupé Black" +msgstr "Coupé Black" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_9 +msgid "Coupé Black Rims 384" +msgstr "Coupé Black Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_10 +msgid "Coupé Black Rims 387" +msgstr "Coupé Black Rims 387" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_1 +msgid "Coupé Red" +msgstr "Coupé Red" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_5 +msgid "Coupé Red Rims 384" +msgstr "Coupé Red Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_6 +msgid "Coupé Red Rims 387" +msgstr "Coupé Red Rims 387" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_2 +msgid "Coupé Silver" +msgstr "Coupé Silver" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_7 +msgid "Coupé Silver Rims 384" +msgstr "Coupé Silver Rims 384" + +#. module: product_configurator +#: model:product.config.image,name:product_configurator.config_image_8 +msgid "Coupé Silver Rims 387" +msgstr "Coupé Silver Rims 387" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_uid +msgid "Created by" +msgstr "Criado por" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_date +msgid "Created on" +msgstr "Criado em" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__currency_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__currency_id +msgid "Currency" +msgstr "Moeda" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__custom +#: model:product.attribute,name:product_configurator.custom_attribute +#: model:product.attribute.value,name:product_configurator.custom_attribute_value +msgid "Custom" +msgstr "Personalizado" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__val_custom +msgid "Custom Value" +msgstr "Valor Personalizado" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__custom_value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__custom_value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view +msgid "Custom Values" +msgstr "Valores Personalizados" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_session_custom_value__value +msgid "Custom value held as string" +msgstr "Valor personalizado mantido como string" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__date +msgid "Date" +msgstr "Data" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__datetime +msgid "DateTime" +msgstr "Data/Hora" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__default_val +msgid "Default Val" +msgstr "Val. Predef." + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Default values for each attribute line must exist in the attribute values " +"(%(attr_name)s: %(default_val)s)" +msgstr "" +"Os valores predefinidos para cada linha de atributo devem existir nos " +"valores de atributo (%(attr_name)s: %(default_val)s)" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Default values provided generate an invalid configuration" +msgstr "Os valores predefinidos fornecidos geram uma configuração inválida" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__description +msgid "Description" +msgstr "Descrição" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__required +msgid "" +"Determines the required value of this attribute though it can be change on " +"the template level" +msgstr "" +"Determina o valor necessário deste atributo, embora possa ser alterado no " +"nível do modelo" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_diesel +msgid "Diesel" +msgstr "Diesel" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__display_name +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__display_name +msgid "Display Name" +msgstr "Nome Apresentado" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain_line +msgid "Domain Line for Config Restrictions" +msgstr "Linha de Domínio para Restrições de Configuração" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_domain +msgid "Domain for Config Restrictions" +msgstr "Domínio para Restrições de Configuração" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__done +msgid "Done" +msgstr "Concluído" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_384 +msgid "Double-spoke 18\"" +msgstr "Jante Spoke Duplo 18\"" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__draft +msgid "Draft" +msgstr "Rascunho" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_engine +#: model:product.config.step,name:product_configurator.config_step_engine +msgid "Engine" +msgstr "Motor" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "Error while calculating mako product name: %s" +msgstr "Erro ao calcular o nome do artigo mako: %s" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_extras +msgid "Extras" +msgstr "Extras" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__custom_type +msgid "Field Type" +msgstr "Tipo de Campo" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Finished configuration session must have a product_id linked" +msgstr "A sessão de configuração concluída deve ter um product_id vinculado" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__float +msgid "Float" +msgstr "N.º Vírgula Flutuante" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Following Attribute/Value from restriction are not present in template " +"attributes/values. Please make sure you are adding right restriction" +msgstr "" +"Atributo/valor a seguir da restrição não está presentes nos atributos/" +"valores do modelo. Certifique-se que está a adicionar a restrição correta" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_line__attr_line_val_ids +msgid "" +"For normal attributes the values configured for the product can be " +"selected.\n" +"For custom attributes the 'Custom' value can also be selected." +msgstr "" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_fuel +msgid "Fuel" +msgstr "Combustível" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_gasoline +msgid "Gasoline" +msgstr "Gasolina" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,help:product_configurator.field_product_template__mako_tmpl_name +msgid "Generate Name based on Mako Template" +msgstr "Gerar Nome com base no Modelo Mako" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__id +msgid "ID" +msgstr "ID" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__image +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__image +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1920 +msgid "Image" +msgstr "Imagem" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1024 +msgid "Image 1024" +msgstr "Imagem 1024" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_128 +msgid "Image 128" +msgstr "Imagem 128" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_256 +msgid "Image 256" +msgstr "Imagem 256" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_512 +msgid "Image 512" +msgstr "Imagem 512" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__implied_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Inherited" +msgstr "Herdado" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__integer +msgid "Integer" +msgstr "Inteiro" + +#. module: product_configurator +#: model:product.config.step,name:product_configurator.config_step_interior +msgid "Interior" +msgstr "Interior" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_preset_ok +msgid "Is Preset" +msgstr "É Predefinido" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__required +msgid "Is this attribute required?" +msgstr "Este atributo é obrigatório?" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line____last_update +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator____last_update +msgid "Last Modified on" +msgstr "Última Modificação em" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_uid +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_uid +msgid "Last Updated by" +msgstr "Última Atualização por" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_date +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_date +msgid "Last Updated on" +msgstr "Última Atualização em" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_model_line +#: model:product.config.step,name:product_configurator.config_step_lines +msgid "Lines" +msgstr "Linhas" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_luxury_line +#: model:product.template,name:product_configurator.product_bmw_luxury_line_product_template +msgid "Luxury Line" +msgstr "Linha Luxury" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i +msgid "M235i" +msgstr "M235i" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m235i_coupe_product_template +msgid "M235i Coupé" +msgstr "M235i Coupé" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i_xdrive +msgid "M235i xDrive" +msgstr "M235i xDrive" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_engine_m2351_xdrive_coupe_product_template +msgid "M235i xDrive Coupe" +msgstr "M235i xDrive Coupe" + +#. module: product_configurator +#: model:res.groups,name:product_configurator.group_product_configurator_manager +msgid "Manager" +msgstr "Chefe" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_dummy +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight_dummy +msgid "Manual Weight" +msgstr "Peso Manual" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_template__weight_dummy +msgid "Manual setting of product template weight" +msgstr "Configuração manual do peso do modelo de artigo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__max_val +msgid "Max Value" +msgstr "Valor Máx." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__max_val +msgid "Maximum value allowed" +msgstr "Valor máximo permitido" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Maximum value must be greater than Minimum value" +msgstr "O valor máximo deve ser maior que o valor mínimo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__min_val +msgid "Min Value" +msgstr "Valor Min." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__min_val +msgid "Minimum value allowed" +msgstr "Valor mínimo permitido" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_advantage +#: model:product.template,name:product_configurator.product_bmw_model_advantage_product_template +msgid "Model Advantage" +msgstr "Modelo Advantage" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_luxury_line +#: model:product.template,name:product_configurator.product_bmw_model_luxury_line_product_template +msgid "Model Luxury Line" +msgstr "Modelo Linha Luxo" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_m_sport +#: model:product.template,name:product_configurator.product_bmw_model_m_sport_product_template +msgid "Model M Sport" +msgstr "Modelo M Sport" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_sport_line +#: model:product.template,name:product_configurator.product_bmw_model_sport_line_product_template +msgid "Model Sport Line" +msgstr "Modelo Sport Line" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__multi +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__multi +msgid "Multi" +msgstr "Múltiplos" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__name +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__name +msgid "Name" +msgstr "Nome" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Next" +msgstr "Seguinte" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"On the product %(product)s you cannot associate the value %(value)s with the " +"attribute %(attr)s because they do not match." +msgstr "" +"No artigo %(product)s, não pode associar o valor %(value)s ao atributo " +"%(attr)s porque eles não correspondem." + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__operator +msgid "Operators" +msgstr "Operadores" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_options +msgid "Options" +msgstr "Opções" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_oyster_black +msgid "Oyster/Black" +msgstr "Pérola/Preto" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_color +msgid "Paint Color" +msgstr "Cor da Pintura" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_preset_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_preset_id +msgid "Preset" +msgstr "Predefinição" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__price +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__price +msgid "Price" +msgstr "Preço" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__product_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_id +msgid "Product" +msgstr "Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute +msgid "Product Attribute" +msgstr "Atributo do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_attribute_value_line +msgid "Product Attribute Value Line" +msgstr "Linha de Valor do Atributo do Artigo" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Product Attribute Values" +msgstr "Atributo e Valores do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_image +msgid "Product Config Image" +msgstr "Imagem da Configuração do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_line +msgid "Product Config Restrictions" +msgstr "Restrições de Configuração do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session +msgid "Product Config Session" +msgstr "Sessão de Config. do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_session_custom_value +msgid "Product Config Session Custom Value" +msgstr "Valor Personalizado da Sessão de Configuração do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step_line +msgid "Product Config Step Lines" +msgstr "Linhas da Etapa de Configuração do Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_config_step +msgid "Product Config Steps" +msgstr "Etapas de Configuração do Artigo" + +#. module: product_configurator +#: model:ir.module.category,name:product_configurator.product_config_category +#: model_terms:ir.ui.view,arch_db:product_configurator.configurator_settings_view_form +msgid "Product Configurator" +msgstr "Configurador de Artigos" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_img +msgid "Product Img" +msgstr "Imagem do Produto" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__product_tmpl_id +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__product_tmpl_id +msgid "Product Template" +msgstr "Modelo de Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Linha de Atributo do Modelo de Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_template_attribute_value +msgid "Product Template Attribute Value" +msgstr "Valor do Atributo do Modelo de Artigo" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "Product Template does not have any attribute lines defined" +msgstr "O Modelo de Artigo não tem nenhuma linha de atributo definida" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_value_ids +msgid "Product Value" +msgstr "Valor do produto" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_product +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_id +msgid "Product Variant" +msgstr "Variante de Artigo" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_product_configurator +msgid "Product configuration Wizard" +msgstr "Assistente de configuração de artigo" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_form_view_custom_vals_inherit +msgid "Reconfigure Product" +msgstr "Reconfigurar Artigo" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_red +msgid "Red" +msgstr "Vermelho" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__required +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__required +msgid "Required" +msgstr "Obrigatório" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "Reset" +msgstr "Reiniciar" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__domain_line_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__domain_id +msgid "Restrictions" +msgstr "Restrições" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Restrictions added make the current default values generate an invalid " +"configuration. \n" +"%s" +msgstr "" +"As restrições adicionadas fazem com que os valores padrão atuais gerem uma " +"configuração inválida. \n" +"%s" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_rims +msgid "Rims" +msgstr "Jantes" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__domain_id +msgid "Rule" +msgstr "Regra" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view +#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template +msgid "Rules" +msgstr "Regras" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__search_ok +msgid "Searchable" +msgstr "Pesquisável" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom field type '%s' is not searchable" +msgstr "O tipo de campo personalizado selecionado '%s' não é pesquisável" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be at least %(min_val)s" +msgstr "" +"O valor personalizado selecionado '%(name)s' deve ser pelo menos %(min_val)s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Selected custom value '%(name)s' must be between %(min_val)s and %(max_val)s" +msgstr "" +"O valor personalizado selecionado '%(name)s' deve estar entre %(min_val)s e " +"%(max_val)s" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "Selected custom value '%(name)s' must be lower than %(max_value)s" +msgstr "" +"O valor personalizado selecionado '%(name)s' deve ser menor que %(max_value)s" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__sequence +#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__sequence +msgid "Sequence" +msgstr "Sequência" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__cfg_session_id +msgid "Session" +msgstr "Sessão" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_configurator__product_id +msgid "Set only when re-configuring a existing variant" +msgstr "Definir apenas ao reconfigurar uma variante existente" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_config_domain_line__sequence +msgid "Set the order of operations for evaluation domain lines" +msgstr "Definir a ordem das operações para as linhas de domínios de avaliação" + +#. module: product_configurator +#: model:ir.actions.act_window,name:product_configurator.action_product_configurator_configuration +#: model:ir.ui.menu,name:product_configurator.menu_action_product_configurator_configuration +msgid "Settings" +msgstr "Configurações" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_silver +msgid "Silver" +msgstr "Prata" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_paint_silver_product_template +msgid "Silver Paint" +msgstr "Pintura Metalizada" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_smoker_package +#: model:product.template,name:product_configurator.product_2_series_smoker_package_product_template +msgid "Smoker Package" +msgstr "Pacote Fumadores" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "" +"Sorry, you are not allowed to create/change this kind of document. For more " +"information please contact your manager." +msgstr "" +"Desculpe, não tem permissão para criar/alterar este tipo de documento. Para " +"obter mais informações, entre em contato com seu gestor." + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_sport_product_template +msgid "Sport Automatic Transmission Steptronic" +msgstr "Transmissão Automática Sport Steptronic" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sport_line +#: model:product.template,name:product_configurator.product_bmw_sport_line_product_template +msgid "Sport Line" +msgstr "Linha Sport" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view +msgid "Standard Products" +msgstr "Artigos Padrão" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__state +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__state +msgid "State" +msgstr "Estado" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_sunroof +#: model:product.template,name:product_configurator.product_2_series_sunroof_product_template +msgid "Sunroof" +msgstr "Teto Panorâmico" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_tapistry +msgid "Tapistry" +msgstr "Estofos" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__template_attribute_value_ids +msgid "Template Attribute Values" +msgstr "Valores de Atributo de Modelo" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__template_attribute_ids +msgid "Template Attributes" +msgstr "Atributos do modelo" + +#. module: product_configurator +#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__text +msgid "Textarea" +msgstr "Área de Texto" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"The attribute %(attr)s must have at least one value for the product " +"%(product)s." +msgstr "" +"O atributo %(attr)s deve ter pelo menos um valor para o artigo %(product)s." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute_value_line__attribute_id +msgid "" +"The attribute cannot be changed once the value is used on at least one " +"product." +msgstr "" +"O atributo não pode ser alterado depois do valor ter sido utilizado em pelo " +"menos um artigo." + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__custom_type +msgid "The type of the custom field generated in the frontend" +msgstr "O tipo do campo personalizado gerado no front-end" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/wizard/product_configurator.py:0 +#, python-format +msgid "There was a problem rendering the view (dynamic_form not found)" +msgstr "" +"Ocorreu um problema ao renderizar a visualização (dynamic_form não " +"encontrado)" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form +msgid "" +"To reset/change the Preset Please close and start the configuration again" +msgstr "" +"Para redefinir/alterar a Predefinição Por Favor Feche e inicie a " +"configuração novamente" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_tow_hook +msgid "Tow hook" +msgstr "Bola de Atrelado" + +#. module: product_configurator +#: model:product.template,name:product_configurator.product_2_series_towhook_product_template +msgid "Towhook" +msgstr "Bola de atrelado" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__trans_implied_ids +msgid "Transitively inherits" +msgstr "Herda transitivamente" + +#. module: product_configurator +#: model:product.attribute,name:product_configurator.product_attribute_transmission +msgid "Transmission" +msgstr "Transmissão" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__uom_id +msgid "Unit of Measure" +msgstr "Unidade de Medida" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__user_id +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__user_id +#: model:res.groups,name:product_configurator.group_product_configurator +msgid "User" +msgstr "Utilizador" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_378 +msgid "V-spoke 16\"" +msgstr "Jante V-spoke 16\"" + +#. module: product_configurator +#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_387 +msgid "V-spoke 18\"" +msgstr "Jante V-spoke 18\"" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__value +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__value_ids +#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view +msgid "Value" +msgstr "Valor" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__value_ids +#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__value_ids +msgid "Values" +msgstr "Valores" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_ids +msgid "Values Configuration" +msgstr "Configuração de Valores" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "Values entered for line '%s' generate a incompatible configuration" +msgstr "" +"Os valores inseridos para a linha '%s' geram uma configuração incompatível" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_config.py:0 +#, python-format +msgid "" +"Values must belong to the attribute of the corresponding attribute_line set " +"on the configuration line" +msgstr "" +"Os valores devem pertencer ao atributo da linha de atributo correspondente " +"definido na linha de configuração" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product_attribute.py:0 +#, python-format +msgid "" +"Values provided to the attribute value line are incompatible with the " +"current rules" +msgstr "" +"Os valores fornecidos para a linha de valor do atributo são incompatíveis " +"com as regras atuais" + +#. module: product_configurator +#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited +msgid "Variant Name" +msgstr "Nome da Variante" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__mako_tmpl_name +#: model:ir.model.fields,field_description:product_configurator.field_product_template__mako_tmpl_name +msgid "Variant name" +msgstr "Nome da variante" + +#. module: product_configurator +#: model:ir.model,name:product_configurator.model_ir_ui_view +msgid "View" +msgstr "Ver" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight +#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight +msgid "Weight" +msgstr "Peso" + +#. module: product_configurator +#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_extra +msgid "Weight Extra" +msgstr "Peso Extra" + +#. module: product_configurator +#: model:ir.model.fields,help:product_configurator.field_product_attribute__search_ok +msgid "" +"When checking for variants with the same configuration, do we include this " +"field in the search?" +msgstr "" +"Ao verificar variantes com a mesma configuração, incluímos este campo na " +"pesquisa?" + +#. module: product_configurator +#. odoo-python +#: code:addons/product_configurator/models/product.py:0 +#, python-format +msgid "You cannot have a duplicate configuration for the same value" +msgstr "Não pode ter uma configuração duplicada para o mesmo valor" + +#, python-format +#~ msgid "" +#~ "Attribute custom type is binary, attachments are the only accepted values " +#~ "with this custom field type" +#~ msgstr "" +#~ "O tipo personalizado de atributo é binário, os anexos são os únicos " +#~ "valores aceites com este tipo de campo personalizado" + +#, python-format +#~ msgid "" +#~ "Attribute custom type must be 'binary' for saving attachments to custom " +#~ "value" +#~ msgstr "" +#~ "O tipo personalizado de atributo deve ser \"binário\" para salvar anexos " +#~ "no valor personalizado" + +#, python-format +#~ msgid "" +#~ "Cannot conduct search on an empty config session without product_tmpl_id " +#~ "kwarg" +#~ msgstr "" +#~ "Não é possível realizar um pesquisa numa sessão de configuração vazia sem " +#~ "kwarg product_tmpl_id" + +#, python-format +#~ msgid "Configuration cannot have the same value inserted twice" +#~ msgstr "A configuração não pode ter o mesmo valor inserido duas vezes" + +#, python-format +#~ msgid "Invalid Configuration" +#~ msgstr "Configuração Inválida" + +#, python-format +#~ msgid "New" +#~ msgstr "Novo" + +#, python-format +#~ msgid "Product created via configuration wizard" +#~ msgstr "Artigo criado por meio do assistente de configuração" + +#, python-format +#~ msgid "Required attribute '%s' is empty" +#~ msgstr "O atributo obrigatório '%s' está vazio" + +#, python-format +#~ msgid "" +#~ "You must select at least one attribute in order to configure a product" +#~ msgstr "Deve selecionar pelo menos um atributo para configurar um artigo" + +#~ msgid "Attribute Line Values" +#~ msgstr "Valores de Linha de Atributo" diff --git a/product_configurator/init_hook.py b/product_configurator/init_hook.py new file mode 100644 index 000000000..5e18812a1 --- /dev/null +++ b/product_configurator/init_hook.py @@ -0,0 +1,10 @@ +import logging + +logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + """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") diff --git a/product_configurator/models/__init__.py b/product_configurator/models/__init__.py new file mode 100644 index 000000000..e2129a2d8 --- /dev/null +++ b/product_configurator/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_config +from . import product_attribute +from . import product +from . import ir_ui_view diff --git a/product_configurator/models/ir_ui_view.py b/product_configurator/models/ir_ui_view.py new file mode 100644 index 000000000..7ad1ae1c9 --- /dev/null +++ b/product_configurator/models/ir_ui_view.py @@ -0,0 +1,11 @@ +from odoo import models + + +class View(models.Model): + _inherit = "ir.ui.view" + + def _validate_tag_button(self, node, name_manager, node_info): + special = node.get("special") + if special and special == "no_save": + return + return super()._validate_tag_button(node, name_manager, node_info) diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py new file mode 100644 index 000000000..9e604eb79 --- /dev/null +++ b/product_configurator/models/product.py @@ -0,0 +1,578 @@ +import logging +from io import StringIO + +from mako.runtime import Context +from mako.template import Template + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + @api.depends("product_variant_ids.product_tmpl_id") + def _compute_product_variant_count(self): + """For configurable products return the number of variants configured or + 1 as many views and methods trigger only when a template has at least + one variant attached. Since we create them from the template we should + have access to them always""" + result = super()._compute_product_variant_count() + for product_tmpl in self: + config_ok = product_tmpl.config_ok + variant_count = product_tmpl.product_variant_count + if config_ok and not variant_count: + product_tmpl.product_variant_count = 1 + return result + + @api.depends("attribute_line_ids.value_ids") + def _compute_template_attr_vals(self): + """Compute all attribute values added in attribute line on + product template""" + for product_tmpl in self: + if product_tmpl.config_ok: + value_ids = product_tmpl.attribute_line_ids.mapped("value_ids") + product_tmpl.attribute_line_val_ids = value_ids + else: + product_tmpl.attribute_line_val_ids = False + + @api.constrains("attribute_line_ids", "attribute_value_line_ids") + def check_attr_value_ids(self): + """Check attribute lines don't have some attribute value that + is not present in attribute lines of that product template""" + for product_tmpl in self: + if not product_tmpl.env.context.get("check_constraint", True): + continue + attr_val_lines = product_tmpl.attribute_value_line_ids + attr_val_ids = attr_val_lines.mapped("value_ids") + if not attr_val_ids <= product_tmpl.attribute_line_val_ids: + raise ValidationError( + _( + "All attribute values used in attribute value lines " + "must be defined in the attribute lines of the " + "template" + ) + ) + + @api.constrains("attribute_value_line_ids") + def _validate_unique_config(self): + """Check for duplicate configurations for the same + attribute value in image lines""" + for template in self: + attr_val_line_vals = template.attribute_value_line_ids.read( + ["value_id", "value_ids"], load=False + ) + attr_val_line_vals = [ + (line["value_id"], tuple(line["value_ids"])) + for line in attr_val_line_vals + ] + if len(set(attr_val_line_vals)) != len(attr_val_line_vals): + raise ValidationError( + _("You cannot have a duplicate configuration for the same value") + ) + + config_ok = fields.Boolean(string="Can be Configured") + + config_line_ids = fields.One2many( + comodel_name="product.config.line", + inverse_name="product_tmpl_id", + string="Attribute Dependencies", + copy=False, + ) + + config_image_ids = fields.One2many( + comodel_name="product.config.image", + inverse_name="product_tmpl_id", + string="Configuration Images", + copy=True, + ) + + attribute_value_line_ids = fields.One2many( + comodel_name="product.attribute.value.line", + inverse_name="product_tmpl_id", + string="Attribute Value Lines", + copy=True, + ) + + attribute_line_val_ids = fields.Many2many( + comodel_name="product.attribute.value", + compute="_compute_template_attr_vals", + store=False, + ) + + config_step_line_ids = fields.One2many( + comodel_name="product.config.step.line", + inverse_name="product_tmpl_id", + string="Configuration Lines", + copy=False, + ) + + mako_tmpl_name = fields.Text( + string="Variant name", + help="Generate Name based on Mako Template", + copy=True, + ) + + # We are calculating weight of variants based on weight of + # product-template so that no need of compute and inverse on this + weight = fields.Float( + compute="_compute_weight", + inverse="_set_weight", # pylint: disable=C8110 + search="_search_weight", + store=False, + ) + weight_dummy = fields.Float( + string="Manual Weight", + digits="Stock Weight", + help="Manual setting of product template weight", + ) + + def _compute_weight(self): + config_products = self.filtered(lambda template: template.config_ok) + for product in config_products: + product.weight = product.weight_dummy + standard_products = self - config_products + return super(ProductTemplate, standard_products)._compute_weight() + + def _set_weight(self): + for product_tmpl in self: + product_tmpl.weight_dummy = product_tmpl.weight + if not product_tmpl.config_ok: + super(ProductTemplate, product_tmpl)._set_weight() + return + + def _search_weight(self, operator, value): + return [("weight_dummy", operator, value)] + + def _check_default_values(self): + default_val_ids = ( + self.attribute_line_ids.filtered(lambda line: line.default_val) + .mapped("default_val") + .ids + ) + + cfg_session_obj = self.env["product.config.session"] + try: + cfg_session_obj.validate_configuration( + value_ids=default_val_ids, product_tmpl_id=self.id, final=False + ) + except ValidationError as exc: + raise ValidationError(exc.args[0]) from exc + except Exception as exc: + raise ValidationError( + _("Default values provided generate an invalid configuration") + ) from exc + + @api.constrains("config_line_ids", "attribute_line_ids") + def _check_default_value_domains(self): + for template in self: + try: + template._check_default_values() + except ValidationError as exc: + raise ValidationError( + _( + "Restrictions added make the current default values " + "generate an invalid configuration.\ + \n%s" + ) + % (exc.name) + ) from exc + + def toggle_config(self): + for record in self: + record.config_ok = not record.config_ok + + def _create_variant_ids(self): + """Prevent configurable products from creating variants as these serve + only as a template for the product configurator""" + templates = self.filtered(lambda t: not t.config_ok) + if not templates: + return None + return super(ProductTemplate, templates)._create_variant_ids() + + def unlink(self): + """- Prevent the removal of configurable product templates + from variants + - Patch for check access rights of user(configurable products)""" + configurable_templates = self.filtered(lambda template: template.config_ok) + if configurable_templates: + configurable_templates[:1].check_config_user_access() + for config_template in configurable_templates: + variant_unlink = config_template.env.context.get( + "unlink_from_variant", False + ) + if variant_unlink: + self -= config_template + res = super().unlink() + return res + + def copy(self, default=None): + """Copy restrictions, config Steps and attribute lines + ith product template""" + if not default: + default = {} + self = self.with_context(check_constraint=False) + res = super().copy(default=default) + + # Attribute lines + attribute_line_dict = {} + for line in res.attribute_line_ids: + attribute_line_dict.update({line.attribute_id.id: line.id}) + + # Restrictions + for line in self.config_line_ids: + old_restriction = line.domain_id + new_restriction = old_restriction.copy() + config_line_default = { + "product_tmpl_id": res.id, + "domain_id": new_restriction.id, + } + new_attribute_line_id = attribute_line_dict.get( + line.attribute_line_id.attribute_id.id, False + ) + if not new_attribute_line_id: + continue + config_line_default.update({"attribute_line_id": new_attribute_line_id}) + line.copy(config_line_default) + + # Config steps + config_step_line_default = {"product_tmpl_id": res.id} + for line in self.config_step_line_ids: + new_attribute_line_ids = [ + attribute_line_dict.get(old_attr_line.attribute_id.id) + for old_attr_line in line.attribute_line_ids + if old_attr_line.attribute_id.id in attribute_line_dict + ] + if new_attribute_line_ids: + config_step_line_default.update( + {"attribute_line_ids": [(6, 0, new_attribute_line_ids)]} + ) + line.copy(config_step_line_default) + return res + + def configure_product(self): + """launches a product configurator wizard with a linked + template in order to configure new product.""" + return self.with_context(product_tmpl_id_readonly=True).create_config_wizard( + click_next=False + ) + + def create_config_wizard( + self, + model_name="product.configurator", + extra_vals=None, + click_next=True, + ): + """create product configuration wizard + - return action to launch wizard + - click on next step based on value of click_next""" + wizard_obj = self.env[model_name] + wizard_vals = {"product_tmpl_id": self.id} + if extra_vals: + wizard_vals.update(extra_vals) + wizard = wizard_obj.create(wizard_vals) + if click_next: + action = wizard.action_next_step() + else: + wizard_obj = wizard_obj.with_context( + wizard_model=model_name, + allow_preset_selection=True, + ) + action = wizard_obj.get_wizard_action(wizard=wizard) + return action + + @api.model + def _check_config_group_rights(self): + """Return True/False from system parameter + - Signals access rights needs to check or not + :Params: return : boolean""" + ICPSudo = self.env["ir.config_parameter"].sudo() + manager_product_configuration_settings = ICPSudo.get_param( + "product_configurator.manager_product_configuration_settings" + ) + return manager_product_configuration_settings + + @api.model + def check_config_user_access(self): + """Check user have access to perform action(create/write/delete) + on configurable products""" + if not self._check_config_group_rights(): + return True + config_manager = self.env.user.has_group( + "product_configurator.group_product_configurator_manager" + ) + user_root = self.env.ref("base.user_root") + user_admin = self.env.ref("base.user_admin") + if ( + config_manager + or self.env.user.id in [user_root.id, user_admin.id] + or self.env.su + ): + return True + raise ValidationError( + _( + "Sorry, you are not allowed to create/change this kind of " + "document. For more information please contact your manager." + ) + ) + + @api.model_create_multi + def create(self, vals_list): + """Patch for check access rights of user(configurable products)""" + for vals in vals_list: + config_ok = vals.get("config_ok", False) + if config_ok: + self.check_config_user_access() + return super().create(vals_list) + + def write(self, vals): + """Patch for check access rights of user(configurable products)""" + change_config_ok = "config_ok" in vals + configurable_templates = self.filtered(lambda template: template.config_ok) + if change_config_ok or configurable_templates: + self[:1].check_config_user_access() + + return super().write(vals) + + @api.constrains("config_line_ids") + def _check_config_line_domain(self): + attribute_line_ids = self.attribute_line_ids + tmpl_value_ids = attribute_line_ids._configurator_value_ids() + tmpl_attribute_ids = attribute_line_ids.mapped("attribute_id") + error_message = False + for domain_id in self.config_line_ids.mapped("domain_id"): + domain_attr_ids = domain_id.domain_line_ids.mapped("attribute_id") + domain_value_ids = domain_id.domain_line_ids.mapped("value_ids") + invalid_value_ids = domain_value_ids - tmpl_value_ids + invalid_attribute_ids = domain_attr_ids - tmpl_attribute_ids + if not invalid_attribute_ids and not invalid_value_ids: + continue + if not error_message: + error_message = _( + "Following Attribute/Value from restriction " + "are not present in template attributes/values. " + "Please make sure you are adding right restriction" + ) + error_message += _("\nRestriction: %s", domain_id.name) + error_message += ( + invalid_attribute_ids + and _( + "\nAttribute/s: %s", ", ".join(invalid_attribute_ids.mapped("name")) + ) + or "" + ) + error_message += ( + invalid_value_ids + and _("\nValue/s: %s\n", ", ".join(invalid_value_ids.mapped("name"))) + or "" + ) + if error_message: + raise ValidationError(error_message) + + +class ProductProduct(models.Model): + _inherit = "product.product" + _rec_name = "config_name" + + @api.constrains("product_template_attribute_value_ids") + def _check_duplicate_product(self): + """Check for prducts with same attribute values/custom values""" + for product in self: + if not product.config_ok: + continue + + # At the moment, I don't have enough confidence with my + # understanding of binary attributes, so will leave these + # as not matching... + # In theory, they should just work, if they are set to "non search" + # in custom field def! + # TODO: Check the logic with binary attributes + config_session_obj = product.env["product.config.session"] + ptav_ids = product.product_template_attribute_value_ids.mapped( + "product_attribute_value_id" + ) + duplicates = config_session_obj.search_variant( + product_tmpl_id=product.product_tmpl_id, + value_ids=ptav_ids.ids, + ).filtered(lambda p, product=product: p.id != product.id) + + if duplicates: + raise ValidationError( + _( + "Configurable Products cannot have duplicates " + "(identical attribute values)" + ) + ) + + def _get_config_name(self): + """Name for configured products + :param: return : String""" + self.ensure_one() + return self.name + + def _get_mako_context(self, buf): + """Return context needed for computing product name based + on mako-tamplate define on it's product template""" + self.ensure_one() + ptav_ids = self.product_template_attribute_value_ids.mapped( + "product_attribute_value_id" + ) + return Context( + buf, + product=self, + attribute_values=ptav_ids, + steps=self.product_tmpl_id.config_step_line_ids, + template=self.product_tmpl_id, + ) + + def _get_mako_tmpl_name(self): + """Compute and return product name based on mako-tamplate + define on it's product template""" + self.ensure_one() + if self.mako_tmpl_name: + try: + mytemplate = Template(self.mako_tmpl_name or "") + buf = StringIO() + ctx = self._get_mako_context(buf) + mytemplate.render_context(ctx) + return buf.getvalue() + except Exception: + _logger.error( + _("Error while calculating mako product name: %s") + % self.display_name + ) + return self.display_name + + @api.depends("product_template_attribute_value_ids.weight_extra") + def _compute_product_weight_extra(self): + for product in self: + product.weight_extra = sum( + product.mapped("product_template_attribute_value_ids.weight_extra") + ) + + def _compute_product_weight(self): + for product in self: + if product.config_ok: + tmpl_weight = product.product_tmpl_id.weight + product.weight = tmpl_weight + product.weight_extra + else: + product.weight = product.weight_dummy + + def _search_product_weight(self, operator, value): + return [("weight_dummy", operator, value)] + + def _inverse_product_weight(self): + """Store weight in dummy field""" + self.weight_dummy = self.weight + + config_name = fields.Char( + string="Configuration Name", compute="_compute_config_name" + ) + weight_extra = fields.Float(compute="_compute_product_weight_extra") + weight_dummy = fields.Float(string="Manual Weight", digits="Stock Weight") + weight = fields.Float( + compute="_compute_product_weight", + inverse="_inverse_product_weight", + search="_search_product_weight", + store=False, + ) + + # product preset + config_preset_ok = fields.Boolean(string="Is Preset") + + def _compute_config_name(self): + """Compute the name of the configurable products and use template + name for others""" + for product in self: + if product.config_ok: + product.config_name = product._get_config_name() + else: + product.config_name = product.name + + def reconfigure_product(self): + """launches a product configurator wizard with a linked + template and variant in order to re-configure an existing product. + It is essentially a shortcut to pre-fill configuration + data of a variant""" + self.ensure_one() + + extra_vals = {"product_id": self.id} + return self.product_tmpl_id.create_config_wizard(extra_vals=extra_vals) + + @api.model + def check_config_user_access(self, mode): + """Check user have access to perform action(create/write/delete) + on configurable products""" + if not self.env["product.template"]._check_config_group_rights(): + return True + config_manager = self.env.user.has_group( + "product_configurator.group_product_configurator_manager" + ) + config_user = self.env.user.has_group( + "product_configurator.group_product_configurator" + ) + user_root = self.env.ref("base.user_root") + user_admin = self.env.ref("base.user_admin") + if ( + config_manager + or (config_user and mode not in ["delete"]) + or self.env.user.id in [user_root.id, user_admin.id] + ): + return True + raise ValidationError( + _( + "Sorry, you are not allowed to create/change this kind of " + "document. For more information please contact your manager." + ) + ) + + def unlink(self): + """- Signal unlink from product variant through context so + removal can be stopped for configurable templates + - check access rights of user(configurable products)""" + config_product = any(p.config_ok for p in self) + if config_product: + self.env["product.product"].check_config_user_access(mode="delete") + ctx = dict(self.env.context, unlink_from_variant=True) + self.env.context = ctx + return super().unlink() + + @api.model_create_multi + def create(self, vals_list): + """Patch for check access rights of user(configurable products)""" + for vals in vals_list: + config_ok = vals.get("config_ok", False) + if config_ok: + self.check_config_user_access(mode="create") + return super().create(vals_list) + + def write(self, vals): + """Patch for check access rights of user(configurable products)""" + change_config_ok = "config_ok" in vals + configurable_products = self.filtered(lambda product: product.config_ok) + if change_config_ok or configurable_products: + self[:1].check_config_user_access(mode="write") + + return super().write(vals) + + def _compute_product_price_extra(self): + standard_products = self.filtered(lambda product: not product.config_ok) + config_products = self - standard_products + if standard_products: + result = super( + ProductProduct, standard_products + )._compute_product_price_extra() + else: + result = None + for product in config_products: + attribute_value_obj = self.env["product.attribute.value"] + value_ids = ( + product.product_template_attribute_value_ids.product_attribute_value_id + ) + extra_prices = attribute_value_obj.get_attribute_value_extra_prices( + product_tmpl_id=product.product_tmpl_id.id, pt_attr_value_ids=value_ids + ) + product.price_extra = sum(extra_prices.values()) + return result diff --git a/product_configurator/models/product_attribute.py b/product_configurator/models/product_attribute.py new file mode 100644 index 000000000..c1491849c --- /dev/null +++ b/product_configurator/models/product_attribute.py @@ -0,0 +1,454 @@ +from ast import literal_eval + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + _order = "sequence" + + def copy(self, default=None): + """Add ' (Copy)' in name to prevent attribute + having same name while copying""" + if not default: + default = {} + new_attrs = self.env["product.attribute"] + for attr in self: + default.update({"name": attr.name + " (copy)"}) + new_attrs += super(ProductAttribute, attr).copy(default) + return new_attrs + + @api.model + def _get_nosearch_fields(self): + """Return a list of custom field types that do not support searching""" + return ["binary"] + + @api.onchange("custom_type") + def onchange_custom_type(self): + if self.custom_type in self._get_nosearch_fields(): + self.search_ok = False + if self.custom_type not in ("integer", "float"): + self.min_val = False + self.max_val = False + + @api.onchange("val_custom") + def onchange_val_custom_field(self): + if not self.val_custom: + self.custom_type = False + + CUSTOM_TYPES = [ + ("char", "Char"), + ("integer", "Integer"), + ("float", "Float"), + ("text", "Textarea"), + ("color", "Color"), + ("binary", "Attachment"), + ("date", "Date"), + ("datetime", "DateTime"), + ] + + active = fields.Boolean( + default=True, + help="By unchecking the active field you can " + "disable a attribute without deleting it", + ) + min_val = fields.Integer(string="Min Value", help="Minimum value allowed") + max_val = fields.Integer(string="Max Value", help="Maximum value allowed") + + # TODO: Exclude self from result-set of dependency + val_custom = fields.Boolean( + string="Custom Value", help="Allow custom value for this attribute?" + ) + custom_type = fields.Selection( + selection=CUSTOM_TYPES, + string="Field Type", + help="The type of the custom field generated in the frontend", + ) + description = fields.Text(translate=True) + search_ok = fields.Boolean( + string="Searchable", + help="When checking for variants with " + "the same configuration, do we " + "include this field in the search?", + ) + required = fields.Boolean( + default=True, + help="Determines the required value of this " + "attribute though it can be change on " + "the template level", + ) + multi = fields.Boolean( + help="Allow selection of multiple values for this attribute?", + ) + uom_id = fields.Many2one(comodel_name="uom.uom", string="Unit of Measure") + image = fields.Binary() + + # TODO prevent the same attribute from being defined twice on the + # attribute lines + + @api.constrains("custom_type", "search_ok") + def check_searchable_field(self): + for attribute in self: + nosearch_fields = attribute._get_nosearch_fields() + if attribute.custom_type in nosearch_fields and attribute.search_ok: + raise ValidationError( + _( + "Selected custom field type '%s' is not searchable", + attribute.custom_type, + ) + ) + + def validate_custom_val(self, val): + """Pass in a desired custom value and ensure it is valid. + Probably should check type, etc., but let's assume fine for the moment. + """ + self.ensure_one() + if self.custom_type in ("integer", "float"): + minv = self.min_val + maxv = self.max_val + val = literal_eval(str(val)) + if minv and maxv and (val < minv or val > maxv): + raise ValidationError( + _( + "Selected custom value '%(name)s' must be " + "between %(min_val)s and %(max_val)s", + **{ + "name": self.name, + "min_val": self.min_val, + "max_val": self.max_val, + }, + ) + ) + elif minv and val < minv: + raise ValidationError( + _( + "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( + _( + "Selected custom value '%(name)s' " + "must be lower than %(max_value)s", + **{"name": self.name, "max_value": self.max_val + 1}, + ) + ) + + @api.constrains("min_val", "max_val") + def _check_constraint_min_max_value(self): + """Prevent to add Maximun value less than minimum value""" + for attribute in self: + if attribute.custom_type not in ("integer", "float"): + continue + minv = attribute.min_val + maxv = attribute.max_val + if maxv and minv and maxv < minv: + raise ValidationError( + _("Maximum value must be greater than Minimum value") + ) + + def _configurator_value_ids(self): + """Values accepted for attributes in `self`.""" + values = self.value_ids + if any(self.mapped("val_custom")): + values += self.env["product.config.session"].get_custom_value_id() + return values + + +class ProductAttributeLine(models.Model): + _inherit = "product.template.attribute.line" + _order = "product_tmpl_id, sequence, id" + # TODO: Order by dependencies first and then sequence so dependent fields + # do not come before master field + + @api.onchange("attribute_id") + def onchange_attribute(self): + """Set default value of required/multi/cutom from attribute""" + self.value_ids = False + self.required = self.attribute_id.required + self.multi = self.attribute_id.multi + self.custom = self.attribute_id.val_custom + # TODO: Remove all dependencies pointed towards the attribute being + # changed + + @api.onchange("value_ids") + def onchange_values(self): + if self.default_val and self.default_val not in self.value_ids: + self.default_val = None + + custom = fields.Boolean(help="Allow custom values for this attribute?") + required = fields.Boolean(help="Is this attribute required?") + multi = fields.Boolean( + help="Allow selection of multiple values for this attribute?", + ) + default_val = fields.Many2one(comodel_name="product.attribute.value") + + sequence = fields.Integer(default=10) + + @api.constrains("value_ids", "default_val") + def _check_default_values(self): + """default value should not be outside of the + values selected in attribute line""" + for line in self.filtered(lambda line: line.default_val): + if line.default_val not in line.value_ids: + raise ValidationError( + _( + "Default values for each attribute line must exist in " + "the attribute values (%(attr_name)s: %(default_val)s)", + **{ + "attr_name": line.attribute_id.name, + "default_val": line.default_val.name, + }, + ) + ) + + @api.constrains("active", "value_ids", "attribute_id") + def _check_valid_values(self): + """Overwrite to save attribute line without + values when custom is true""" + for ptal in self: + # Customization + if ptal.active and not ptal.value_ids and not ptal.custom: + # Old code + # if ptal.active and not ptal.value_ids: + # Customization End + raise ValidationError( + _( + "The attribute %(attr)s must have at least one value for " + "the product %(product)s.", + **{ + "attr": ptal.attribute_id.display_name, + "product": ptal.product_tmpl_id.display_name, + }, + ) + ) + for pav in ptal.value_ids: + if pav.attribute_id != ptal.attribute_id: + raise ValidationError( + _( + "On the product %(product)s you cannot associate the " + "value %(value)s with the attribute %(attr)s because they " + "do not match.", + **{ + "product": ptal.product_tmpl_id.display_name, + "value": pav.display_name, + "attr": ptal.attribute_id.display_name, + }, + ) + ) + return True + + def _configurator_value_ids(self): + """Values accepted for template attribute lines in `self`.""" + values = self.value_ids + if any(self.mapped("custom")): + values += self.env["product.config.session"].get_custom_value_id() + return values + + +class ProductAttributeValue(models.Model): + _inherit = "product.attribute.value" + + def copy(self, default=None): + """Add ' (Copy)' in name to prevent attribute + having same name while copying""" + if not default: + default = {} + default.update({"name": self.name + " (copy)"}) + product = super().copy(default) + return product + + active = fields.Boolean( + default=True, + help="By unchecking the active field you can " + "disable a attribute value without deleting it", + ) + product_id = fields.Many2one(comodel_name="product.product") + image = fields.Binary( + attachment=True, + help="Attribute value image (Display on website for radio buttons)", + ) + + @api.model + def get_attribute_value_extra_prices( + self, product_tmpl_id, pt_attr_value_ids, pricelist=None + ): + extra_prices = {} + if not pricelist: + pricelist = self.env.user.partner_id.property_product_pricelist + + related_product_av_ids = self.env["product.attribute.value"].search( + [("id", "in", pt_attr_value_ids.ids), ("product_id", "!=", False)] + ) + extra_prices = { + av.id: av.product_id.with_context( + pricelist=pricelist.id + )._get_contextual_price() + for av in related_product_av_ids + } + remaining_av_ids = pt_attr_value_ids - related_product_av_ids + pe_lines = self.env["product.template.attribute.value"].search( + [ + ("product_attribute_value_id", "in", remaining_av_ids.ids), + ("product_tmpl_id", "=", product_tmpl_id), + ] + ) + for line in pe_lines: + attr_val_id = line.product_attribute_value_id + if attr_val_id.id not in extra_prices: + extra_prices[attr_val_id.id] = 0 + extra_prices[attr_val_id.id] += line.price_extra + return extra_prices + + def name_get(self): + res = super().name_get() + 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]) + if price_extra: + val = ( + val[0], + "{} ( +{} )".format( + val[1], + ("{0:,.%sf}" % (price_precision)).format(price_extra), + ), + ) + res_prices.append(val) + return res_prices + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Use name_search as a domain restriction for the frontend to show + only values set on the product template taking all the configuration + restrictions into account. + + TODO: This only works when activating the selection not when typing + """ + product_tmpl_id = self.env.context.get("_cfg_product_tmpl_id") + if product_tmpl_id: + # TODO: Avoiding browse here could be a good performance enhancer + product_tmpl = self.env["product.template"].browse(product_tmpl_id) + tmpl_vals = product_tmpl.attribute_line_ids.mapped("value_ids") + attr_restrict_ids = [] + preset_val_ids = [] + new_args = [] + for arg in args: + # Restrict values only to value_ids set on product_template + if arg[0] == "id" and arg[1] == "not in": + preset_val_ids = arg[2] + # TODO: Check if all values are available for configuration + else: + new_args.append(arg) + val_ids = set(tmpl_vals.ids) + if preset_val_ids: + val_ids -= set(arg[2]) + val_ids = self.env["product.config.session"].values_available( + val_ids, preset_val_ids, product_tmpl_id=product_tmpl_id + ) + new_args.append(("id", "in", val_ids)) + mono_tmpl_lines = product_tmpl.attribute_line_ids.filtered( + lambda line: not line.multi + ) + for line in mono_tmpl_lines: + line_val_ids = set(line.mapped("value_ids").ids) + if line_val_ids & set(preset_val_ids): + attr_restrict_ids.append(line.attribute_id.id) + if attr_restrict_ids: + new_args.append(("attribute_id", "not in", attr_restrict_ids)) + args = new_args + res = super().name_search(name=name, args=args, operator=operator, limit=limit) + return res + + # TODO: Prevent unlinking custom options by overriding unlink + + # _sql_constraints = [ + # ('unique_custom', 'unique(id,allow_custom_value)', + # 'Only one custom value per dimension type is allowed') + # ] + + +class ProductAttributePrice(models.Model): + _inherit = "product.template.attribute.value" + # Leverage product.template.attribute.value to compute the extra weight + # each attribute adds + + weight_extra = fields.Float(string="Attribute Weight Extra", digits="Stock Weight") + + +class ProductAttributeValueLine(models.Model): + _name = "product.attribute.value.line" + _description = "Product Attribute Value Line" + _order = "sequence" + + sequence = fields.Integer(default=10) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ondelete="cascade", + required=True, + ) + value_id = fields.Many2one( + comodel_name="product.attribute.value", + required=True, + string="Attribute Value", + ) + attribute_id = fields.Many2one( + comodel_name="product.attribute", related="value_id.attribute_id" + ) + value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_attribute_value_product_attribute_value_line_rel", + column1="product_attribute_value_line_id", + column2="product_attribute_value_id", + string="Values Configuration", + ) + product_value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_attr_values_attr_values_rel", + column1="product_val_id", + column2="attr_val_id", + compute="_compute_get_value_id", + store=True, + ) + + @api.depends( + "product_tmpl_id", + "product_tmpl_id.attribute_line_ids", + "product_tmpl_id.attribute_line_ids.value_ids", + ) + def _compute_get_value_id(self): + for attr_val_line in self: + template = attr_val_line.product_tmpl_id + value_list = template.attribute_line_ids.mapped("value_ids") + attr_val_line.product_value_ids = [(6, 0, value_list.ids)] + + @api.constrains("value_ids") + def _validate_configuration(self): + """Ensure that the passed configuration in value_ids is a valid""" + cfg_session_obj = self.env["product.config.session"] + for attr_val_line in self: + value_ids = attr_val_line.value_ids.ids + value_ids.append(attr_val_line.value_id.id) + valid = cfg_session_obj.validate_configuration( + value_ids=value_ids, + product_tmpl_id=attr_val_line.product_tmpl_id.id, + final=False, + ) + if not valid: + raise ValidationError( + _( + "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 new file mode 100644 index 000000000..02565a488 --- /dev/null +++ b/product_configurator/models/product_config.py @@ -0,0 +1,1692 @@ +import logging +from ast import literal_eval + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.misc import flatten, formatLang + +_logger = logging.getLogger(__name__) + + +class ProductConfigDomain(models.Model): + _name = "product.config.domain" + _description = "Domain for Config Restrictions" + + @api.depends("implied_ids") + def _get_trans_implied(self): + """Computes the transitive closure of relation implied_ids""" + + def linearize(domains): + trans_domains = domains + for domain in domains: + implied_domains = domain.implied_ids - domain + if implied_domains: + trans_domains |= linearize(implied_domains) + return trans_domains + + for domain in self: + domain.trans_implied_ids = linearize(domain) + + def compute_domain(self): + """Returns a list of domains defined on a + product.config.domain_line_ids and all implied_ids""" + # TODO: Enable the usage of OR operators between implied_ids + # TODO: Add implied_ids sequence field to enforce order of operations + # TODO: Prevent circular dependencies + computed_domain = [] + for domain in self: + lines = domain.trans_implied_ids.mapped("domain_line_ids").sorted() + if not lines: + continue + for line in lines[:-1]: + if line.operator == "or": + computed_domain.append("|") + computed_domain.append( + (line.attribute_id.id, line.condition, line.value_ids.ids) + ) + # ensure 2 operands follow the last operator + computed_domain.append( + ( + lines[-1].attribute_id.id, + lines[-1].condition, + lines[-1].value_ids.ids, + ) + ) + return computed_domain + + name = fields.Char(required=True) + domain_line_ids = fields.One2many( + comodel_name="product.config.domain.line", + inverse_name="domain_id", + string="Restrictions", + required=True, + copy=True, + ) + implied_ids = fields.Many2many( + comodel_name="product.config.domain", + relation="product_config_domain_implied_rel", + string="Inherited", + column1="domain_id", + column2="parent_id", + ) + trans_implied_ids = fields.Many2many( + comodel_name="product.config.domain", + compute=_get_trans_implied, + column1="domain_id", + column2="parent_id", + string="Transitively inherits", + ) + + +class ProductConfigDomainLine(models.Model): + _name = "product.config.domain.line" + _order = "sequence" + _description = "Domain Line for Config Restrictions" + + def _get_domain_conditions(self): + operators = [("in", "In"), ("not in", "Not In")] + + return operators + + def _get_domain_operators(self): + andor = [("and", "And"), ("or", "Or")] + + return andor + + @api.depends("attribute_id") + def _compute_template_attribute_value_ids(self): + for domain in self: + domain.template_attribute_value_ids = ( + domain._get_allowed_attribute_value_ids() + ) + + def _compute_attribute_id_domain(self): + if "product_attribute_ids" in self.env.context: + return [("id", "in", self.env.context["product_attribute_ids"][0][2])] + return [] + + def _get_allowed_attribute_value_ids(self): + self.ensure_one() + product_template = self.env["product.template"] + if self.env.context.get("product_tmpl_id"): + product_template = product_template.browse( + self.env.context.get("product_tmpl_id") + ) + template_lines = product_template.attribute_line_ids + attribute_values = self.attribute_id._configurator_value_ids() + return ( + product_template + and (template_lines._configurator_value_ids() & attribute_values) + or attribute_values + ) + + template_attribute_value_ids = fields.Many2many( + comodel_name="product.attribute.value", + string="Template Attribute Values", + compute="_compute_template_attribute_value_ids", + ) + attribute_id = fields.Many2one( + comodel_name="product.attribute", + string="Attribute", + required=True, + domain=lambda self: self._compute_attribute_id_domain(), + ) + domain_id = fields.Many2one( + comodel_name="product.config.domain", required=True, string="Rule" + ) + condition = fields.Selection(selection=_get_domain_conditions, required=True) + value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_config_domain_line_attr_rel", + column1="line_id", + column2="attribute_id", + string="Values", + required=True, + ) + operator = fields.Selection( + selection=_get_domain_operators, + string="Operators", + default="and", + required=True, + ) + sequence = fields.Integer( + default=1, + help="Set the order of operations for evaluation domain lines", + ) + + +class ProductConfigLine(models.Model): + _name = "product.config.line" + _description = "Product Config Restrictions" + _order = "product_tmpl_id, sequence, id" + + # TODO: Prevent config lines having dependencies that are not set in other + # config lines + # TODO: Prevent circular depdencies: Length -> Color, Color -> Length + + @api.onchange("attribute_line_id") + def onchange_attribute(self): + self.value_ids = False + self.domain_id = False + + @api.depends( + "product_tmpl_id", + "attribute_line_id", + "product_tmpl_id.attribute_line_ids", + "product_tmpl_id.config_line_ids", + ) + def _compute_template_attribute_ids(self): + for config_line in self: + product_template = config_line.product_tmpl_id + attribute_line_ids = product_template.attribute_line_ids + config_line.template_attribute_ids = attribute_line_ids.mapped( + "attribute_id" + ) + + template_attribute_ids = fields.Many2many( + comodel_name="product.attribute", + string="Template Attributes", + compute="_compute_template_attribute_ids", + ) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ondelete="cascade", + required=True, + ) + attribute_line_id = fields.Many2one( + comodel_name="product.template.attribute.line", + string="Attribute Line", + ondelete="cascade", + required=True, + ) + attr_line_val_ids = fields.Many2many( + comodel_name="product.attribute.value", + compute="_compute_attr_line_val_ids", + string="Allowed Attribute Values", + help="For normal attributes " + "the values configured for the product can be selected.\n" + "For custom attributes the 'Custom' value can also be selected.", + ) + value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="cfg_line_attr_val_id_rel", + column1="cfg_line_id", + column2="attr_val_id", + string="Values", + ) + domain_id = fields.Many2one( + comodel_name="product.config.domain", + required=True, + string="Restrictions", + ) + sequence = fields.Integer(default=10) + + @api.constrains("value_ids") + def check_value_attributes(self): + """Values selected in config lines must be allowed.""" + for line in self: + forbidden_values = line.value_ids - line.attr_line_val_ids + if forbidden_values: + raise ValidationError( + _( + "Values must belong to the attribute of the " + "corresponding attribute_line set on the " + "configuration line" + ) + ) + + @api.depends( + "attribute_line_id.value_ids", + "attribute_line_id.attribute_id.val_custom", + ) + def _compute_attr_line_val_ids(self): + for config_line in self: + config_line.attr_line_val_ids = ( + config_line.attribute_line_id._configurator_value_ids() + ) + + +class ProductConfigImage(models.Model): + _name = "product.config.image" + _inherit = ["image.mixin"] + _description = "Product Config Image" + _order = "sequence" + + name = fields.Char(required=True, translate=True) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product", + ondelete="cascade", + required=True, + ) + sequence = fields.Integer(default=10) + value_ids = fields.Many2many( + comodel_name="product.attribute.value", string="Configuration" + ) + + @api.constrains("value_ids") + def _check_value_ids(self): + """Check combination of values is possible according to given + restrictions on linked product template""" + cfg_session_obj = self.env["product.config.session"] + for cfg_img in self: + try: + cfg_session_obj.validate_configuration( + value_ids=cfg_img.value_ids.ids, + product_tmpl_id=cfg_img.product_tmpl_id.id, + final=False, + ) + except ValidationError as exc: + raise ValidationError( + _( + "Values entered for line '%s' generate " + "a incompatible configuration", + cfg_img.name, + ) + ) from exc + + +class ProductConfigStep(models.Model): + _name = "product.config.step" + _description = "Product Config Steps" + + # TODO: Prevent values which have dependencies to be set in a + # step with higher sequence than the dependency + + name = fields.Char(required=True, translate=True) + + +class ProductConfigStepLine(models.Model): + _name = "product.config.step.line" + _description = "Product Config Step Lines" + _order = "sequence, config_step_id, id" + + name = fields.Char(related="config_step_id.name") + config_step_id = fields.Many2one( + comodel_name="product.config.step", + string="Configuration Step", + required=True, + ) + attribute_line_ids = fields.Many2many( + comodel_name="product.template.attribute.line", + relation="config_step_line_attr_id_rel", + column1="cfg_line_id", + column2="attr_id", + string="Attribute Lines", + ) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ondelete="cascade", + required=True, + ) + sequence = fields.Integer(default=10) + + @api.constrains("config_step_id") + def _check_config_step(self): + """Prevent to add same step more than once on same product template""" + for config_step in self: + cfg_step_lines = config_step.product_tmpl_id.config_step_line_ids + cfg_steps = cfg_step_lines.filtered( + lambda line, config_step=config_step: line != config_step + ).mapped("config_step_id") + if config_step.config_step_id in cfg_steps: + raise ValidationError( + _("Cannot have a configuration step defined twice.") + ) + + +class ProductConfigSession(models.Model): + _name = "product.config.session" + _description = "Product Config Session" + + @api.depends( + "value_ids", + "product_tmpl_id.list_price", + "product_tmpl_id.attribute_line_ids", + "product_tmpl_id.attribute_line_ids.value_ids", + "product_tmpl_id.attribute_line_ids.product_template_value_ids", + "product_tmpl_id.attribute_line_ids." "product_template_value_ids.price_extra", + ) + def _compute_cfg_price(self): + for session in self: + if session.product_tmpl_id: + price = session.get_cfg_price() + else: + price = 0.00 + session.price = price + + def get_custom_value_id(self): + """Return record set of attribute value 'custom'""" + custom_ext_id = "product_configurator.custom_attribute_value" + custom_val_id = self.env.ref(custom_ext_id) + return custom_val_id + + @api.model + def _get_custom_vals_dict(self): + """Retrieve session custom values as a dictionary of the form + {attribute_id: parsed_custom_value}""" + custom_vals = {} + for val in self.custom_value_ids: + if val.attribute_id.custom_type in ["float", "integer"]: + custom_vals[val.attribute_id.id] = literal_eval(val.value) + elif val.attribute_id.custom_type == "binary": + custom_vals[val.attribute_id.id] = val.attachment_ids + else: + custom_vals[val.attribute_id.id] = val.value + return custom_vals + + def _compute_config_step_name(self): + """Get the config.step.line name using the string stored in config_step + field of the session""" + cfg_step_line_obj = self.env["product.config.step.line"] + cfg_session_step_lines = self.mapped("config_step") + cfg_step_line_ids = set() + for step in cfg_session_step_lines: + try: + cfg_step_line_ids.add(int(step)) + except ValueError: + _logger.debug("Step from session not valid") + cfg_step_lines = cfg_step_line_obj.browse(cfg_step_line_ids) + for session in self: + try: + config_step = int(session.config_step) + config_step_line = cfg_step_lines.filtered( + lambda x, config_step=config_step: x.id == config_step + ) + session.config_step_name = config_step_line.name + except Exception: + _logger.debug("Invalid session data ignored") + if not session.config_step_name: + session.config_step_name = session.config_step + + @api.model + def get_cfg_weight(self, value_ids=None, custom_vals=None): + """Computes the weight of the configured product based on the + configuration passed in via value_ids and custom_values + + :param value_ids: list of attribute value_ids + :param custom_vals: dictionary of custom attribute values + :returns: final configuration weight""" + + if value_ids is None: + value_ids = self.value_ids.ids + + if custom_vals is None: + custom_vals = {} + + product_tmpl = self.product_tmpl_id + + self = self.with_context(active_id=product_tmpl.id) + + value_ids = flatten(value_ids) + + weight_extra = 0.0 + product_attr_val_obj = self.env["product.template.attribute.value"] + product_tmpl_attr_values = product_attr_val_obj.search( + [ + ("product_tmpl_id", "in", product_tmpl.ids), + ("product_attribute_value_id", "in", value_ids), + ] + ) + for product_tmpl_attr_val in product_tmpl_attr_values: + weight_extra += product_tmpl_attr_val.weight_extra + + return product_tmpl.weight + weight_extra + + @api.depends( + "value_ids", + "product_tmpl_id", + "product_tmpl_id.attribute_line_ids", + "product_tmpl_id.attribute_line_ids.value_ids", + "product_tmpl_id.attribute_line_ids.product_template_value_ids", + "product_tmpl_id.attribute_line_ids.product_template_value_ids" ".weight_extra", + ) + def _compute_cfg_weight(self): + for cfg_session in self: + cfg_session.weight = cfg_session.get_cfg_weight() + + def _compute_currency_id(self): + main_company = self.env["res.company"]._get_main_company() + for session in self: + template = session.product_tmpl_id + session.currency_id = ( + template.company_id.sudo().currency_id.id or main_company.currency_id.id + ) + + name = fields.Char(string="Configuration Session Number", readonly=True) + config_step = fields.Char(string="Configuration Step ID") + config_step_name = fields.Char( + compute="_compute_config_step_name", string="Configuration Step" + ) + product_id = fields.Many2one( + comodel_name="product.product", + name="Configured Variant", + ondelete="cascade", + ) + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + domain=[("config_ok", "=", True)], + string="Configurable Template", + required=True, + ) + value_ids = fields.Many2many( + comodel_name="product.attribute.value", + relation="product_config_session_attr_values_rel", + column1="cfg_session_id", + column2="attr_val_id", + ) + user_id = fields.Many2one(comodel_name="res.users", required=True, string="User") + custom_value_ids = fields.One2many( + comodel_name="product.config.session.custom.value", + inverse_name="cfg_session_id", + string="Custom Values", + ) + price = fields.Float( + compute="_compute_cfg_price", + store=True, + digits="Product Price", + ) + currency_id = fields.Many2one( + comodel_name="res.currency", + string="Currency", + compute="_compute_currency_id", + ) + state = fields.Selection( + required=True, + selection=[("draft", "Draft"), ("done", "Done")], + default="draft", + ) + weight = fields.Float(compute="_compute_cfg_weight", digits="Stock Weight") + # Product preset + 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): + for session in self: + if product_id is None: + product_id = session.create_get_variant() + session.write({"state": "done", "product_id": product_id.id}) + return True + + @api.constrains("state") + 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") + ) + + 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" + ) + + 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) + custom_field_name = custom_field_prefix + str(attr_id) + + if field_name not in vals and custom_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! + 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] + elif not attr_line.multi and isinstance(vals[field_name], int): + field_val = vals[field_name] + 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}) + # 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}) + + self.update_config(attr_val_dict, custom_val_dict) + + def update_config(self, attr_val_dict=None, custom_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 = {} + 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 x, attr_id=attr_id: x.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)]}) + + # Remove all custom values included in the custom_vals dict + self.custom_value_ids.filtered( + lambda x: x.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 write(self, vals): + """Validate configuration when writing new values to session""" + # TODO: Issue warning when writing to value_ids or custom_val_ids + res = super().write(vals) + if not self.product_tmpl_id: + return res + value_ids = self.value_ids.ids + avail_val_ids = self.values_available(value_ids) + if set(value_ids) - set(avail_val_ids): + self.value_ids = [(6, 0, avail_val_ids)] + try: + self.validate_configuration(final=False) + except ValidationError as exc: + raise ValidationError(_(f"{exc}")) from exc + except Exception as exc: + raise ValidationError(_("Invalid Configuration")) from exc + return res + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals["name"] = self.env["ir.sequence"].next_by_code( + "product.config.session" + ) or _("New") + product_tmpl = ( + self.env["product.template"] + .browse(vals.get("product_tmpl_id")) + .exists() + ) + if product_tmpl: + default_val_ids = ( + product_tmpl.attribute_line_ids.filtered( + lambda line: line.default_val + ) + .mapped("default_val") + .ids + ) + value_ids = vals.get("value_ids") + if value_ids: + default_val_ids += value_ids[0][2] + try: + self.validate_configuration( + value_ids=default_val_ids, + final=False, + product_tmpl_id=product_tmpl.id, + ) + # TODO: Remove if cond when PR with + # raise error on github is merged + except ValidationError as exc: + raise ValidationError(_("%s") % exc.name) from exc + except Exception as exc: + raise ValidationError( + _( + "Default values provided generate an invalid " + "configuration" + ) + ) from exc + vals.update({"value_ids": [(6, 0, default_val_ids)]}) + return super().create(vals_list) + + def create_get_variant(self, value_ids=None, custom_vals=None): + """Creates a new product variant with the attributes passed + via value_ids and custom_values or retrieves an existing + one based on search result + + :param value_ids: list of product.attribute.values ids + :param custom_vals: dict {product.attribute.id: custom_value} + + :returns: new/existing product.product recordset + + """ + if self.product_tmpl_id.config_ok: + self.validate_configuration() + if value_ids is None: + value_ids = self.value_ids.ids + + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + + try: + self.validate_configuration() + except ValidationError as exc: + raise ValidationError(_("%s") % exc.name) from exc + except Exception as exc: + raise ValidationError(_("Invalid Configuration")) from exc + + duplicates = self.search_variant( + value_ids=value_ids, product_tmpl_id=self.product_tmpl_id + ) + if duplicates: + return duplicates[:1] + + vals = self.get_variant_vals(value_ids, custom_vals) + product_obj = ( + self.env["product.product"].sudo().with_context(mail_create_nolog=True) + ) + variant = product_obj.sudo().create(vals) + + variant.message_post( + body=_("Product created via configuration wizard"), + author_id=self.env.user.partner_id.id, + ) + + return variant + + def _get_option_values(self, pricelist, value_ids=None): + """Return only attribute values that have products attached with a + price set to them""" + if value_ids is None: + value_ids = self.value_ids.ids + + value_obj = self.env["product.attribute.value"].with_context( + pricelist=pricelist.id + ) + values = ( + value_obj.sudo() + .browse(value_ids) + .filtered(lambda x: x.product_id._get_contextual_price()) + ) + return values + + def get_components_prices(self, prices, pricelist, value_ids=None): + """Return prices of the components which make up the final + configured variant""" + if value_ids is None: + value_ids = self.value_ids.ids + vals = self._get_option_values(pricelist, value_ids) + for val in vals: + prices["vals"].append( + ( + val.attribute_id.name, + val.product_id.name, + val.product_id._get_contextual_price(), + ) + ) + product = val.product_id.with_context(pricelist=pricelist.id) + product_prices = product.taxes_id.sudo().compute_all( + price_unit=product._get_contextual_price(), + currency=pricelist.currency_id, + quantity=1, + product=self, + partner=self.env.user.partner_id, + ) + + total_included = product_prices["total_included"] + taxes = total_included - product_prices["total_excluded"] + prices["taxes"] += taxes + prices["total"] += total_included + return prices + + @api.model + def get_cfg_price(self, value_ids=None, custom_vals=None): + """Computes the price of the configured product based on the + configuration passed in via value_ids and custom_values + + :param value_ids: list of attribute value_ids + :param custom_vals: dictionary of custom attribute values + :returns: final configuration price""" + + if value_ids is None: + value_ids = self.value_ids.ids + + if custom_vals is None: + custom_vals = {} + + product_tmpl = self.product_tmpl_id + self = self.with_context(active_id=product_tmpl.id) + + value_ids = self.flatten_val_ids(value_ids) + + price_extra = 0.0 + attr_val_obj = self.env["product.attribute.value"] + av_ids = attr_val_obj.browse(value_ids) + extra_prices = attr_val_obj.get_attribute_value_extra_prices( + product_tmpl_id=product_tmpl.id, pt_attr_value_ids=av_ids + ) + price_extra = sum(extra_prices.values()) + return product_tmpl.list_price + price_extra + + def _get_config_image(self, value_ids=None, custom_vals=None, size=None): + """ + Retreive the image object that most closely resembles the configuration + code sent via value_ids list + + The default image object is the template (self) + :param value_ids: a list representing the ids of attribute values + (usually stored in the user's session) + :param custom_vals: dictionary of custom attribute values + :returns: path to the selected image + """ + # TODO: Also consider custom values for image change + if value_ids is None: + value_ids = self.value_ids.ids + + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + + img_obj = self.product_tmpl_id + max_matches = 0 + value_ids = self.flatten_val_ids(value_ids) + for line in self.product_tmpl_id.config_image_ids: + matches = len(set(line.value_ids.ids) & set(value_ids)) + if matches > max_matches: + img_obj = line + max_matches = matches + return img_obj + + def get_config_image(self, value_ids=None, custom_vals=None, size=None): + """ + Retreive the image object that most closely resembles the configuration + code sent via value_ids list + For more information check _get_config_image + """ + config_image_id = self._get_config_image( + value_ids=value_ids, custom_vals=custom_vals + ) + return config_image_id.image_1920 + + @api.model + def get_variant_vals(self, value_ids=None, custom_vals=None, **kwargs): + """Hook to alter the values of the product variant before creation + + :param value_ids: list of product.attribute.values ids + :param custom_vals: dict {product.attribute.id: custom_value} + + :returns: dictionary of values to pass to product.create() method + """ + self.ensure_one() + + if value_ids is None: + value_ids = self.value_ids.ids + + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + + image = self.get_config_image(value_ids) + ptav_ids = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", self.product_tmpl_id.id), + ("product_attribute_value_id", "in", value_ids), + ] + ) + vals = { + "product_tmpl_id": self.product_tmpl_id.id, + "product_template_attribute_value_ids": [(6, 0, ptav_ids.ids)], + "taxes_id": [(6, 0, self.product_tmpl_id.taxes_id.ids)], + "image_1920": image, + } + return vals + + def get_session_search_domain(self, product_tmpl_id, state="draft", parent_id=None): + """Return domain to search session linked to given + product template and current login user""" + domain = [ + ("product_tmpl_id", "=", product_tmpl_id), + ("user_id", "=", self.env.uid), + ("state", "=", state), + ] + if parent_id: + domain.append(("parent_id", "=", parent_id)) + return domain + + def get_session_vals(self, product_tmpl_id, parent_id=None, user_id=None): + """Return the values for creating session""" + if not user_id: + user_id = self.env.user.id + vals = {"product_tmpl_id": product_tmpl_id, "user_id": user_id} + if parent_id: + vals.update(parent_id=parent_id) + return vals + + def get_next_step( + self, + state, + product_tmpl_id=False, + value_ids=False, + custom_value_ids=False, + ): + """Find and return next step if it exists. This usually + implies the next configuration step (if any) defined via the + config_step_line_ids on the product.template. + """ + + if not product_tmpl_id: + product_tmpl_id = self.product_tmpl_id + if value_ids is False: + value_ids = self.value_ids + if custom_value_ids is False: + custom_value_ids = self.custom_value_ids + if not state: + state = self.config_step + + cfg_step_lines = product_tmpl_id.config_step_line_ids + if not cfg_step_lines: + if (value_ids or custom_value_ids) and state != "select": + return False + elif not (value_ids or custom_value_ids) and state != "select": + raise UserError( + _( + "You must select at least one " + "attribute in order to configure a product" + ) + ) + else: + return "configure" + + 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) + ) + + session_config_step = self.config_step + if ( + session_config_step + and state != session_config_step + and session_config_step in open_step_lines + ): + next_step = self.config_step + else: + next_step = str(next_step.id) if next_step else None + if next_step: + pass + elif not (value_ids or custom_value_ids): + raise UserError( + _( + "You must select at least one " + "attribute in order to configure a product" + ) + ) + else: + return False + return next_step + + # TODO: Should be renamed to get_active_step_line + + @api.model + def get_active_step(self): + """Attempt to return product.config.step.line object that has the id + of the config session step stored as string""" + cfg_step_line_obj = self.env["product.config.step.line"] + + try: + cfg_step_line_id = int(self.config_step) + except ValueError: + cfg_step_line_id = None + + if cfg_step_line_id: + return cfg_step_line_obj.browse(cfg_step_line_id) + return cfg_step_line_obj + + @api.model + def get_open_step_lines(self, value_ids=None): + """ + Returns a recordset of configuration step lines open for access given + the configuration passed through value_ids + + e.g: Field A and B from configuration step 2 depend on Field C + from configuration step 1. Since fields A and B require action from + the previous step, configuration step 2 is deemed closed and redirect + is made for configuration step 1. + + :param value_ids: list of value.ids representing the + current configuration + :returns: recordset of accesible configuration steps + """ + + if value_ids is None: + value_ids = self.value_ids.ids + + open_step_lines = self.env["product.config.step.line"] + + for cfg_line in self.product_tmpl_id.config_step_line_ids: + for attr_line in cfg_line.attribute_line_ids: + available_vals = self.values_available( + attr_line.value_ids.ids, + value_ids, + product_template_attribute_line_id=attr_line.id, + ) + # TODO: Refactor when adding restriction to custom values + if available_vals or attr_line.custom: + open_step_lines |= cfg_line + break + + return open_step_lines.sorted() + + @api.model + def get_all_step_lines(self, product_tmpl_id=None): + """ + Returns a recordset of configuration step lines of product_tmpl_id + + :param product_tmpl_id: record-set of product.template + :returns: recordset of all configuration steps + """ + if not product_tmpl_id: + product_tmpl_id = self.product_tmpl_id + + open_step_lines = product_tmpl_id.config_step_line_ids + return open_step_lines.sorted() + + @api.model + def get_adjacent_steps(self, value_ids=None, active_step_line_id=None): + """Returns the previous and next steps given the configuration passed + via value_ids and the active step line passed via cfg_step_line_id.""" + + # If there is no open step return empty dictionary + + if value_ids is None: + value_ids = self.value_ids.ids + + if not active_step_line_id: + active_step_line_id = self.get_active_step().id + + config_step_lines = self.product_tmpl_id.config_step_line_ids + + if not config_step_lines: + return {} + + active_cfg_step_line = config_step_lines.filtered( + lambda line: line.id == active_step_line_id + ) + + open_step_lines = self.get_open_step_lines(value_ids) + + if not active_cfg_step_line: + return {"next_step": open_step_lines[0]} + + nr_steps = len(open_step_lines) + + adjacent_steps = {} + + for i, cfg_step in enumerate(open_step_lines): + if cfg_step == active_cfg_step_line: + adjacent_steps.update( + { + "next_step": None + if i + 1 == nr_steps + else open_step_lines[i + 1], + "previous_step": None if i == 0 else open_step_lines[i - 1], + } + ) + return adjacent_steps + + def check_and_open_incomplete_step(self, value_ids=None, custom_value_ids=None): + """Check and open incomplete step if any + :param value_ids: recordset of product.attribute.value + """ + if value_ids is None: + value_ids = self.value_ids + if custom_value_ids is None: + custom_value_ids = self.custom_value_ids + custom_attr_selected = custom_value_ids.mapped("attribute_id") + open_step_lines = self.get_open_step_lines() + step_to_open = False + for step in open_step_lines: + unset_attr_line = step.attribute_line_ids.filtered( + lambda attr_line: attr_line.required + and not any([value in value_ids for value in attr_line.value_ids]) + and not ( + attr_line.custom and attr_line.attribute_id in custom_attr_selected + ) + ) + check_val_ids = unset_attr_line.mapped("value_ids") + avail_val_ids = self.values_available( + check_val_ids.ids, + value_ids.ids, + product_tmpl_id=self.product_tmpl_id, + ) + if unset_attr_line and avail_val_ids: + step_to_open = step + break + if step_to_open: + return "%s" % (step_to_open.id) + return False + + @api.model + def get_variant_search_domain(self, product_tmpl_id, value_ids=None): + """Method called by search_variant used to search duplicates in the + database""" + + if value_ids is None: + value_ids = self.value_ids.ids + + domain = [ + ("product_tmpl_id", "=", product_tmpl_id.id), + ("config_ok", "=", True), + ] + pta_value_ids = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", product_tmpl_id.id), + ("product_attribute_value_id", "in", value_ids), + ] + ) + for value_id in pta_value_ids: + domain.append(("product_template_attribute_value_ids", "=", value_id.id)) + return domain + + def validate_domains_against_sels(self, domains, value_ids=None, custom_vals=None): + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + + if value_ids is None: + value_ids = self.value_ids.ids + + # process domains as shown in this wikipedia pseudocode: + # https://en.wikipedia.org/wiki/Polish_notation#Order_of_operations + stack = [] + for domain in reversed(domains): + if isinstance(domain, tuple): + # evaluate operand and push to stack + if domain[1] == "in": + if not set(domain[2]) & set(value_ids): + stack.append(False) + continue + else: + if set(domain[2]) & set(value_ids): + stack.append(False) + continue + stack.append(True) + else: + # evaluate operator and previous 2 operands + # compute_domain() only inserts 'or' operators + # compute_domain() enforces 2 operands per operator + operand1 = stack.pop() + operand2 = stack.pop() + stack.append(operand1 or operand2) + + # 'and' operator is implied for remaining stack elements + avail = True + while stack: + avail &= stack.pop() + return avail + + @api.model + def values_available( + self, + check_val_ids=None, + value_ids=None, + custom_vals=None, + product_tmpl_id=None, + product_template_attribute_line_id=None, + ): + """Determines whether the attr_values from the product_template + are available for selection given the configuration ids and the + dependencies set on the product template + + :param check_val_ids: list of attribute value ids to check for + availability + :param value_ids: list of attribute value ids + :param custom_vals: custom values dict {attr_id: custom_val} + + :returns: list of available attribute values + """ + if check_val_ids is None: + check_val_ids = self.value_ids.ids + elif check_val_ids: + check_val_ids = check_val_ids.copy() + if not self.product_tmpl_id: + product_tmpl = self.env["product.template"].browse(product_tmpl_id) + else: + product_tmpl = self.product_tmpl_id + + product_tmpl.ensure_one() + + if product_template_attribute_line_id is not None: + product_template_attribute_lines = self.env[ + "product.template.attribute.line" + ].browse(product_template_attribute_line_id) + else: + product_template_attribute_lines = product_tmpl.attribute_line_ids + + if value_ids is None: + value_ids = self.value_ids.ids + elif value_ids: + value_ids = value_ids.copy() + + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + + avail_val_ids = [] + for attr_val_id in check_val_ids: + config_lines = product_tmpl.config_line_ids.filtered( + lambda line, attr_val_id=attr_val_id: attr_val_id in line.value_ids.ids + ) + if product_template_attribute_lines: + config_lines = config_lines.filtered( + lambda line: line.attribute_line_id + in product_template_attribute_lines + ) + domains = config_lines.mapped("domain_id").compute_domain() + avail = self.validate_domains_against_sels(domains, value_ids, custom_vals) + if avail: + avail_val_ids.append(attr_val_id) + elif attr_val_id in value_ids: + value_ids.remove(attr_val_id) + + return avail_val_ids + + @api.model + def get_extra_attribute_line_ids(self, product_template_id): + """Retrieve attribute lines defined on the product_template_id + which are not assigned to configuration steps""" + + extra_attribute_line_ids = ( + product_template_id.attribute_line_ids + - product_template_id.config_step_line_ids.mapped("attribute_line_ids") + ) + return extra_attribute_line_ids + + def check_attributes_configuration( + self, attribute_line_ids, custom_vals, value_ids, final=True + ): + for line in attribute_line_ids: + # Validate custom values + attr = line.attribute_id + if attr.id in custom_vals: + attr.validate_custom_val(custom_vals[attr.id]) + if final: + line_values = line._configurator_value_ids() + common_vals = set(value_ids) & set(line_values.ids) + custom_val = custom_vals.get(attr.id) + avail_val_ids = self.values_available( + check_val_ids=line_values.ids, + value_ids=value_ids, + product_tmpl_id=self.product_tmpl_id, + product_template_attribute_line_id=line.id, + ) + if ( + line.required + and avail_val_ids + and not common_vals + and not custom_val + ): + # TODO: Verify custom value type to be correct + raise ValidationError( + _("Required attribute '%s' is empty", attr.name) + ) + + @api.model + def validate_configuration( + self, + value_ids=None, + custom_vals=None, + product_tmpl_id=False, + final=True, + ): + """Verifies if the configuration values passed via value_ids and + custom_vals are valid + + :param value_ids: list of attribute value ids + :param custom_vals: custom values dict {attr_id: custom_val} + :param final: boolean marker to check required attributes. + pass false to check non-final configurations + + :returns: Error dict with reason of validation failure + or True + """ + # TODO: Raise ConfigurationError with reason + # Check if required values are missing for final configuration + if value_ids is None: + value_ids = self.value_ids.ids + + if product_tmpl_id: + product_tmpl = self.env["product.template"].browse(product_tmpl_id) + else: + product_tmpl = self.product_tmpl_id + + product_tmpl.ensure_one() + + if custom_vals is None: + custom_vals = self._get_custom_vals_dict() + open_step_lines = self.get_open_step_lines() + attribute_line_ids = open_step_lines.mapped("attribute_line_ids") + attribute_line_ids += self.get_extra_attribute_line_ids( + product_template_id=product_tmpl + ) + self.check_attributes_configuration( + attribute_line_ids, custom_vals, value_ids, final=final + ) + + # Check if all the values passed are not restricted + avail_val_ids = self.values_available( + value_ids, value_ids, product_tmpl_id=product_tmpl_id + ) + if set(value_ids) - set(avail_val_ids): + restrict_val = list(set(value_ids) - set(avail_val_ids)) + product_att_values = self.env["product.attribute.value"].browse( + restrict_val + ) + group_by_attr = {} + for val in product_att_values: + if val.attribute_id in group_by_attr: + group_by_attr[val.attribute_id] += val + else: + group_by_attr[val.attribute_id] = val + + message = _("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) + + # Check if custom values are allowed + custom_attr_ids = ( + product_tmpl.attribute_line_ids.filtered("custom") + .mapped("attribute_id") + .ids + ) + if not set(custom_vals.keys()) <= set(custom_attr_ids): + custom_attrs_with_error = list( + set(custom_vals.keys()) - set(custom_attr_ids) + ) + custom_attrs_with_error = self.env["product.attribute"].browse( + custom_attrs_with_error + ) + error_message = _( + "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 " + "while your configuration was in process. Please reset your " + "current session and start over or contact your administrator" + " in order to proceed." + ) + message_vals = "" + for attr_id in custom_attrs_with_error: + message_vals += f"\n {attr_id.name}: {custom_vals.get(attr_id.id)}" + raise ValidationError(error_message % (message_vals)) + + # Check if there are multiple values passed for non-multi attributes + mono_attr_lines = product_tmpl.attribute_line_ids.filtered( + lambda line: not line.multi + ) + attrs_with_error = {} + for line in mono_attr_lines: + if len(set(line.value_ids.ids) & set(value_ids)) > 1: + wrong_vals = self.env["product.attribute.value"].browse( + set(line.value_ids.ids) & set(value_ids) + ) + attrs_with_error[line.attribute_id] = wrong_vals + if attrs_with_error: + error_message = _( + "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 " + "while your configuration was in process. Please reset your " + "current session and start over or contact your administrator" + " in order to proceed." + ) + message_vals = "" + for attr_id, vals in attrs_with_error.items(): + message_vals += "\n {}: {}".format( + attr_id.name, ", ".join(vals.mapped("name")) + ) + raise ValidationError(error_message % (message_vals)) + return True + + @api.model + def search_variant(self, value_ids=None, product_tmpl_id=None): + """Searches product.variants with given value_ids and custom values + given in the custom_vals dict + + :param value_ids: list of product.attribute.values ids + :param custom_vals: dict {product.attribute.id: custom_value} + + :returns: product.product recordset of products matching domain + """ + if value_ids is None: + value_ids = self.value_ids.ids + + custom_value_id = self.get_custom_value_id() + value_ids = list(set(value_ids) - set(custom_value_id.ids)) + + if not product_tmpl_id: + product_tmpl_id = self.product_tmpl_id + if not product_tmpl_id: + raise ValidationError( + _( + "Cannot conduct search on an empty config session " + "without product_tmpl_id kwarg" + ) + ) + + domain = self.get_variant_search_domain( + product_tmpl_id=product_tmpl_id, value_ids=value_ids + ) + products = self.env["product.product"].search(domain) + + # At this point, we might have found products with all of the passed + # in values, but it might have more attributes! These are NOT + # matches + more_attrs = products.filtered( + lambda p: len(p.product_template_attribute_value_ids) != len(value_ids) + ) + products -= more_attrs + return products + + def search_session(self, product_tmpl_id, parent_id=None): + domain = self.get_session_search_domain( + product_tmpl_id=product_tmpl_id, parent_id=parent_id + ) + session = self.search(domain, order="create_date desc", limit=1) + return session + + @api.model + def create_get_session( + self, product_tmpl_id, parent_id=None, force_create=False, user_id=None + ): + if not force_create: + session = self.search_session( + product_tmpl_id=product_tmpl_id, parent_id=parent_id + ) + if session: + return session + vals = self.get_session_vals( + product_tmpl_id=product_tmpl_id, + parent_id=parent_id, + user_id=user_id, + ) + 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) + + :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)) + return list(flat_val_ids) + + def formatPrices(self, prices=None, dp="Product Price"): + if prices is None: + prices = {} + dp = None + prices["taxes"] = formatLang(self.env, prices["taxes"], monetary=True, dp=dp) + prices["total"] = formatLang(self.env, prices["total"], monetary=True, dp=dp) + prices["vals"] = [ + (v[0], v[1], formatLang(self.env, v[2], monetary=True, dp=dp)) + for v in prices["vals"] + ] + return prices + + def encode_custom_values(self, custom_vals): + """Hook to alter the values of the custom values before creating + or writing + :param custom_vals: dict {product.attribute.id: custom_value} + :returns: list of custom values compatible with write and create + """ + attr_obj = self.env["product.attribute"] + binary_attribute_ids = attr_obj.search([("custom_type", "=", "binary")]).ids + custom_lines = [] + + for key, val in custom_vals.items(): + custom_vals = {"attribute_id": key} + # TODO: Is this extra check neccesairy as we already make + # the check in validate_configuration? + attr_obj.browse(key).validate_custom_val(val) + if key in binary_attribute_ids: + custom_vals.update({"attachment_ids": [(6, 0, val.ids)]}) + else: + custom_vals.update({"value": val}) + custom_lines.append((0, 0, custom_vals)) + return custom_lines + + @api.model + def get_child_specification(self, model, parent): + """return dictiory of onchange specification by + appending parent before each key""" + model_obj = self.env[model] + specs = model_obj._onchange_spec() + new_specs = {} + for key, val in specs.items(): + new_specs[f"{parent}.{key}"] = val + return new_specs + + @api.model + def get_onchange_specifications(self, model): + """return onchange specification + - same functionality by _onchange_spec + - needed this method because odoo don't add specification for fields + one2many or many2many there is view-reference(using : tree_view_ref) + intead of view in that field""" + model_obj = self.env[model] + specs = model_obj._onchange_spec() + for name, field in model_obj._fields.items(): + if field.type not in ["one2many", "many2many"]: + continue + ch_specs = self.get_child_specification( + model=field.comodel_name, parent=name + ) + specs.update(ch_specs) + return specs + + @api.model + def get_vals_to_write(self, values, model): + """Return values in formate excepted by write/create methods + - same functionality by _convert_to_write + - needed this method because odoo don't call convert to write + for the many2many/one2many fields""" + model_obj = self.env[model] + values = model_obj._convert_to_write(values) + fields = model_obj._fields + for key, vals in values.items(): + if not isinstance(vals, list): + continue + new_lst = [] + for line in vals: + new_line = line + if line and isinstance(line[-1], dict): + new_line = line[:-1] + ( + self.get_vals_to_write( + values=line[-1], model=fields[key].comodel_name + ), + ) + new_lst.append(new_line) + values[key] = new_lst + return values + + +class ProductConfigSessionCustomValue(models.Model): + _name = "product.config.session.custom.value" + _rec_name = "attribute_id" + _description = "Product Config Session Custom Value" + + @api.depends("attribute_id", "attribute_id.uom_id") + def _compute_val_name(self): + for attr_val_custom in self: + uom = attr_val_custom.attribute_id.uom_id.name + attr_val_custom.name = "{}{}".format( + attr_val_custom.value, + (" %s" % uom) or "", + ) + + name = fields.Char(readonly=True, compute="_compute_val_name", store=True) + attribute_id = fields.Many2one( + comodel_name="product.attribute", string="Attribute", required=True + ) + cfg_session_id = fields.Many2one( + comodel_name="product.config.session", + required=True, + ondelete="cascade", + string="Session", + ) + value = fields.Char(help="Custom value held as string") + attachment_ids = fields.Many2many( + comodel_name="ir.attachment", + relation="product_config_session_custom_value_attachment_rel", + column1="cfg_sesion_custom_val_id", + column2="attachment_id", + string="Attachments", + ) + + def eval(self): + """Return custom value evaluated using the related custom field type""" + field_type = self.attribute_id.custom_type + if field_type == "binary": + vals = self.attachment_ids.mapped("datas") + if len(vals) == 1: + return vals[0] + return vals + elif field_type == "integer": + return int(self.value) + elif field_type == "float": + return float(self.value) + return self.value + + @api.constrains("cfg_session_id", "attribute_id") + def unique_attribute(self): + for custom_val in self: + values = custom_val.cfg_session_id.custom_value_ids + if ( + len( + values.filtered( + lambda x, custom_val=custom_val: x.attribute_id + == custom_val.attribute_id + ) + ) + > 1 + ): + raise ValidationError( + _("Configuration cannot have the " "same value inserted twice") + ) + + # @api.constrains('cfg_session_id.value_ids') + # def custom_only(self): + # """Verify that the attribute_id is not present in vals as well""" + # import ipdb;ipdb.set_trace() + # if self.cfg_session_id.value_ids.filtered( + # lambda x: x.attribute_id == self.attribute_id): + # raise ValidationError( + # _("Configuration cannot have a selected option and a custom " + # "value with the same attribute") + # ) + + @api.constrains("attachment_ids", "value") + def check_custom_type(self): + for custom_val in self: + custom_type = custom_val.attribute_id.custom_type + if custom_val.value and custom_type == "binary": + raise ValidationError( + _( + "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( + _( + "Attribute custom type must be 'binary' for saving " + "attachments to custom value" + ) + ) diff --git a/product_configurator/readme/CONTRIBUTORS.rst b/product_configurator/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..6afa1541b --- /dev/null +++ b/product_configurator/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Aion Tech `_: + + * Simone Rubino diff --git a/product_configurator/readme/DESCRIPTION.rst b/product_configurator/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a0b4c6515 --- /dev/null +++ b/product_configurator/readme/DESCRIPTION.rst @@ -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/security/configurator_security.xml b/product_configurator/security/configurator_security.xml new file mode 100644 index 000000000..8d7f835f0 --- /dev/null +++ b/product_configurator/security/configurator_security.xml @@ -0,0 +1,38 @@ + + + + + Product Configurator + + + + User + + + + + + Manager + + + + + + + + + + + + + + diff --git a/product_configurator/security/ir.model.access.csv b/product_configurator/security/ir.model.access.csv new file mode 100644 index 000000000..d9bfcf086 --- /dev/null +++ b/product_configurator/security/ir.model.access.csv @@ -0,0 +1,40 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +product_configurator_config_line,Config Line,model_product_config_line,group_product_configurator,1,0,0,0 +product_configurator_config_image,Config Image,model_product_config_image,group_product_configurator,1,0,0,0 +product_configurator_config_step,Config Step,model_product_config_step,group_product_configurator,1,0,0,0 +product_configurator_config_step_line,Config Step Line,model_product_config_step_line,group_product_configurator,1,0,0,0 +product_configurator_config_domain,Config Domain,model_product_config_domain,group_product_configurator,1,0,0,0 +product_configurator_config_domain_line,Config Domain Line,model_product_config_domain_line,group_product_configurator,1,0,0,0 +product_configurator_custom_attribute_value,Attribute Value Line,model_product_attribute_value_line,group_product_configurator,1,0,0,0 +product_configurator_config_session,Config Session,model_product_config_session,group_product_configurator,1,1,1,1 +product_configurator_config_session_custom_value,Config Session Custom Value,model_product_config_session_custom_value,group_product_configurator,1,1,1,1 +user_config_line,User Config Line,model_product_config_line,base.group_user,1,0,0,0 +user_config_image,User Config Image,model_product_config_image,base.group_user,1,0,0,0 +user_config_step,User Config Step,model_product_config_step,base.group_user,1,0,0,0 +user_config_step_line,User Config Step Line,model_product_config_step_line,base.group_user,1,0,0,0 +user_config_domain_line,User Config Domain Line,model_product_config_domain_line,base.group_user,1,0,0,0 +user_config_domain,User Config Domain,model_product_config_domain,base.group_user,1,0,0,0 +user_custom_attribute_value,User Attribute Value Line,model_product_attribute_value_line,base.group_user,1,0,0,0 +user_config_session,User Config Session,model_product_config_session,base.group_user,1,0,0,0 +user_config_session_custom_value,User Config Session Custom Value,model_product_config_session_custom_value,base.group_user,1,0,0,0 +portal_config_image,Portal Config Image,model_product_config_image,base.group_portal,1,0,0,0 +portal_config_step,Portal Config Step,model_product_config_step,base.group_portal,1,0,0,0 +portal_config_session,Portal Config Session,model_product_config_session,base.group_portal,1,0,0,0 +portal_config_session_custom_value,Portal Config Session Custom Value,model_product_config_session_custom_value,base.group_portal,1,0,0,0 +portal_configurator_config_line,Portal Config Line,model_product_config_line,base.group_portal,1,0,0,0 +portal_configurator_config_step_line,Portal Config Step Line,model_product_config_step_line,base.group_portal,1,0,0,0 +portal_configurator_config_domain,Portal Config Domain,model_product_config_domain,base.group_portal,1,0,0,0 +portal_configurator_config_domain_line,Portal Config Domain Line,model_product_config_domain_line,base.group_portal,1,0,0,0 +product_configurator_config_line_manager,Config Line Manager,product_configurator.model_product_config_line,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_config_image_manager,Config Image Manager,product_configurator.model_product_config_image,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_config_step_manager,Config Step Manager,product_configurator.model_product_config_step,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_config_step_line_manager,Config Step Line Manager,product_configurator.model_product_config_step_line,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_config_domain_manager,Config Domain Manager,product_configurator.model_product_config_domain,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_config_domain_line_manager,Config Domain Line Manager,product_configurator.model_product_config_domain_line,product_configurator.group_product_configurator_manager,1,1,1,1 +product_configurator_custom_attribute_value_manager,Attribute Value Line Manager,product_configurator.model_product_attribute_value_line,product_configurator.group_product_configurator_manager,1,1,1,1 +access_product_template_product_config_user,product.template Product Config user,product.model_product_template,product_configurator.group_product_configurator,1,0,0,0 +access_product_template_product_config_manager,product.template Product Config Manager,product.model_product_template,product_configurator.group_product_configurator_manager,1,1,1,1 +access_product_product_product_config_user,product.product Product Config user,product.model_product_product,product_configurator.group_product_configurator,1,0,0,0 +access_product_product_product_config_manager,product.product Product Config Manager,product.model_product_product,product_configurator.group_product_configurator_manager,1,1,1,1 +access_product_attribute_line_product_config_manager,product.attribute line Product Config Manager,product.model_product_template_attribute_line,product_configurator.group_product_configurator_manager,1,1,1,1 +access_product_configurator_group,product_configurator,model_product_configurator,product_configurator.group_product_configurator,1,1,1,1 diff --git a/product_configurator/static/description/configurable-template.png b/product_configurator/static/description/configurable-template.png new file mode 100644 index 000000000..860c818ea Binary files /dev/null and b/product_configurator/static/description/configurable-template.png differ diff --git a/product_configurator/static/description/cover.png b/product_configurator/static/description/cover.png new file mode 100644 index 000000000..be5e2578c Binary files /dev/null and b/product_configurator/static/description/cover.png differ diff --git a/product_configurator/static/description/icon.png b/product_configurator/static/description/icon.png new file mode 100644 index 000000000..d5d307c64 Binary files /dev/null and b/product_configurator/static/description/icon.png differ diff --git a/product_configurator/static/description/index.html b/product_configurator/static/description/index.html new file mode 100644 index 000000000..b30bc6217 --- /dev/null +++ b/product_configurator/static/description/index.html @@ -0,0 +1,75 @@ +
+
+
+

Odoo Product Configurator

+

Generate products on-demand, easy and error-free

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

Select your template

+
+
+ +
+
+
+
+ +
+
+

Choose your options and get live image updates

+
+
+ +
+
+
+
+ +
+
+

Custom values and multiple selections supported

+
+
+ +
+
+
+
+ +
+
+

The wizard is generated automatically for you!

+

Just define the attributes and rules on the template and you are done

+
+
+ +
+
+
+
+ +
+
+

Compatible with Odoo Enterprise and Community

+

Odoo versions supported: 8 / 9 / 10

+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/product_configurator/static/description/odoo-community-interface.png b/product_configurator/static/description/odoo-community-interface.png new file mode 100644 index 000000000..e741d6153 Binary files /dev/null and b/product_configurator/static/description/odoo-community-interface.png differ diff --git a/product_configurator/static/description/odoo-enterprise-interface.png b/product_configurator/static/description/odoo-enterprise-interface.png new file mode 100644 index 000000000..5b13b3dd1 Binary files /dev/null and b/product_configurator/static/description/odoo-enterprise-interface.png differ diff --git a/product_configurator/static/description/pledra-logo.png b/product_configurator/static/description/pledra-logo.png new file mode 100644 index 000000000..b82d4ab00 Binary files /dev/null and b/product_configurator/static/description/pledra-logo.png differ diff --git a/product_configurator/static/description/quotation-updated.png b/product_configurator/static/description/quotation-updated.png new file mode 100644 index 000000000..8b0f5aafb Binary files /dev/null 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 new file mode 100644 index 000000000..8ed801383 Binary files /dev/null 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 new file mode 100644 index 000000000..396e046b7 Binary files /dev/null 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 new file mode 100644 index 000000000..3d89020dc Binary files /dev/null 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 new file mode 100644 index 000000000..948ce549b Binary files /dev/null and b/product_configurator/static/description/wizard-template.png differ diff --git a/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg new file mode 100644 index 000000000..a20f55525 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg new file mode 100644 index 000000000..f7fa6043e Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-black.jpg b/product_configurator/static/img/2-series-coupe-black.jpg new file mode 100644 index 000000000..77584cb03 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg new file mode 100644 index 000000000..47b2937e3 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg new file mode 100644 index 000000000..92d4f1ab7 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg new file mode 100644 index 000000000..5e747277e Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg new file mode 100644 index 000000000..b62f75fce Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg differ diff --git a/product_configurator/static/img/2-series-coupe-silver.jpg b/product_configurator/static/img/2-series-coupe-silver.jpg new file mode 100644 index 000000000..2c5e003a5 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver.jpg differ diff --git a/product_configurator/static/img/2-series-coupe.jpg b/product_configurator/static/img/2-series-coupe.jpg new file mode 100644 index 000000000..0e4b3df72 Binary files /dev/null and b/product_configurator/static/img/2-series-coupe.jpg differ diff --git a/product_configurator/static/img/product-advantage.jpg b/product_configurator/static/img/product-advantage.jpg new file mode 100644 index 000000000..60a6e1c1c Binary files /dev/null and b/product_configurator/static/img/product-advantage.jpg differ diff --git a/product_configurator/static/img/product-armrest.jpg b/product_configurator/static/img/product-armrest.jpg new file mode 100644 index 000000000..97ab6c955 Binary files /dev/null and b/product_configurator/static/img/product-armrest.jpg differ diff --git a/product_configurator/static/img/product-engine.jpg b/product_configurator/static/img/product-engine.jpg new file mode 100644 index 000000000..2f1b6f6be Binary files /dev/null and b/product_configurator/static/img/product-engine.jpg differ diff --git a/product_configurator/static/img/product-luxury-line.jpg b/product_configurator/static/img/product-luxury-line.jpg new file mode 100644 index 000000000..0fd9248e5 Binary files /dev/null and b/product_configurator/static/img/product-luxury-line.jpg differ diff --git a/product_configurator/static/img/product-m-sport.jpg b/product_configurator/static/img/product-m-sport.jpg new file mode 100644 index 000000000..0cfc029d9 Binary files /dev/null and b/product_configurator/static/img/product-m-sport.jpg differ diff --git a/product_configurator/static/img/product-paint-silver.jpg b/product_configurator/static/img/product-paint-silver.jpg new file mode 100644 index 000000000..b49c8619d Binary files /dev/null and b/product_configurator/static/img/product-paint-silver.jpg differ diff --git a/product_configurator/static/img/product-smoker-package.jpg b/product_configurator/static/img/product-smoker-package.jpg new file mode 100644 index 000000000..8cdaf3741 Binary files /dev/null and b/product_configurator/static/img/product-smoker-package.jpg differ diff --git a/product_configurator/static/img/product-sport-line.jpg b/product_configurator/static/img/product-sport-line.jpg new file mode 100644 index 000000000..7f18487aa Binary files /dev/null and b/product_configurator/static/img/product-sport-line.jpg differ diff --git a/product_configurator/static/img/product-sunroof.jpg b/product_configurator/static/img/product-sunroof.jpg new file mode 100644 index 000000000..11c5c8758 Binary files /dev/null and b/product_configurator/static/img/product-sunroof.jpg differ diff --git a/product_configurator/static/img/product-towhook.jpg b/product_configurator/static/img/product-towhook.jpg new file mode 100644 index 000000000..2f4d10f8b Binary files /dev/null and b/product_configurator/static/img/product-towhook.jpg differ diff --git a/product_configurator/static/img/product-transmission-steptronic-sport.jpg b/product_configurator/static/img/product-transmission-steptronic-sport.jpg new file mode 100644 index 000000000..84e495630 Binary files /dev/null and b/product_configurator/static/img/product-transmission-steptronic-sport.jpg differ diff --git a/product_configurator/static/img/product-transmission-steptronic.jpg b/product_configurator/static/img/product-transmission-steptronic.jpg new file mode 100644 index 000000000..a46a0f7ff Binary files /dev/null and b/product_configurator/static/img/product-transmission-steptronic.jpg 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 new file mode 100644 index 000000000..0973764ba --- /dev/null +++ b/product_configurator/static/src/js/boolean_button_widget.esm.js @@ -0,0 +1,58 @@ +/** @odoo-module **/ +const {onMounted, onRendered, useRef, useState} = owl; +import {BooleanField} from "@web/views/fields/boolean/boolean_field"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; + +export class BooleanButtonField extends BooleanField { + 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.props.activeString + : this.props.inactiveString; + this.hover = this.props.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 = $("") + .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 = { + ...standardFieldProps, + activeString: {type: String, optional: true}, + inactiveString: {type: String, optional: true}, +}; + +BooleanButtonField.extractProps = ({attrs}) => { + return { + activeString: attrs.options.active, + inactiveString: attrs.options.inactive, + }; +}; + +BooleanButtonField.template = "product_configurator.BooleanButtonField"; +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 new file mode 100644 index 000000000..b1d6447d5 --- /dev/null +++ b/product_configurator/static/src/js/boolean_button_widget.xml @@ -0,0 +1,16 @@ + + + + +
+ +
+
+ +
diff --git a/product_configurator/static/src/js/form_widgets.js b/product_configurator/static/src/js/form_widgets.js new file mode 100644 index 000000000..b45e17f26 --- /dev/null +++ b/product_configurator/static/src/js/form_widgets.js @@ -0,0 +1,79 @@ +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/relational_fields.js b/product_configurator/static/src/js/relational_fields.js new file mode 100644 index 000000000..cabb0950e --- /dev/null +++ b/product_configurator/static/src/js/relational_fields.js @@ -0,0 +1,26 @@ +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/static/src/scss/form_widget.scss b/product_configurator/static/src/scss/form_widget.scss new file mode 100644 index 000000000..7a9323bc1 --- /dev/null +++ b/product_configurator/static/src/scss/form_widget.scss @@ -0,0 +1,20 @@ +.oe_stat_button { + &:hover { + .o_boolean_button.o_not_hover { + display: none; + } + .o_boolean_button.o_hover { + display: inline-block !important; + } + } +} + +.oe_prod_config_image { + img { + min-height: 30px; + } +} + +.pull-right { + float: right; +} diff --git a/product_configurator/tests/__init__.py b/product_configurator/tests/__init__.py new file mode 100644 index 000000000..9991dba49 --- /dev/null +++ b/product_configurator/tests/__init__.py @@ -0,0 +1,6 @@ +from . import test_create +from . import test_configuration_rules +from . import test_product +from . import test_product_attribute +from . import test_product_config +from . import test_wizard diff --git a/product_configurator/tests/common.py b/product_configurator/tests/common.py new file mode 100644 index 000000000..d4adb4e44 --- /dev/null +++ b/product_configurator/tests/common.py @@ -0,0 +1,116 @@ +from odoo.addons.base.tests.common import BaseCommon + + +class ProductConfiguratorTestCases(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ProductConfWizard = cls.env["product.configurator"] + cls.config_product = cls.env.ref("product_configurator.bmw_2_series") + cls.product_category = cls.env.ref("product.product_category_5") + # attributes + cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel") + cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine") + cls.attr_color = cls.env.ref("product_configurator.product_attribute_color") + cls.attr_rims = cls.env.ref("product_configurator.product_attribute_rims") + cls.attr_model_line = cls.env.ref( + "product_configurator.product_attribute_model_line" + ) + cls.attr_tapistry = cls.env.ref( + "product_configurator.product_attribute_tapistry" + ) + cls.attr_transmission = cls.env.ref( + "product_configurator.product_attribute_transmission" + ) + cls.attr_options = cls.env.ref("product_configurator.product_attribute_options") + + # values + cls.value_gasoline = cls.env.ref( + "product_configurator.product_attribute_value_gasoline" + ) + cls.value_218i = cls.env.ref( + "product_configurator.product_attribute_value_218i" + ) + cls.value_220i = cls.env.ref( + "product_configurator.product_attribute_value_220i" + ) + cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red") + cls.value_rims_378 = cls.env.ref( + "product_configurator.product_attribute_value_rims_378" + ) + cls.value_sport_line = cls.env.ref( + "product_configurator.product_attribute_value_sport_line" + ) + cls.value_model_sport_line = cls.env.ref( + "product_configurator.product_attribute_value_model_sport_line" + ) + cls.value_tapistry = cls.env.ref( + "product_configurator.product_attribute_value_tapistry" + "_oyster_black" + ) + cls.value_transmission = cls.env.ref( + "product_configurator.product_attribute_value_steptronic" + ) + cls.value_options_1 = cls.env.ref( + "product_configurator.product_attribute_value_smoker_package" + ) + cls.value_options_2 = cls.env.ref( + "product_configurator.product_attribute_value_sunroof" + ) + + @classmethod + def _configure_product_nxt_step(cls): + product_config_wizard = cls.ProductConfWizard.create( + { + "product_tmpl_id": cls.config_product.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_fuel.id}": cls.value_gasoline.id, + f"__attribute_{cls.attr_engine.id}": cls.value_218i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_color.id}": cls.value_red.id, + f"__attribute_{cls.attr_rims.id}": cls.value_rims_378.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_model_line.id}": cls.value_sport_line.id, + } + ) + product_config_wizard.action_previous_step() + product_config_wizard.action_previous_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_engine.id}": cls.value_220i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.action_next_step() + vals = { + f"__attribute_{cls.attr_model_line.id}": cls.value_model_sport_line.id, + } + product_config_wizard.write(vals) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_tapistry.id}": cls.value_tapistry.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{cls.attr_transmission.id}": cls.value_transmission.id, + f"__attribute_{cls.attr_options.id}": [ + [6, 0, [cls.value_options_2.id]] + ], + } + ) + + return product_config_wizard.action_next_step() diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py new file mode 100644 index 000000000..7419a3fc5 --- /dev/null +++ b/product_configurator/tests/test_configuration_rules.py @@ -0,0 +1,324 @@ +# Copyright 2024 Simone Rubino - Aion Tech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +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.tools.safe_eval import safe_eval + + +class ConfigurationRules(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # The product attribute view only shows configuration fields + # (such as `val_custom`) + # when called with a specific context + # that is set by this action + configuration_attributes_action = cls.env.ref( + "product_configurator.action_attributes_view" + ) + action_eval_context = configuration_attributes_action._get_eval_context() + configuration_attribute_context = safe_eval( + configuration_attributes_action.context, globals_dict=action_eval_context + ) + configuration_attribute_model = cls.env["product.attribute"].with_context( + **configuration_attribute_context + ) + + cls.generic_custom_attribute_value = cls.env.ref( + "product_configurator.custom_attribute_value" + ) + + custom_attribute_form = Form(configuration_attribute_model) + custom_attribute_form.name = "Test custom attribute" + with custom_attribute_form.value_ids.new() as value: + value.name = "Test custom value" + custom_attribute_form.val_custom = True + cls.custom_attribute = custom_attribute_form.save() + cls.custom_attribute_value = cls.custom_attribute.value_ids + + other_custom_attribute_form = Form(configuration_attribute_model) + other_custom_attribute_form.name = "Test other custom attribute" + other_custom_attribute_form.val_custom = True + with other_custom_attribute_form.value_ids.new() as value: + value.name = "Test other custom value" + cls.other_custom_attribute = other_custom_attribute_form.save() + cls.other_custom_attribute_value = cls.other_custom_attribute.value_ids + + regular_attribute_form = Form(configuration_attribute_model) + regular_attribute_form.name = "Test regular attribute" + regular_attribute_form.val_custom = False + with regular_attribute_form.value_ids.new() as value: + value.name = "Test value 1" + with regular_attribute_form.value_ids.new() as value: + value.name = "Test value 2" + cls.regular_attribute = regular_attribute_form.save() + cls.regular_attribute_value_1 = first(cls.regular_attribute.value_ids) + cls.regular_attribute_value_2 = ( + cls.regular_attribute.value_ids - cls.regular_attribute_value_1 + ) + + config_domain_form = Form(cls.env["product.config.domain"]) + config_domain_form.name = "Regular attribute has value 1" + with config_domain_form.domain_line_ids.new() as line: + line.attribute_id = cls.regular_attribute + line.condition = "in" + line.value_ids.add(cls.regular_attribute_value_1) + regular_has_value_1_domain = config_domain_form.save() + + product_template_form = Form(cls.env["product.template"]) + product_template_form.name = "Test configurable product" + with product_template_form.attribute_line_ids.new() as regular_line: + regular_line.attribute_id = cls.regular_attribute + for attribute_value in cls.regular_attribute.value_ids: + regular_line.value_ids.add(attribute_value) + with product_template_form.attribute_line_ids.new() as custom_line: + custom_line.attribute_id = cls.custom_attribute + for attribute_value in cls.custom_attribute.value_ids: + custom_line.value_ids.add(attribute_value) + with product_template_form.attribute_line_ids.new() as other_custom_line: + other_custom_line.attribute_id = cls.other_custom_attribute + for attribute_value in cls.other_custom_attribute.value_ids: + other_custom_line.value_ids.add(attribute_value) + product_template = product_template_form.save() + product_template.config_ok = True + # When the regular attribute has value 1, + # the custom attribute must have the generic custom value. + # The other custom attribute id not restricted. + with Form(product_template) as product_template_form: + with product_template_form.config_line_ids.new() as restriction: + restriction.attribute_line_id = ( + product_template.attribute_line_ids.filtered( + lambda al: al.attribute_id == cls.custom_attribute + ) + ) + restriction.value_ids.add(cls.generic_custom_attribute_value) + restriction.domain_id = regular_has_value_1_domain + + cls.product_template = product_template + + def setUp(self): + super().setUp() + + self.cfg_tmpl = self.env.ref("product_configurator.bmw_2_series") + self.cfg_session = self.env["product.config.session"].create( + {"product_tmpl_id": self.cfg_tmpl.id, "user_id": SUPERUSER_ID} + ) + + attribute_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids") + self.attr_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids") + + self.attr_val_ext_ids = { + v: k for k, v in attribute_vals.get_external_id().items() + } + + def get_attr_val_ids(self, ext_ids): + """Return a list of database ids using the external_ids + passed via ext_ids argument""" + + value_ids = [] + + attr_val_prefix = "product_configurator.product_attribute_value_%s" + + for ext_id in ext_ids: + if ext_id in self.attr_val_ext_ids: + value_ids.append(self.attr_val_ext_ids[ext_id]) + elif attr_val_prefix % ext_id in self.attr_val_ext_ids: + value_ids.append(self.attr_val_ext_ids[attr_val_prefix % ext_id]) + + return value_ids + + def test_valid_configuration(self): + """Test validation of a valid configuration""" + + conf = [ + "gasoline", + "228i", + "model_luxury_line", + "silver", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + + attr_val_ids = self.get_attr_val_ids(conf) + validation = self.cfg_session.validate_configuration(attr_val_ids) + self.assertTrue(validation, "Valid configuration failed validation") + + def test_invalid_configuration(self): + conf = [ + "diesel", + "228i", + "model_luxury_line", + "silver", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + + attr_val_ids = self.get_attr_val_ids(conf) + with self.assertRaises(ValidationError): + self.cfg_session.validate_configuration(attr_val_ids) + + def test_missing_val_configuration(self): + conf = [ + "diesel", + "228i", + "model_luxury_line", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + + attr_val_ids = self.get_attr_val_ids(conf) + with self.assertRaises(ValidationError): + self.cfg_session.validate_configuration(attr_val_ids) + + def test_invalid_multi_configuration(self): + conf = [ + "gasoline", + "228i", + "model_luxury_line", + "silver", + "red", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + + attr_val_ids = self.get_attr_val_ids(conf) + with self.assertRaises(ValidationError): + self.cfg_session.validate_configuration(attr_val_ids) + + def test_invalid_custom_value_configuration(self): + conf = [ + "gasoline", + "228i", + "model_luxury_line", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + + attr_color_id = self.env.ref("product_configurator.product_attribute_color") + + custom_vals = {attr_color_id: {"value": "#fefefe"}} + + attr_val_ids = self.get_attr_val_ids(conf) + with self.assertRaises(ValidationError): + self.cfg_session.validate_configuration(attr_val_ids, custom_vals) + + def test_filled_custom_value(self): + """When custom values are restricted, + filling them correctly creates a valid configuration.""" + # Arrange + generic_custom_attribute_value = self.generic_custom_attribute_value + custom_attribute = self.custom_attribute + custom_value = 5 + other_custom_attribute = self.other_custom_attribute + other_custom_attribute_value = self.other_custom_attribute_value + regular_attribute = self.regular_attribute + regular_attribute_value_1 = self.regular_attribute_value_1 + product_template = self.product_template + + wizard_action = product_template.configure_product() + wizard = self.env[wizard_action["res_model"]].browse(wizard_action["res_id"]) + wizard.action_next_step() + fields_prefixes = wizard._prefixes + field_prefix = fields_prefixes.get("field_prefix") + custom_field_prefix = fields_prefixes.get("custom_field_prefix") + # Regular attribute has value 1 + # so the custom attribute must have the generic custom value. + # The other custom attribute can have any value. + wizard.write( + { + field_prefix + str(regular_attribute.id): regular_attribute_value_1.id, + field_prefix + + str(custom_attribute.id): generic_custom_attribute_value.id, + custom_field_prefix + str(custom_attribute.id): custom_value, + field_prefix + + str(other_custom_attribute.id): other_custom_attribute_value.id, + } + ) + # pre-condition + self.assertEqual(wizard.state, "configure") + + # Act + wizard.action_config_done() + + # Assert + config = wizard.config_session_id + self.assertEqual(config.state, "done") + + def test_fill_restricted_custom_value(self): + """When custom values are restricted, + filling them with the wrong value creates an invalid configuration.""" + # Arrange + generic_custom_attribute_value = self.generic_custom_attribute_value + custom_attribute = self.custom_attribute + custom_value = 5 + other_custom_attribute = self.other_custom_attribute + other_custom_attribute_value = self.other_custom_attribute_value + regular_attribute = self.regular_attribute + regular_attribute_value_2 = self.regular_attribute_value_2 + product_template = self.product_template + + wizard_action = product_template.configure_product() + wizard = self.env[wizard_action["res_model"]].browse(wizard_action["res_id"]) + wizard.action_next_step() + fields_prefixes = wizard._prefixes + field_prefix = fields_prefixes.get("field_prefix") + custom_field_prefix = fields_prefixes.get("custom_field_prefix") + # Regular attribute has value 2 + # so the custom attribute cannot have the generic custom value. + # The other custom attribute can have any value. + regular_attribute_field_name = field_prefix + str(regular_attribute.id) + custom_attribute_field_name = field_prefix + str(custom_attribute.id) + other_custom_attribute_field_name = field_prefix + str( + other_custom_attribute.id + ) + wizard_values = { + regular_attribute_field_name: regular_attribute_value_2.id, + custom_attribute_field_name: generic_custom_attribute_value.id, + custom_field_prefix + str(custom_attribute.id): custom_value, + other_custom_attribute_field_name: other_custom_attribute_value.id, + } + + # Act + onchange_result = wizard.onchange( + { + "value_ids": [ + Command.set([wizard_values[regular_attribute_field_name]]), + ], + **{wiz_field: False for wiz_field in wizard_values.keys()}, + }, + regular_attribute_field_name, + { + regular_attribute_field_name: "1", + }, + ) + + # Assert + domains = onchange_result["domain"] + custom_attribute_domain = domains[custom_attribute_field_name] + self.assertNotIn( + generic_custom_attribute_value, + self.env["product.attribute.value"].search(custom_attribute_domain), + ) + other_custom_attribute_domain = domains[other_custom_attribute_field_name] + self.assertIn( + generic_custom_attribute_value, + self.env["product.attribute.value"].search(other_custom_attribute_domain), + ) diff --git a/product_configurator/tests/test_create.py b/product_configurator/tests/test_create.py new file mode 100644 index 000000000..fe6bdf1b0 --- /dev/null +++ b/product_configurator/tests/test_create.py @@ -0,0 +1,184 @@ +from odoo.addons.base.tests.common import BaseCommon + + +class ConfigurationCreate(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ProductConfWizard = cls.env["product.configurator"] + cls.config_product = cls.env.ref("product_configurator.bmw_2_series") + cls.product_category = cls.env.ref("product.product_category_5") + + # attributes + cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel") + cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine") + cls.attr_color = cls.env.ref("product_configurator.product_attribute_color") + cls.attr_rims = cls.env.ref("product_configurator.product_attribute_rims") + cls.attr_model_line = cls.env.ref( + "product_configurator.product_attribute_model_line" + ) + cls.attr_tapistry = cls.env.ref( + "product_configurator.product_attribute_tapistry" + ) + cls.attr_transmission = cls.env.ref( + "product_configurator.product_attribute_transmission" + ) + cls.attr_options = cls.env.ref("product_configurator.product_attribute_options") + + # values + cls.value_gasoline = cls.env.ref( + "product_configurator.product_attribute_value_gasoline" + ) + cls.value_218i = cls.env.ref( + "product_configurator.product_attribute_value_218i" + ) + cls.value_220i = cls.env.ref( + "product_configurator.product_attribute_value_220i" + ) + cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red") + cls.value_rims_378 = cls.env.ref( + "product_configurator.product_attribute_value_rims_378" + ) + cls.value_sport_line = cls.env.ref( + "product_configurator.product_attribute_value_sport_line" + ) + cls.value_model_sport_line = cls.env.ref( + "product_configurator.product_attribute_value_model_sport_line" + ) + cls.value_tapistry = cls.env.ref( + "product_configurator.product_attribute_value_tapistry" + "_oyster_black" + ) + cls.value_transmission = cls.env.ref( + "product_configurator.product_attribute_value_steptronic" + ) + cls.value_options_1 = cls.env.ref( + "product_configurator.product_attribute_value_smoker_package" + ) + cls.value_options_2 = cls.env.ref( + "product_configurator.product_attribute_value_sunroof" + ) + + def test_01_create(self): + """Test configuration item does not make variations""" + + attr_test = self.env["product.attribute"].create( + { + "name": "Test", + "value_ids": [ + (0, 0, {"name": "1"}), + (0, 0, {"name": "2"}), + ], + } + ) + + test_template = self.env["product.template"].create( + { + "name": "Test Configuration", + "config_ok": True, + "type": "consu", + "categ_id": self.product_category.id, + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": attr_test.id, + "value_ids": [ + (6, 0, attr_test.value_ids.ids), + ], + "required": True, + }, + ), + ], + } + ) + + self.assertEqual( + len(test_template.product_variant_ids), + 0, + "Create should not have any variants", + ) + + def test_02_previous_step_incompatible_changes(self): + """Test changes in previous steps which would makes + values in next configuration steps invalid""" + + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.config_product.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + f"__attribute_{self.attr_rims.id}": self.value_rims_378.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id, + } + ) + product_config_wizard.action_previous_step() + product_config_wizard.action_previous_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_engine.id}": self.value_220i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.action_next_step() + vals = { + f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id, + } + product_config_wizard.write(vals) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_transmission.id}": self.value_transmission.id, + f"__attribute_{self.attr_options.id}": [ + [6, 0, [self.value_options_1.id, self.value_options_2.id]] + ], + } + ) + product_config_wizard.action_next_step() + value_ids = ( # noqa + self.value_gasoline + + self.value_220i + + self.value_red + + self.value_rims_378 + + self.value_model_sport_line + + self.value_tapistry + + self.value_transmission + + self.value_options_1 + + self.value_options_2 + ) + # FIXME: broken as + # """ + # AttributeError: 'product.product' object + # has no attribute 'attribute_value_ids'. + # Did you mean: 'attribute_line_ids'? + # """ + # new_variant = self.config_product.product_variant_ids.filtered( + # lambda variant: variant.attribute_value_ids == value_ids + # ) + # self.assertNotEqual( + # new_variant.id, + # False, + # "Variant not generated at the end of the configuration process", + # ) diff --git a/product_configurator/tests/test_product.py b/product_configurator/tests/test_product.py new file mode 100644 index 000000000..ec4935ac6 --- /dev/null +++ b/product_configurator/tests/test_product.py @@ -0,0 +1,705 @@ +from odoo.exceptions import ValidationError + +from ..tests.common import ProductConfiguratorTestCases + +# FIXME: many tests here do not have any assertions. +# They simply run something and expect it to not raise an exception. +# This is not a good practice. Tests should have assertions. + + +class TestProduct(ProductConfiguratorTestCases): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.productTemplate = cls.env["product.template"] + cls.productAttributeLine = cls.env["product.template.attribute.line"] + cls.productConfigStepLine = cls.env["product.config.step.line"] + cls.product_category = cls.env.ref("product.product_category_5") + cls.attributelinefuel = cls.env.ref( + "product_configurator.product_attribute_line_2_series_fuel" + ) + cls.attributelineengine = cls.env.ref( + "product_configurator.product_attribute_line_2_series_engine" + ) + cls.value_diesel = cls.env.ref( + "product_configurator.product_attribute_value_diesel" + ) + cls.value_218d = cls.env.ref( + "product_configurator.product_attribute_value_218d" + ) + cls.value_220d = cls.env.ref( + "product_configurator.product_attribute_value_220d" + ) + cls.value_silver = cls.env.ref( + "product_configurator.product_attribute_value_silver" + ) + cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine") + cls.config_step_body = cls.env.ref("product_configurator.config_step_body") + cls.product_tmpl_id = cls.env["product.template"].create( + { + "name": "Test Configuration", + "config_ok": True, + "type": "consu", + "categ_id": cls.product_category.id, + } + ) + # create attribute line 1 + cls.attributeLine1 = cls.productAttributeLine.create( + { + "product_tmpl_id": cls.product_tmpl_id.id, + "attribute_id": cls.attr_fuel.id, + "value_ids": [(6, 0, [cls.value_gasoline.id, cls.value_diesel.id])], + "required": True, + } + ) + # create attribute line 2 + cls.attributeLine2 = cls.productAttributeLine.create( + { + "product_tmpl_id": cls.product_tmpl_id.id, + "attribute_id": cls.attr_engine.id, + "value_ids": [ + ( + 6, + 0, + [ + cls.value_218i.id, + cls.value_220i.id, + cls.value_218d.id, + cls.value_220d.id, + ], + ) + ], + "required": True, + } + ) + # create attribute line 3 + cls.attributeLine3 = cls.productAttributeLine.create( + { + "product_tmpl_id": cls.product_tmpl_id.id, + "attribute_id": cls.attr_color.id, + "value_ids": [(6, 0, [cls.value_red.id, cls.value_silver.id])], + "required": True, + } + ) + + def _get_product_id(self): + self._configure_product_nxt_step() + return self.config_product.product_variant_ids + + def test_00__compute_template_attr_vals(self): + value_ids = self.product_tmpl_id.attribute_line_ids.mapped("value_ids") + self.product_tmpl_id._compute_template_attr_vals() + self.assertEqual( + value_ids, + self.product_tmpl_id.attribute_line_val_ids, + "Error: if value are different\ + Method: _compute_template_attr_vals() ", + ) + + def test_01_set_weight(self): + self.product_tmpl_id.weight = 120 + self.product_tmpl_id._set_weight() + self.assertEqual( + self.product_tmpl_id.weight, + self.product_tmpl_id.weight_dummy, + "Error: If set diffrent value for dummy_weight\ + Method: _set_weight()", + ) + self.product_tmpl_id.config_ok = False + set_weight = self.product_tmpl_id._set_weight() + self.assertIsNone( + set_weight, + "Error: If Value none\ + Method: _set_weight()", + ) + + def test_02_compute_weight(self): + self.product_tmpl_id.weight_dummy = 50.0 + self.product_tmpl_id._compute_weight() + self.assertEqual( + self.product_tmpl_id.weight_dummy, + self.product_tmpl_id.weight, + "Error: If set diffrent value for weight\ + Method: _compute_weight()", + ) + + def test_03_toggle_config(self): + configFalse = self.product_tmpl_id.toggle_config() + self.assertFalse( + configFalse, + "Error: If Boolean False\ + Method: toggle_config()", + ) + self.product_tmpl_id.toggle_config() + varient_value = self.product_tmpl_id._create_variant_ids() + self.assertIsNone( + varient_value, + "Error: If its return none\ + Method: create_variant_ids()", + ) + + def test_04_unlink(self): + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + product_config_wizard.action_next_step() + config_session_id = self.env["product.config.session"].search( + [("product_tmpl_id", "=", self.product_tmpl_id.id)] + ) + config_session_id.unlink() + varientId = self.product_tmpl_id.product_variant_ids + boolValue = varientId.unlink() + self.assertTrue( + boolValue, + "Error: if record are not unlink\ + Method: unlink()", + ) + + def test_05_check_default_values(self): + self.attributelinefuel.default_val = (self.value_gasoline.id,) + self.attributelineengine.default_val = self.value_218d.id + with self.assertRaises(ValidationError): + self.config_product._check_default_values() + + def test_06_configure_product(self): + # configure product + self.product_tmpl_id.configure_product() + self.ProductConfWizard.action_next_step() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + wizard_action = product_config_wizard.action_next_step() + varient_id = wizard_action.get("res_id") + self.assertEqual( + varient_id, + self.product_tmpl_id.product_variant_ids.id, + "Error: If get diffrent varient Id\ + Method: action_next_step()", + ) + product_config_wizard.action_previous_step() + self.assertEqual( + product_config_wizard.state, + "select", + "Error: If get diffrent State\ + Method: action_previous_step()", + ) + # create config_step_line 1 + self.configStepLine1 = self.productConfigStepLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "config_step_id": self.config_step_engine.id, + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + # create config_step_line 2 + self.configStepLine2 = self.productConfigStepLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "config_step_id": self.config_step_body.id, + "attribute_line_ids": [(6, 0, [self.attributeLine3.id])], + } + ) + self.product_tmpl_id.write( + { + "config_step_line_ids": [ + (6, 0, [self.configStepLine1.id, self.configStepLine2.id]) + ], + } + ) + + # configure product + self.product_tmpl_id.configure_product() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + product_config_wizard.action_previous_step() + self.assertEqual( + product_config_wizard.state, + str(self.configStepLine1.id), + "Error: If diffrent previous state and config state\ + Method: action_previous_step()", + ) + product_config_wizard.action_next_step() + self.assertEqual( + product_config_wizard.config_session_id.config_step, + product_config_wizard.state, + "Error: If diffrent state and config_step\ + Method: action_previous_step()", + ) + product_config_wizard.action_next_step() + + def test_07_get_mako_tmpl_name(self): + # check for product_product + product_product = self._get_product_id() + mako_tmpl_vals = product_product._get_mako_tmpl_name() + self.assertEqual( + mako_tmpl_vals, + product_product.display_name, + "Error: If get display_name are different\ + Method: _get_mako_tmpl_name()", + ) + self.config_product.write({"mako_tmpl_name": "Test Configuration Product"}) + mako_tmpl_vals = product_product._get_mako_tmpl_name() + self.assertEqual( + self.config_product.mako_tmpl_name, + mako_tmpl_vals, + "Error: If Mako Template are not exists or different\ + Method: _get_mako_tmpl_name()", + ) + + def test_08_compute_product_weight(self): + product_product = self._get_product_id() + self.config_product.weight = 10 + product_product.weight_extra = 20 + product_product._compute_product_weight() + self.assertEqual( + product_product.weight, + 30, + "Error: If value are not get 30\ + Method: _compute_product_weight()", + ) + product_product.config_ok = False + product_product.weight_dummy = 50 + product_product._compute_product_weight() + self.assertEqual( + product_product.weight, + 50, + "Error: If value are not get 50\ + Method: _compute_product_weight()", + ) + + def test_09_compute_config_name(self): + product_product = self._get_product_id() + product_product.config_ok = False + product_product._compute_config_name() + self.assertEqual( + product_product.config_name, + "2 Series", + "Error: If different product config_name\ + Method: _compute_config_name()", + ) + product_product.config_ok = True + product_product._compute_config_name() + self.assertEqual( + product_product.config_name, + "2 Series", + "Error: If different product config_name\ + Method: _compute_config_name()", + ) + + def test_10_reconfigure_product(self): + self.product_tmpl_id.configure_product() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + product_config_wizard.action_next_step() + # reconfigure product + product_product = self.product_tmpl_id.product_variant_ids + product_product.reconfigure_product() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218d.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_color.id}": self.value_silver.id, + } + ) + product_config_wizard.action_next_step() + value_ids = self.value_gasoline + self.value_218d + self.value_silver + # val_ids = self.value_gasoline + self.value_218i + self.value_red + # pta_val_ids = self.env["product.template.attribute.value"].search( + # [ + # ("product_tmpl_id", "=", self.product_tmpl_id.id), + # ("product_attribute_value_id", "in", value_ids.ids), + # ] + # ) + new_variant = self.product_tmpl_id.product_variant_ids.filtered( + lambda variant: variant.product_template_attribute_value_ids == value_ids + ) + self.assertFalse( + new_variant.id, + "Error: if variant id not exists\ + Method: reconfigure_product()", + ) + + def test_11_compute_product_weight_extra(self): + product_id = self.env.ref("product.product_delivery_01") + product_template_attr_value_ids = self.env.ref( + "product.product_4_attribute_1_value_2" + ) + product_template_attr_value_ids.write( + { + "weight_extra": 50.0, + } + ) + product_id._compute_product_weight_extra() + vals = {"product_template_attribute_value_ids": product_template_attr_value_ids} + product_id.write(vals) + self.assertEqual( + product_template_attr_value_ids.weight_extra, + 50.0, + product_id.weight_extra, + ) + + # _compute_product_weight_extra + product_product = self._get_product_id() + productAttPrice = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", self.config_product.id), + ("product_attribute_value_id", "=", self.value_gasoline.id), + ] + ) + productAttPrice.weight_extra = 45 + product_product._compute_product_weight_extra() + self.assertEqual( + productAttPrice.weight_extra, + product_product.weight_extra, + "Error: If weight_extra not equal\ + Method: _compute_product_weight_extra()", + ) + + def test_12_unlink(self): + product_product = self._get_product_id() + unlinkVals = product_product.unlink() + self.assertTrue( + unlinkVals, + "Error: If unlink record true\ + Method: unlink()", + ) + + def test_13_copy(self): + vals = self.config_product.copy() + self.assertEqual( + vals.name, + "2 Series (copy)", + "Error: If not equal\ + Method: copy()", + ) + self.assertTrue( + vals.attribute_line_ids, + "Error: If attribute_line_ids not exists\ + Method: copy()", + ) + + def test_14_validate_unique_config(self): + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_gasoline.id, + "value_ids": [(6, 0, [self.value_218i.id])], + }, + ) + ] + } + ) + with self.assertRaises(ValidationError): + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_gasoline.id, + "value_ids": [(6, 0, [self.value_218i.id])], + }, + ) + ] + } + ) + + def test_15_check_attr_value_ids(self): + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_gasoline.id, + "value_ids": [(6, 0, [self.value_gasoline.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_diesel.id, + "value_ids": [(6, 0, [self.value_diesel.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_218i.id, + "value_ids": [(6, 0, [self.value_218i.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_220i.id, + "value_ids": [(6, 0, [self.value_220i.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_218d.id, + "value_ids": [(6, 0, [self.value_218d.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_220d.id, + "value_ids": [(6, 0, [self.value_220d.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_red.id, + "value_ids": [(6, 0, [self.value_red.id])], + }, + ) + ] + } + ) + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_silver.id, + "value_ids": [(6, 0, [self.value_silver.id])], + }, + ) + ] + } + ) + with self.assertRaises(ValidationError): + self.product_tmpl_id.write( + { + "attribute_value_line_ids": [ + ( + 0, + 0, + { + "product_tmpl_id": self.product_tmpl_id.id, + "value_id": self.value_rims_378.id, + "value_ids": [(6, 0, [self.value_rims_378.id])], + }, + ) + ] + } + ) + + def test_16_check_duplicate_product(self): + self.product_tmpl_id.configure_product() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + product_config_wizard.action_next_step() + val_ids = self.value_gasoline + self.value_218i + self.value_red + pta_val_ids = self.env["product.template.attribute.value"].search( + [ + ("product_tmpl_id", "=", self.product_tmpl_id.id), + ("product_attribute_value_id", "in", val_ids.ids), + ] + ) + with self.assertRaises(ValidationError): + self.env["product.product"].create( + { + "name": "Test Configuration", + "product_tmpl_id": self.product_tmpl_id.id, + "product_template_attribute_value_ids": [(6, 0, pta_val_ids.ids)], + } + ) + + def test_17_fields_view_get(self): + product_product = self._get_product_id() + product_product.with_context(default_config_ok=True).get_view() + + def test_19_compute_product_variant_count(self): + self.product_tmpl_id = self.env["product.template"].create( + { + "name": "Test Configuration", + "config_ok": True, + "type": "consu", + "categ_id": self.product_category.id, + } + ) + product_variant_count = self.product_tmpl_id.product_variant_count + self.assertEqual( + product_variant_count, + 1, + "Error: If not equal\ + Method: _compute_product_variant_count()", + ) + + def test_20_get_config_name(self): + product_product = self._get_product_id() + product_product._get_config_name() + self.assertTrue( + product_product.name, + "Error: If value False\ + Method: _get_config_name()", + ) + + def test_21_search_product_weight(self): + product_product = self._get_product_id() + operator = "and" + value = 10 + search_product_weight = product_product._search_product_weight(operator, value) + self.assertTrue( + search_product_weight, + "Error: If value False\ + Method: _search_product_weight()", + ) + + def test_22_search_weight(self): + operator = "and" + value = 10 + search_weight = self.product_tmpl_id._search_weight(operator, value) + self.assertTrue( + search_weight, + "Error: If value False\ + Method: _search_weight()", + ) + + def test_23_check_config_line_domain(self): + product_config_line = self.env.ref( + "product_configurator.product_config_line_218_lines" + ) + with self.assertRaises(ValidationError): + self.env["product.template"].create( + { + "name": "template_test", + "config_line_ids": product_config_line, + } + ) diff --git a/product_configurator/tests/test_product_attribute.py b/product_configurator/tests/test_product_attribute.py new file mode 100644 index 000000000..453d6e59a --- /dev/null +++ b/product_configurator/tests/test_product_attribute.py @@ -0,0 +1,192 @@ +from odoo.exceptions import ValidationError + +from odoo.addons.base.tests.common import BaseCommon + +# FIXME: many tests here do not have any assertions. +# They simply run something and expect it to not raise an exception. +# This is not a good practice. Tests should have assertions. + + +class ProductAttributes(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.productAttributeLine = cls.env["product.template.attribute.line"] + cls.ProductAttributeFuel = cls.env.ref( + "product_configurator.product_attribute_fuel" + ) + cls.ProductAttributeLineFuel = cls.env.ref( + "product_configurator.product_attribute_line_2_series_fuel" + ) + cls.ProductTemplate = cls.env.ref("product_configurator.bmw_2_series") + cls.product_category = cls.env.ref("product.product_category_5") + cls.ProductAttributePrice = cls.env["product.template.attribute.value"] + cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel") + cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine") + cls.value_diesel = cls.env.ref( + "product_configurator.product_attribute_value_diesel" + ) + cls.value_218i = cls.env.ref( + "product_configurator.product_attribute_value_218i" + ) + cls.value_gasoline = cls.env.ref( + "product_configurator.product_attribute_value_gasoline" + ) + cls.ProductAttributeValueFuel = cls.value_gasoline.attribute_id.id + + def test_01_onchange_custome_type(self): + self.ProductAttributeFuel.min_val = 20 + self.ProductAttributeFuel.max_val = 30 + self.ProductAttributeFuel.custom_type = "char" + self.ProductAttributeFuel.onchange_custom_type() + self.assertEqual(self.ProductAttributeFuel.min_val, 0, "Min value is not False") + self.assertEqual(self.ProductAttributeFuel.max_val, 0, "Max value is not False") + + self.ProductAttributeFuel.min_val = 20 + self.ProductAttributeFuel.max_val = 30 + self.ProductAttributeFuel.custom_type = "integer" + self.ProductAttributeFuel.onchange_custom_type() + self.assertEqual( + self.ProductAttributeFuel.min_val, + 20, + "Min value is not equal to existing min value", + ) + self.assertEqual( + self.ProductAttributeFuel.max_val, + 30, + "Max value is not equal to existing max value", + ) + + self.ProductAttributeFuel.custom_type = "float" + self.ProductAttributeFuel.onchange_custom_type() + self.assertEqual( + self.ProductAttributeFuel.min_val, + 20, + "Min value is equal to existing min value \ + when type is changed to integer to float", + ) + self.assertEqual( + self.ProductAttributeFuel.max_val, + 30, + "Max value is equal to existing max value \ + when type is changed to integer to float", + ) + self.ProductAttributeFuel.custom_type = "binary" + self.ProductAttributeFuel.onchange_custom_type() + self.assertFalse( + self.ProductAttributeFuel.search_ok, + "Error: if search true\ + Method: onchange_custom_type()", + ) + + def test_02_onchange_val_custom(self): + self.ProductAttributeFuel.val_custom = False + self.ProductAttributeFuel.custom_type = "integer" + self.ProductAttributeFuel.onchange_val_custom_field() + self.assertFalse( + self.ProductAttributeFuel.custom_type, "custom_type is not False" + ) + + def test_03_check_searchable_field(self): + self.ProductAttributeFuel.custom_type = "binary" + with self.assertRaises(ValidationError): + self.ProductAttributeFuel.search_ok = True + + def test_04_validate_custom_val(self): + self.ProductAttributeFuel.write({"max_val": 20, "min_val": 10}) + self.ProductAttributeFuel.custom_type = "integer" + with self.assertRaises(ValidationError): + self.ProductAttributeFuel.validate_custom_val(5) + + self.ProductAttributeFuel.write({"max_val": 0, "min_val": 10}) + self.ProductAttributeFuel.custom_type = "integer" + with self.assertRaises(ValidationError): + self.ProductAttributeFuel.validate_custom_val(5) + + self.ProductAttributeFuel.write({"min_val": 0, "max_val": 20}) + self.ProductAttributeFuel.custom_type = "integer" + with self.assertRaises(ValidationError): + self.ProductAttributeFuel.validate_custom_val(25) + + def test_05_check_constraint_min_max_value(self): + self.ProductAttributeFuel.custom_type = "integer" + with self.assertRaises(ValidationError): + self.ProductAttributeFuel.write({"max_val": 10, "min_val": 20}) + + # FIXME: broken on call `onchange_attribute` method as + # """ + # odoo.exceptions.ValidationError: + # The attribute Fuel must have at least one value for the product 2 Series. + # + # def test_06_onchange_attribute(self): + # self.ProductAttributeLineFuel.onchange_attribute() + # self.assertFalse( + # self.ProductAttributeLineFuel.value_ids, "value_ids is not False" + # ) + # self.assertTrue( + # self.ProductAttributeLineFuel.required, "required not exsits value" + # ) + # self.ProductAttributeLineFuel.multi = True + # self.assertTrue( + # self.ProductAttributeLineFuel.multi, "multi not exsits value" + # ) + # self.ProductAttributeLineFuel.custom = True + # self.assertTrue( + # self.ProductAttributeLineFuel.custom, "custom not exsits value" + # ) + + def test_07_check_default_values(self): + with self.assertRaises(ValidationError): + self.ProductAttributeLineFuel.default_val = self.value_218i.id + + def test_08_copy_attribute(self): + copyAttribute = self.ProductAttributeFuel.copy() + self.assertEqual( + copyAttribute.name, + "Fuel (copy)", + "Error: If not copy attribute\ + Method: copy()", + ) + + def test_09_compute_get_value_id(self): + attrvalline = self.env["product.attribute.value.line"].create( + { + "product_tmpl_id": self.ProductTemplate.id, + "value_id": self.value_gasoline.id, + } + ) + self.assertTrue( + attrvalline.product_value_ids, + "Error: If product_value_ids not exists\ + Method: _compute_get_value_id()", + ) + + def test_10_validate_configuration(self): + with self.assertRaises(ValidationError): + self.env["product.attribute.value.line"].create( + { + "product_tmpl_id": self.ProductTemplate.id, + "value_id": self.value_diesel.id, + "value_ids": [(6, 0, [self.value_218i.id])], + } + ) + + def test_11_copy(self): + default = {} + productattribute = self.value_gasoline.copy(default) + self.assertEqual( + productattribute.name, + self.value_gasoline.name + " (copy)", + "Error: If not equal productattribute name\ + Method: copy()", + ) + + def test_12_onchange_values(self): + productattributeline = self.env["product.template.attribute.line"] + productattributeline.onchange_values() + self.assertEqual( + productattributeline.default_val, + productattributeline.value_ids, + "Error: If default_val not exists\ + Method: onchange_values()", + ) diff --git a/product_configurator/tests/test_product_config.py b/product_configurator/tests/test_product_config.py new file mode 100644 index 000000000..8a8f2278a --- /dev/null +++ b/product_configurator/tests/test_product_config.py @@ -0,0 +1,733 @@ +from odoo.exceptions import UserError, ValidationError + +from ..tests.common import ProductConfiguratorTestCases + +# FIXME: many tests here do not have any assertions. +# They simply run something and expect it to not raise an exception. +# This is not a good practice. Tests should have assertions. + + +class ProductConfig(ProductConfiguratorTestCases): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.productConfWizard = cls.env["product.configurator"] + cls.productTemplate = cls.env["product.template"] + cls.productAttribute = cls.env["product.attribute"] + cls.productAttributeVals = cls.env["product.attribute.value"] + cls.productAttributeLine = cls.env["product.template.attribute.line"] + cls.productConfigSession = cls.env["product.config.session"] + cls.productConfigDomain = cls.env["product.config.domain"] + cls.config_product = cls.env.ref("product_configurator.bmw_2_series") + cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine") + cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine") + cls.config_product_1 = cls.env.ref( + "product_configurator.product_config_line_gasoline_engines" + ) + cls.config_product_2 = cls.env.ref( + "product_configurator.2_series_config_step_body" + ) + # domain + cls.domain_gasolin = cls.env.ref( + "product_configurator.product_config_domain_gasoline" + ) + cls.domain_engine = cls.env.ref( + "product_configurator.product_config_domain_diesel" + ) + cls.config_image_red = cls.env.ref("product_configurator.config_image_1") + # value + cls.value_gasoline = cls.env.ref( + "product_configurator.product_attribute_value_gasoline" + ) + cls.value_diesel = cls.env.ref( + "product_configurator.product_attribute_value_diesel" + ) + cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red") + # config_step + cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine") + cls.attribute_line = cls.env.ref( + "product_configurator.product_attribute_line_2_series_engine" + ) + cls.value_silver = cls.env.ref( + "product_configurator.product_attribute_value_silver" + ) + cls.value_rims_387 = cls.env.ref( + "product_configurator.product_attribute_value_rims_387" + ) + # attribute line + cls.attribute_line_2_series_rims = cls.env.ref( + "product_configurator.product_attribute_line_2_series_rims" + ) + cls.attribute_line_2_series_tapistry = cls.env.ref( + "product_configurator.product_attribute_line_2_series_tapistry" + ) + cls.attribute_value_tapistry_oyster_black = cls.env.ref( + "product_configurator." + "product_attribute_value_tapistry_oyster_black" + ) + cls.attribute_line_2_series_transmission = cls.env.ref( + "product_configurator.product_attribute_line_2_series_transmission" + ) + + # attribute value + cls.attribute_rims = cls.env.ref("product_configurator.product_attribute_rims") + cls.attribute_tapistry = cls.env.ref( + "product_configurator.product_attribute_tapistry" + ) + cls.attribute_transmission = cls.env.ref( + "product_configurator.product_attribute_transmission" + ) + + # session id + cls.session_id = cls.productConfigSession.create( + { + "product_tmpl_id": cls.config_product.id, + "value_ids": [ + ( + 6, + 0, + [ + cls.value_gasoline.id, + cls.value_transmission.id, + cls.value_red.id, + ], + ) + ], + "user_id": cls.env.user.id, + } + ) + # ir attachment + cls.irAttachement = cls.env["ir.attachment"].create( + { + "name": "Test attachement", + "datas": "bWlncmF0aW9uIHRlc3Q=", + } + ) + + # configure product + cls._configure_product_nxt_step() + cls.config_session = cls.productConfigSession.search( + [("product_tmpl_id", "=", cls.config_product.id)] + ) + + # create product template + cls.product_tmpl_id = cls.productTemplate.create({"name": "Coca-Cola"}) + # create attribute 1 + cls.attribute_1 = cls.productAttribute.create( + { + "name": "Color", + } + ) + # create attribute 2 + cls.attribute_2 = cls.productAttribute.create( + { + "name": "Flavour", + } + ) + + # create attribute value 1 + cls.attribute_vals_1 = cls.productAttributeVals.create( + { + "name": "Orange", + "attribute_id": cls.attribute_1.id, + } + ) + # create attribute value 2 + cls.attribute_vals_2 = cls.productAttributeVals.create( + { + "name": "Balck", + "attribute_id": cls.attribute_1.id, + } + ) + # create attribute value 3 + cls.attribute_vals_3 = cls.productAttributeVals.create( + { + "name": "Coke", + "attribute_id": cls.attribute_2.id, + } + ) + # create attribute value 4 + cls.attribute_vals_4 = cls.productAttributeVals.create( + { + "name": "Mango", + "attribute_id": cls.attribute_2.id, + } + ) + + # TODO :: Left to take review of code + def test_00_check_value_attributes(self): + with self.assertRaises(ValidationError): + self.config_product_1.write( + {"value_ids": [(6, 0, [self.value_gasoline.id])]} + ) + + def test_01_check_config_step(self): + with self.assertRaises(ValidationError): + self.config_product_2.config_step_id = 4 + + def test_02_get_trans_implied(self): + self.domain_gasolin.write({"implied_ids": [(6, 0, [self.domain_engine.id])]}) + trans_implied_ids = self.domain_gasolin.trans_implied_ids.ids + self.assertEqual( + trans_implied_ids[-1], + self.domain_engine.id, + "Error: If value not exists\ + Method: _get_trans_implied()", + ) + + def test_03_check_config_step(self): + with self.assertRaises(ValidationError): + self.env["product.config.step.line"].create( + { + "product_tmpl_id": self.config_product.id, + "config_step_id": self.config_step_engine.id, + "attribute_line_ids": [(6, 0, [self.attribute_line.id])], + } + ) + + def test_04_compute_cfg_price(self): + # check for _compute_cfg_price + price = self.config_product.list_price + price += self.value_220i.product_id.lst_price + price += self.value_model_sport_line.product_id.lst_price + price += self.value_transmission.product_id.lst_price + price += self.value_options_2.product_id.lst_price + self.assertEqual( + self.session_id.price, + price, + "Error: If different session price and list_price\ + Method: _compute_cfg_price", + ) + + def test_05_get_custom_vals_dict(self): + # check for _get_custom_vals_dict + productConfigSessionCustVals = self.env[ + "product.config.session.custom.value" + ].create( + {"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id} + ) + # check for custom type Int + self.attr_fuel.custom_type = "integer" + productConfigSessionCustVals.update({"value": 154}) + checkIntval = self.session_id._get_custom_vals_dict() + attr_id = productConfigSessionCustVals.attribute_id.id + self.assertEqual( + checkIntval.get(attr_id), + 154, + "Error: If Not Integer value or False\ + Method: _get_custom_vals_dict()", + ) + # check for custom type Float + self.attr_fuel.custom_type = "float" + productConfigSessionCustVals.update({"value": 94.5}) + checkFloatval = self.session_id._get_custom_vals_dict() + attr_id = productConfigSessionCustVals.attribute_id.id + self.assertEqual( + checkFloatval.get(attr_id), + 94.5, + "Error: If Not Float value or False\ + Method: _get_custom_vals_dict()", + ) + # check for custom type Binary + self.attr_color.custom_type = "binary" + productConfigSessionCustVals1 = self.env[ + "product.config.session.custom.value" + ].create( + { + "cfg_session_id": self.session_id.id, + "attribute_id": self.attr_color.id, + "attachment_ids": [(6, 0, [self.irAttachement.id])], + } + ) + checkBinaryval = self.session_id._get_custom_vals_dict() + attr_id = productConfigSessionCustVals1.attribute_id.id + self.assertEqual( + checkBinaryval.get(attr_id), + productConfigSessionCustVals1.attachment_ids, + "Error: If Not attachement\ + Method: _get_custom_vals_dict()", + ) + + def test_06_compute_config_step_name(self): + self.config_session._compute_config_step_name() + self.assertTrue( + self.config_session.config_step_name, + "Error: If not config step name\ + Method: _compute_config_step_name()", + ) + self.config_session._compute_config_step_name() + self.assertEqual( + self.config_session.config_step_name, + "Extras", + "Error: If not equal config_step_name and config_step\ + Method: _compute_config_step_name()", + ) + session = self.productConfigSession.create( + { + "product_tmpl_id": self.config_product.id, + "value_ids": [ + (6, 0, [self.value_gasoline.id, self.value_transmission.id]) + ], + "user_id": self.env.user.id, + } + ) + session._compute_config_step_name() + self.assertFalse( + session.config_step_name, + "Error: If config_step_name not False\ + Method: _compute_config_step_name()", + ) + + def test_07_search_variant(self): + with self.assertRaises(ValidationError): + self.env["product.config.session"].search_variant() + + # check for search duplicate variant + variant_id = self.config_product.product_variant_ids + checkSearchvarient = self.config_session.search_variant() + self.assertEqual( + checkSearchvarient, + variant_id, + "Error: If Not Equal Variant or False\ + Method: search_variant()", + ) + + def test_08_check_custom_type(self): + # check for check_custom_type + with self.assertRaises(ValidationError): + self.env["product.config.session.custom.value"].create( + { + "attribute_id": self.value_silver.attribute_id.id, + "cfg_session_id": self.config_session.id, + "value": "Test", + "attachment_ids": [(6, 0, [self.irAttachement.id])], + } + ) + + self.attr_color.custom_type = "binary" + with self.assertRaises(ValidationError): + self.env["product.config.session.custom.value"].create( + { + "attribute_id": self.value_silver.attribute_id.id, + "cfg_session_id": self.config_session.id, + "value": "Test", + "attachment_ids": [(6, 0, [self.irAttachement.id])], + } + ) + + def test_09_create_get_variant(self): + # configure new product to check for search not dublicate variant + attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_1.id, + "value_ids": [ + (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id]) + ], + } + ) + # create attribute line 2 + attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_2.id, + "value_ids": [ + (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id]) + ], + } + ) + self.product_tmpl_id.write( + { + "attribute_line_ids": [(6, 0, [attributeLine1.id, attributeLine2.id])], + } + ) + self.product_tmpl_id.configure_product() + self.productConfWizard.action_next_step() + product_config_wizard = self.productConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attribute_1.id}": self.attribute_vals_1.id, + f"__attribute_{self.attribute_2.id}": self.attribute_vals_3.id, + } + ) + product_config_wizard.action_next_step() + config_session_1 = self.productConfigSession.search( + [("product_tmpl_id", "=", self.product_tmpl_id.id)] + ) + createVarientId = config_session_1.create_get_variant() + self.assertEqual( + createVarientId.name, + self.product_tmpl_id.name, + "Error: If Not Equal variant name\ + Method: search_variant()", + ) + # FIXME: broken when running `attributeLine1.custom = True` + # """ + # psycopg2.errors.UniqueViolation: + # duplicate key value violates unique constraint + # "product_product_combination_unique" + # DETAIL: Key (product_tmpl_id, combination_indices)=(81, 459,461) + # already exists. + # attributeLine1.custom = True + # self.env["product.config.session.custom.value"].create( + # { + # "cfg_session_id": config_session_1.id, + # "attribute_id": self.attribute_1.id, + # "value": "Coke", + # } + # ) + # config_session_1.create_get_variant() + + def test_10_check_value_ids(self): + with self.assertRaises(ValidationError): + self.config_image_red.write( + {"value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])]} + ) + + def test_11_unique_attribute(self): + with self.assertRaises(ValidationError): + self.env["product.config.session.custom.value"].create( + { + "cfg_session_id": self.config_session.id, + "attribute_id": self.attr_engine.id, + "value": "1234", + } + ) + self.env["product.config.session.custom.value"].create( + { + "cfg_session_id": self.config_session.id, + "attribute_id": self.attr_engine.id, + "value": "1234", + } + ) + + # FIXME: broken at the first create as + # """ + # psycopg2.errors.NotNullViolation + # null value in column "attribute_line_id" of + # relation "product_template_attribute_value" + # violates not-null constraint + # DETAIL: Failing row contains ... + # def test_12_get_cfg_weight(self): + # self.env["product.template.attribute.value"].create( + # { + # "product_tmpl_id": self.config_product.id, + # "product_attribute_value_id": self.value_red.id, + # "weight_extra": 20.0, + # } + # ) + # self.config_product.weight = 20 + # weightVal = self.config_session.get_cfg_weight() + # self.assertEqual( + # weightVal, + # 40.0, + # "Error: If Value are not equal\ + # Method: get_cfg_weight()", + # ) + # # check for config weight + # self.assertEqual( + # self.config_session.weight, + # 40.0, + # "Error: If config weight are not equal\ + # Method: _compute_cfg_weight()", + # ) + + def test_13_update_session_configuration_value(self): + # configure new product to check for search not dublicate variant + self.custom_vals = self.productConfigSession.get_custom_value_id() + self.attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_1.id, + "value_ids": [ + (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id]) + ], + "custom": True, + "required": True, + } + ) + # create attribute line 2 + self.attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_2.id, + "value_ids": [ + (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id]) + ], + "custom": True, + "required": True, + } + ) + self.product_tmpl_id.write( + { + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + self.attribute_1.custom_type = "binary" + self.product_tmpl_id.configure_product() + self.productConfWizard.action_next_step() + product_config_wizard = self.productConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attribute_1.id}": self.custom_vals.id, + f"__custom_{self.attribute_1.id}": "Test", + } + ) + # FIXME: broken validation at `product_config.create_get_variant` + # """ + # odoo.exceptions.ValidationError: Required attribute 'Flavour' is empty + # product_config_wizard.action_next_step() + + # FIXME: broken at the first create as + # """ + # psycopg2.errors.NotNullViolation + # null value in column "attribute_line_id" of + # relation "product_template_attribute_value" + # violates not-null constraint + # DETAIL: Failing row contains ... + # def test_14_get_cfg_price(self): + # self.env["product.template.attribute.value"].create( + # { + # "product_tmpl_id": self.config_product.id, + # "product_attribute_value_id": self.value_red.id, + # "weight_extra": 20.0, + # "price_extra": 20.0, + # } + # ) + # price = self.config_product.list_price + # price += self.value_220i.product_id.lst_price + # price += self.value_model_sport_line.product_id.lst_price + # price += self.value_transmission.product_id.lst_price + # price += self.value_options_2.product_id.lst_price + # price_extra_val = self.session_id.get_cfg_price() + # self.assertEqual( + # price_extra_val, + # price + 20, + # "Error: If not equal price extra\ + # Method: get_cfg_price()", + # ) + + def test_15_get_next_step(self): + self.session_id.get_next_step(state=None) + self.session_id.get_next_step(state="draft") + with self.assertRaises(UserError): + self.productConfigSession.get_next_step( + state="draft", value_ids=False, custom_value_ids=False + ) + + def test_16_get_all_step_lines(self): + step_line_value_1 = self.productConfigSession.get_all_step_lines() + self.assertFalse( + step_line_value_1, + "Error: If return True\ + Method: get_all_step_lines()", + ) + step_line_value_2 = self.session_id.get_all_step_lines() + self.assertTrue( + step_line_value_2, + "Error: If return True\ + Method: get_all_step_lines()", + ) + + def test_17_custom_value_validate_configuration(self): + self.custom_vals = self.productConfigSession.get_custom_value_id() + self.attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_1.id, + "value_ids": [ + (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id]) + ], + "custom": True, + "required": True, + } + ) + # create attribute line 2 + self.attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_2.id, + "value_ids": [ + (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id]) + ], + "custom": True, + "required": True, + } + ) + self.product_tmpl_id.write( + { + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + self.attribute_1.custom_type = "binary" + self.product_tmpl_id.configure_product() + self.productConfWizard.action_next_step() + product_config_wizard = self.productConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attribute_1.id}": self.custom_vals.id, + f"__custom_{self.attribute_1.id}": "Test", + } + ) + self.attributeLine1.custom = False + self.attributeLine2.custom = False + with self.assertRaises(ValidationError): + self.product_tmpl_id.configure_product() + + def test_18_onchange_attribute(self): + # create domain + self.productConfigDomainId = self.env["product.config.domain"].create( + {"name": "restriction 1"} + ) + self.productConfigDomainId.compute_domain() + # create attribute value line 1 + self.env["product.config.domain.line"].create( + { + "domain_id": self.productConfigDomainId.id, + "attribute_id": self.attr_fuel.id, + "condition": "in", + "value_ids": [(6, 0, [self.value_gasoline.id])], + "operator": "and", + } + ) + self.env["product.config.domain.line"].create( + { + "domain_id": self.productConfigDomainId.id, + "attribute_id": self.attr_color.id, + "condition": "in", + "value_ids": [(6, 0, [self.value_red.id])], + "operator": "and", + } + ) + self.attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_1.id, + "value_ids": [ + (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id]) + ], + "required": True, + } + ) + # create attribute line 2 + self.attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attribute_2.id, + "value_ids": [ + (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id]) + ], + "required": True, + } + ) + self.product_tmpl_id.write( + { + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + self.productConfigDomainId.compute_domain() + # create attribute value line 1 + config_line = self.env["product.config.line"].create( # noqa + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_line_id": self.attributeLine1.id, + "value_ids": [ + (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id]) + ], + "domain_id": self.productConfigDomainId.id, + } + ) + # FIXME: broken as + # """ + # psycopg2.errors.NotNullViolation: + # null value in column "domain_id" + # of relation "product_config_line" + # violates not-null constraint + # DETAIL: Failing row contains ... + # with self.assertRaises(ValidationError): + # config_line.onchange_attribute() + + # self.assertFalse( + # config_line.value_ids, + # "Error: If value_ids True\ + # Method: onchange_attribute()", + # ) + + def test_19_eval(self): + self.attr_color.custom_type = "binary" + productConfigSessionCustVals1 = self.env[ + "product.config.session.custom.value" + ].create( + { + "cfg_session_id": self.session_id.id, + "attribute_id": self.attr_color.id, + "attachment_ids": [(6, 0, [self.irAttachement.id])], + } + ) + checkBinary = productConfigSessionCustVals1.eval() + self.assertTrue( + checkBinary, + "Error: If value False\ + Method: eval()", + ) + + productConfigSessionCustVals = self.env[ + "product.config.session.custom.value" + ].create( + {"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id} + ) + self.attr_fuel.custom_type = "integer" + productConfigSessionCustVals.update({"value": 154}) + checkIntval = productConfigSessionCustVals.eval() + self.assertEqual( + 154, + checkIntval, + "Error: If Value not equal\ + Method: eval()", + ) + + self.attr_fuel.custom_type = "float" + productConfigSessionCustVals.update({"value": 15.4}) + checkfloat = productConfigSessionCustVals.eval() + self.assertEqual( + 15.4, + checkfloat, + "Error: If Value not equal\ + Method: eval()", + ) + + def test_20_values_available(self): + check_available_val_ids = ( + self.value_gasoline + self.value_218i + self.value_sport_line + ).ids + product_tmpl_id = self.config_product.id + values_ids = [self.value_diesel.id] + available_value_ids = self.productConfigSession.values_available( + check_available_val_ids, values_ids, {}, product_tmpl_id + ) + self.assertNotIn( + self.value_sport_line.id, + available_value_ids, + "Error: If value exists\ + Method: values_available()", + ) diff --git a/product_configurator/tests/test_wizard.py b/product_configurator/tests/test_wizard.py new file mode 100644 index 000000000..1a0660486 --- /dev/null +++ b/product_configurator/tests/test_wizard.py @@ -0,0 +1,602 @@ +from odoo.exceptions import UserError + +from ..tests.common import ProductConfiguratorTestCases + +# FIXME: many tests here do not have any assertions. +# They simply run something and expect it to not raise an exception. +# This is not a good practice. Tests should have assertions. + + +class ConfigurationWizard(ProductConfiguratorTestCases): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.productTemplate = cls.env["product.template"] + cls.productAttributeLine = cls.env["product.template.attribute.line"] + cls.productConfigStepLine = cls.env["product.config.step.line"] + cls.productConfigSession = cls.env["product.config.session"] + cls.product_category = cls.env.ref("product.product_category_5") + cls.attr_line_fuel = cls.env.ref( + "product_configurator.product_attribute_line_2_series_fuel" + ) + cls.attr_line_engine = cls.env.ref( + "product_configurator.product_attribute_line_2_series_engine" + ) + cls.value_diesel = cls.env.ref( + "product_configurator.product_attribute_value_diesel" + ) + cls.value_218d = cls.env.ref( + "product_configurator.product_attribute_value_218d" + ) + cls.value_220d = cls.env.ref( + "product_configurator.product_attribute_value_220d" + ) + cls.value_silver = cls.env.ref( + "product_configurator.product_attribute_value_silver" + ) + cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine") + cls.config_step_body = cls.env.ref("product_configurator.config_step_body") + cls.product_tmpl_id = cls.env["product.template"].create( + { + "name": "Test Configuration", + "config_ok": True, + "type": "consu", + "categ_id": cls.product_category.id, + } + ) + cls.custom_vals = cls.productConfigSession.get_custom_value_id() + cls.cfg_tmpl = cls.env.ref("product_configurator.bmw_2_series") + + attribute_vals = cls.cfg_tmpl.attribute_line_ids.mapped("value_ids") + cls.attr_vals = attribute_vals + + cls.attr_val_ext_ids = { + v: k for k, v in attribute_vals.get_external_id().items() + } + + def _check_wizard_nxt_step(self): + self.ProductConfWizard.action_next_step() + product_config_wizard = self.ProductConfWizard.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + } + ) + # create attribute line 1 + self.attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attr_fuel.id, + "value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])], + "required": True, + } + ) + # create attribute line 2 + self.attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attr_engine.id, + "value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])], + "required": True, + } + ) + # create attribute line 2 + self.attributeLine3 = self.productAttributeLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "attribute_id": self.attr_engine.id, + "value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])], + "required": True, + } + ) + # configure product creating config step + self.configStepLine1 = self.productConfigStepLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "config_step_id": self.config_step_engine.id, + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + # create config_step_line 2 + self.configStepLine2 = self.productConfigStepLine.create( + { + "product_tmpl_id": self.product_tmpl_id.id, + "config_step_id": self.config_step_body.id, + "attribute_line_ids": [(6, 0, [self.attributeLine3.id])], + } + ) + self.product_tmpl_id.write( + { + "config_step_line_ids": [ + (6, 0, [self.configStepLine1.id, self.configStepLine2.id]) + ], + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + } + ) + product_config_wizard.action_next_step() + product_config_wizard.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + return product_config_wizard + + def test_01_action_previous_step(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard.action_previous_step() + self.assertEqual( + product_config_wizard.state, + str(self.configStepLine1.id), + "Error: If state are not equal\ + Method: action_next_step()", + ) + product_config_wizard.action_next_step() + self.assertEqual( + product_config_wizard.state, + str(self.configStepLine2.id), + "Error: If state are not equal\ + Method: action_next_step()", + ) + wizard_action = product_config_wizard.action_next_step() + variant_id2 = wizard_action.get("res_id") + self.assertTrue( + variant_id2, + "Error: If varient not exists\ + Method: action_next_step()", + ) + + def test_02_action_reset(self): + product_config_wizard = self._check_wizard_nxt_step() + action_wizard = product_config_wizard.action_reset() + product_tmpl_id = action_wizard.get("context") + self.assertTrue( + product_tmpl_id.get("default_product_tmpl_id"), + "Error: If product_tmpl_id not exists\ + Method: action_reset()", + ) + + def test_03_compute_attr_lines(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard._compute_attr_lines() + self.assertTrue( + product_config_wizard.attribute_line_ids, + "Error: If atttribute_line_ids not exists\ + Method: _compute_attr_lines()", + ) + + def test_04_get_state_selection(self): + product_config_wizard = self._check_wizard_nxt_step() + config_wiz = product_config_wizard.with_context( + wizard_id=product_config_wizard.id + ).get_state_selection() + self.assertTrue( + config_wiz[1:], + "Error: If not config step selection\ + Method: get_state_selection()", + ) + + def test_05_compute_cfg_image(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard._compute_cfg_image() + self.assertFalse( + product_config_wizard.product_img, + "Error: If product_img exists\ + Method: _compute_cfg_image()", + ) + + def test_06_onchange_product_tmpl(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard.write( + { + "product_tmpl_id": self.config_product.id, + } + ) + with self.assertRaises(UserError): + product_config_wizard.onchange_product_tmpl() + + 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", + "model_luxury_line", + "silver", + "rims_384", + "tapistry_black", + "steptronic", + "smoker_package", + "tow_hook", + ] + product_config_wizard.get_onchange_domains(values, conf) + + def test_08_onchange_state(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard._onchange_state() + + def test_09_onchange_product_preset(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard._onchange_product_preset() + + def test_10_open_step(self): + wizard = self.env["product.configurator"] + step_to_open = wizard.config_session_id.check_and_open_incomplete_step() + wizard.open_step(step_to_open) + + # FIXME: broken test + # Fails at `product_config_wizard.attribute_line_ids.update(` as + # """odoo.exceptions.UserError: + # On the product Test Configuration + # you cannot transform the attribute Engine into the attribute 5.""" + # + # Also, the test is not very useful. It does not assert anything. + # + # def test_11_onchange(self): + # field_name = "" + # values = {f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id} + # product_config_wizard = self._check_wizard_nxt_step() + # field_prefix = product_config_wizard._prefixes.get("field_prefix") + # field_name = f"{field_prefix}{field_name}" + # specs = product_config_wizard._onchange_spec() + # product_config_wizard.onchange(values, field_name, specs) + # + # product_config_wizard.attribute_line_ids.update( + # { + # "attribute_id": self.attr_fuel.id, + # "custom": True, + # } + # ) + # values2 = { + # f"__attribute_{self.attr_fuel.id}": self.custom_vals.id, + # f"__custom_{self.attr_fuel.id}": "Test1", + # } + # product_config_wizard.onchange(values2, field_name, specs) + + def test_12_fields_get(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard.fields_get() + product_config_wizard.with_context( + wizard_id=product_config_wizard.id + ).fields_get() + + # custom value + self.attr_line_fuel.custom = True + self.attr_line_engine.custom = True + product_config_wizard_1 = self.ProductConfWizard.create( + { + "product_tmpl_id": self.config_product.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__custom_{self.attr_fuel.id}": "Test1", + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__custom_{self.attr_engine.id}": "Test2", + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + f"__attribute_{self.attr_rims.id}": self.value_rims_378.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id, + } + ) + product_config_wizard_1.action_previous_step() + product_config_wizard_1.action_previous_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_engine.id}": self.value_220i.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.action_next_step() + + vals = { + f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id, + } + product_config_wizard_1.write(vals) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_transmission.id}": self.value_transmission.id, + f"__attribute_{self.attr_options.id}": [ + [6, 0, [self.value_options_2.id]] + ], + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.with_context( + wizard_id=product_config_wizard_1.id + ).fields_get() + + def test_13_fields_view_get(self): + product_config_wizard = self._check_wizard_nxt_step() + product_config_wizard.fields_view_get() + product_config_wizard.with_context( + wizard_id=product_config_wizard.id + ).fields_view_get() + # custom value + # custom value + self.attr_line_fuel.custom = True + self.attr_line_engine.custom = True + product_config_wizard_1 = self.ProductConfWizard.create( + { + "product_tmpl_id": self.config_product.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__custom_{self.attr_fuel.id}": "Test1", + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__custom_{self.attr_engine.id}": "Test2", + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + f"__attribute_{self.attr_rims.id}": self.value_rims_378.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id, + } + ) + product_config_wizard_1.action_previous_step() + product_config_wizard_1.action_previous_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_engine.id}": self.value_220i.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.action_next_step() + vals = { + f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id, + } + product_config_wizard_1.write(vals) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id, + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_transmission.id}": self.value_transmission.id, + f"__attribute_{self.attr_options.id}": [ + [6, 0, [self.value_options_2.id]] + ], + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.with_context( + wizard_id=product_config_wizard_1.id + ).fields_view_get() + + def test_14_unlink(self): + product_config_wizard = self._check_wizard_nxt_step() + unlinkWizard = product_config_wizard.unlink() + self.assertTrue( + unlinkWizard, + "Error: If not unlink record\ + Method: unlink()", + ) + + def test_15_read(self): + product_config_wizard = self._check_wizard_nxt_step() + values = { + f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id, + f"__attribute_{self.attr_engine.id}": self.value_218i.id, + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + product_config_wizard.read(values) + product_tmpl = self.env["product.template"].create( + { + "name": "Test Custom", + "config_ok": True, + "type": "consu", + "categ_id": self.product_category.id, + } + ) + self.ProductConfWizard.action_next_step() + product_config_wizard_1 = self.ProductConfWizard.create( + { + "product_tmpl_id": product_tmpl.id, + } + ) + # create attribute line 1 + self.attributeLine1 = self.productAttributeLine.create( + { + "product_tmpl_id": product_tmpl.id, + "attribute_id": self.attr_fuel.id, + "value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])], + "required": True, + "custom": True, + } + ) + # create attribute line 2 + self.attributeLine2 = self.productAttributeLine.create( + { + "product_tmpl_id": product_tmpl.id, + "attribute_id": self.attr_engine.id, + "value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])], + "required": True, + "custom": True, + } + ) + # create attribute line 2 + self.attributeLine3 = self.productAttributeLine.create( + { + "product_tmpl_id": product_tmpl.id, + "attribute_id": self.attr_engine.id, + "value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])], + "required": True, + } + ) + # configure product creating config step + self.configStepLine1 = self.productConfigStepLine.create( + { + "product_tmpl_id": product_tmpl.id, + "config_step_id": self.config_step_engine.id, + "attribute_line_ids": [ + (6, 0, [self.attributeLine1.id, self.attributeLine2.id]) + ], + } + ) + # create config_step_line 2 + self.configStepLine2 = self.productConfigStepLine.create( + { + "product_tmpl_id": product_tmpl.id, + "config_step_id": self.config_step_body.id, + "attribute_line_ids": [(6, 0, [self.attributeLine3.id])], + } + ) + product_tmpl.write( + { + "config_step_line_ids": [ + (6, 0, [self.configStepLine1.id, self.configStepLine2.id]) + ], + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_fuel.id}": self.custom_vals.id, + f"__custom_{self.attr_fuel.id}": "#DEFSRE", + f"__attribute_{self.attr_engine.id}": self.custom_vals.id, + f"__custom_{self.attr_engine.id}": "#FERDFGR", + } + ) + product_config_wizard_1.action_next_step() + product_config_wizard_1.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + # check for custom value + custom_vals = { + f"__attribute_{self.attr_fuel.id}": self.custom_vals.id, + f"__custom_{self.attr_fuel.id}": "#DEFSRE", + f"__attribute_{self.attr_engine.id}": self.custom_vals.id, + f"__custom_{self.attr_engine.id}": "#FERDFGR", + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + product_config_wizard_1.read(custom_vals) + session = self.productConfigSession.search( + [("product_tmpl_id", "=", product_tmpl.id)] + ) + session.unlink() + self.attributeLine1.custom = False + self.attributeLine1.multi = True + self.ProductConfWizard.action_next_step() + product_config_wizard_2 = self.ProductConfWizard.create( + { + "product_tmpl_id": product_tmpl.id, + } + ) + product_config_wizard_2.action_next_step() + product_config_wizard_2.write( + { + f"__attribute_{self.attr_fuel.id}": [ + (6, 0, [self.value_diesel.id, self.value_gasoline.id]) + ], + f"__attribute_{self.attr_engine.id}": self.custom_vals.id, + f"__custom_{self.attr_engine.id}": "#FERDFGR", + } + ) + product_config_wizard_2.action_next_step() + product_config_wizard_2.write( + { + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + ) + # check for multi value + multi_vals = { + f"__attribute_{self.attr_fuel.id}": [ + (6, 0, [self.value_diesel.id, self.value_gasoline.id]) + ], + f"__attribute_{self.attr_engine.id}": self.custom_vals.id, + f"__custom_{self.attr_engine.id}": "#FERDFGR", + f"__attribute_{self.attr_color.id}": self.value_red.id, + } + product_config_wizard_2.read(multi_vals) + + def test_16_get_onchange_domains(self): + self.wizard = self.env["product.configurator"] + # session id + session_id = self.productConfigSession.create( + { + "product_tmpl_id": self.config_product.id, + "value_ids": [ + ( + 6, + 0, + [ + self.value_gasoline.id, + self.value_transmission.id, + self.value_red.id, + ], + ) + ], + "user_id": self.env.user.id, + } + ) + 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 + ) + rec = domains_available[ + field_prefix + str(self.value_sport_line.attribute_id.id) + ][-1][-1] + self.assertNotIn( + self.value_sport_line.id, + rec, + "Error: If value exists\ + Method: get_onchange_domains()", + ) diff --git a/product_configurator/views/product_attribute_view.xml b/product_configurator/views/product_attribute_view.xml new file mode 100644 index 000000000..2316db3ac --- /dev/null +++ b/product_configurator/views/product_attribute_view.xml @@ -0,0 +1,225 @@ + + + + + + + product.config.product.attribute.tree + product.attribute + + + + + + + + + + product.attribute.form.view + product.attribute + 100 + + + +
+ +
+
+ + + + + base.group_no_one + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Attributes + ir.actions.act_window + product.attribute + tree,form + {'flag_config_ok': True} + + + + + + + + product.template.attribute.value.view.tree.weight.extra + product.template.attribute.value + + tree + + + + + + + + + product.template.attribute.value.view.form.weight.extra + product.template.attribute.value + + form + + + + + + + + + + + product.attribute.value.tree + product.attribute.value + + + + + + + + + + + product.config.product.attribute.value.form.view + product.attribute.value + +
+ + +
+
+
+
+ + + + + + + + +
+
+
+
+ + + Attribute Values + ir.actions.act_window + product.attribute.value + tree,form + + + + + + + product.template.attribute.line.form + product.template.attribute.line + + + + {'flag_config_ok': context.get('default_config_ok', False)} + + + + +
diff --git a/product_configurator/views/product_config_view.xml b/product_configurator/views/product_config_view.xml new file mode 100644 index 000000000..768d6a476 --- /dev/null +++ b/product_configurator/views/product_config_view.xml @@ -0,0 +1,192 @@ + + + + + + + product.configurator.config.step.form + product.config.step + +
+ + + + + + + +
+
+
+ + + product.configurator.config.step.tree + product.config.step + +
+ + + +
+
+
+ + + + + product.configurator.domain.form + product.config.domain + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + product.configurator.domain.form.template + product.config.domain + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + product.config.session.tree + product.config.session + + + + + + + + + + + + + + + + + + product.config.session.form + product.config.session + +
+
+ +
+ + +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
diff --git a/product_configurator/views/product_view.xml b/product_configurator/views/product_view.xml new file mode 100644 index 000000000..3d8d26c06 --- /dev/null +++ b/product_configurator/views/product_view.xml @@ -0,0 +1,389 @@ + + + + + product.template.common.form + product.template + 16 + + + + + + + + + + product.configurator.product.template.form + product.template + 16 + + + + + + +
+ +
+
+ + + + { + 'show_attribute': False, + 'attribute_line_ids': attribute_line_ids, + } + + + + + + + + + + + + {'required': [('custom','!=',True)]} + + + + {'flag_config_ok': context.get('default_config_ok', False)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {'invisible': [('config_ok','=',True)]} + + +
+
+ + + product.configurator.product.template.search.view + product.template + + + + + + + + + + + + product.template.product.tree + product.template + + + + context.get('default_config_ok', 0) + + + + + + Product.template.product.kanban + product.template + + + + + + +
+ + + +
+
+
+
+ + + product.configurator.form.view.custom.vals + product.product + + + +