diff --git a/pim_base/README.rst b/pim_base/README.rst new file mode 100644 index 000000000..c4198158f --- /dev/null +++ b/pim_base/README.rst @@ -0,0 +1,110 @@ +=================================== +Product Information Management base +=================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2bac3955accb9ac2fee35c5d7861291f62359a9f46306e904d528504fc2a5ec6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fodoo--pim-lightgray.png?logo=github + :target: https://github.com/OCA/odoo-pim/tree/17.0/pim_base + :alt: OCA/odoo-pim +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/odoo-pim-17-0/odoo-pim-17-0-pim_base + :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/odoo-pim&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Base module for Product Information Management. + +The module creates a new application menu "PIM" gathering native views +about Products : + +- Products and Products Variants views +- Attributes +- Categories + +It also creates a new user group category with 3 access rights levels : + +- PIM Reader +- PIM User +- PIM Manager + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Define your user **PIM access rights** in the Application Accesses as a +Manager, User or Reader and the application menu "PIM" will appear. + +Usage +===== + + + +Known issues / Roadmap +====================== + + + +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 +------- + +* Akretion +* Pierre Verkest + +Contributors +------------ + +- Sébastien BEAU +- Clément Mombereau +- Cédric PIGEON +- Denis Roussel +- Pierre Verkest + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/odoo-pim `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pim_base/__init__.py b/pim_base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pim_base/__manifest__.py b/pim_base/__manifest__.py new file mode 100644 index 000000000..ea4837d70 --- /dev/null +++ b/pim_base/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2020 Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Information Management base", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "author": "Akretion, Pierre Verkest , " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/odoo-pim", + "depends": [ + "product", + ], + "data": [ + "data/ir_module_category_data.xml", + "security/pim_security.xml", + "views/product_view.xml", + "views/pim_view.xml", + "views/product_attribute_value.xml", + ], + "demo": [], + "installable": True, + "application": True, +} diff --git a/pim_base/data/ir_module_category_data.xml b/pim_base/data/ir_module_category_data.xml new file mode 100644 index 000000000..21da090c6 --- /dev/null +++ b/pim_base/data/ir_module_category_data.xml @@ -0,0 +1,8 @@ + + + + PIM + Manage your products catalogue easily + 2 + + diff --git a/pim_base/pyproject.toml b/pim_base/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/pim_base/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pim_base/readme/CONFIGURE.md b/pim_base/readme/CONFIGURE.md new file mode 100644 index 000000000..ef0b8fd80 --- /dev/null +++ b/pim_base/readme/CONFIGURE.md @@ -0,0 +1,2 @@ +Define your user **PIM access rights** in the Application Accesses as a +Manager, User or Reader and the application menu "PIM" will appear. diff --git a/pim_base/readme/CONTRIBUTORS.md b/pim_base/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..bbefce888 --- /dev/null +++ b/pim_base/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Sébastien BEAU \ +- Clément Mombereau \ +- Cédric PIGEON \ +- Denis Roussel \ +- Pierre Verkest \ diff --git a/pim_base/readme/DESCRIPTION.md b/pim_base/readme/DESCRIPTION.md new file mode 100644 index 000000000..7b7d84a8c --- /dev/null +++ b/pim_base/readme/DESCRIPTION.md @@ -0,0 +1,14 @@ +Base module for Product Information Management. + +The module creates a new application menu "PIM" gathering native views +about Products : + +- Products and Products Variants views +- Attributes +- Categories + +It also creates a new user group category with 3 access rights levels : + +- PIM Reader +- PIM User +- PIM Manager diff --git a/pim_base/readme/ROADMAP.md b/pim_base/readme/ROADMAP.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pim_base/readme/ROADMAP.md @@ -0,0 +1 @@ + diff --git a/pim_base/readme/USAGE.md b/pim_base/readme/USAGE.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pim_base/readme/USAGE.md @@ -0,0 +1 @@ + diff --git a/pim_base/security/pim_security.xml b/pim_base/security/pim_security.xml new file mode 100644 index 000000000..bbb224738 --- /dev/null +++ b/pim_base/security/pim_security.xml @@ -0,0 +1,30 @@ + + + + + Reader + + + the user will have only the right to consult the products catalogue + + + + User + + + + the user will be able to modify products but will not be authorized to + create attributes + + + + Manager + + + + + the user will be able to modify products and create attributes + + + diff --git a/pim_base/static/description/icon.png b/pim_base/static/description/icon.png new file mode 100644 index 000000000..9ec04c54e Binary files /dev/null and b/pim_base/static/description/icon.png differ diff --git a/pim_base/static/description/index.html b/pim_base/static/description/index.html new file mode 100644 index 000000000..9ecefab02 --- /dev/null +++ b/pim_base/static/description/index.html @@ -0,0 +1,452 @@ + + + + + +Product Information Management base + + + +
+

Product Information Management base

+ + +

Beta License: AGPL-3 OCA/odoo-pim Translate me on Weblate Try me on Runboat

+

Base module for Product Information Management.

+

The module creates a new application menu “PIM” gathering native views +about Products :

+
    +
  • Products and Products Variants views
  • +
  • Attributes
  • +
  • Categories
  • +
+

It also creates a new user group category with 3 access rights levels :

+
    +
  • PIM Reader
  • +
  • PIM User
  • +
  • PIM Manager
  • +
+

Table of contents

+ +
+

Configuration

+

Define your user PIM access rights in the Application Accesses as a +Manager, User or Reader and the application menu “PIM” will appear.

+
+
+

Usage

+
+ +
+

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

+ +
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/odoo-pim project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pim_base/views/pim_view.xml b/pim_base/views/pim_view.xml new file mode 100644 index 000000000..2c83ed100 --- /dev/null +++ b/pim_base/views/pim_view.xml @@ -0,0 +1,74 @@ + + + + + Product Attribute Values + product.attribute.value + tree,form + + + + + + + + + + + + + + + diff --git a/pim_base/views/product_attribute_value.xml b/pim_base/views/product_attribute_value.xml new file mode 100644 index 000000000..6c97952be --- /dev/null +++ b/pim_base/views/product_attribute_value.xml @@ -0,0 +1,12 @@ + + + + product.attribute.value + + + + + + + + diff --git a/pim_base/views/product_view.xml b/pim_base/views/product_view.xml new file mode 100644 index 000000000..dbe8fe09b --- /dev/null +++ b/pim_base/views/product_view.xml @@ -0,0 +1,43 @@ + + + + + pim.product.template.product.tree + product.template + + 90 + + + + + + + + + + + + + + + + Products + product.template + tree,kanban,form + + {"include_native_attribute": 1, "search_default_filter_to_sell": 1} + + diff --git a/product_creation_dynamic_wizard/README.rst b/product_creation_dynamic_wizard/README.rst new file mode 100644 index 000000000..8c7c3e136 --- /dev/null +++ b/product_creation_dynamic_wizard/README.rst @@ -0,0 +1,229 @@ +=============================== +Product creation dynamic wizard +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4ccd47b19299ac5bdf8295fabf6939f2722a0aaba3a5caad6c53a3e9cf4cb207 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fodoo--pim-lightgray.png?logo=github + :target: https://github.com/OCA/odoo-pim/tree/17.0/product_creation_dynamic_wizard + :alt: OCA/odoo-pim +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/odoo-pim-17-0/odoo-pim-17-0-product_creation_dynamic_wizard + :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/odoo-pim&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module introduces a fully customizable, dynamic wizard to guide +users through product creation. + +As Odoo evolves and integrates more features, the standard product form +becomes increasingly complex. This wizard simplifies the process by +presenting a series of dynamic questions tailored to your business +needs, ensuring better data consistency, faster onboarding, and reduced +user errors. + +Key Features +------------ + +- Fully configurable step-by-step product creation flow +- Each step corresponds to a field, form, or logical decision +- Conditions allow custom paths depending on answers +- Default values can be set and inherited across steps +- Supports custom views, model relations, and field domains + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure the dynamic wizard, go to PIM > Product Wizard Settings > +Questions as a PIM manager. + +Wizard configuration can be a bit tricky as more features are added. As +of now, available question attributes include: + +- **Name**: Question name, shown in the bottom-right of the wizard to + help with maintenance (especially if a screenshot is taken). +- **Parent question**: Questions can be nested like a tree. If the + parent condition passes, its branch will be processed; otherwise, + child questions are skipped. +- **Is Automatic**: Hides the question from the user and allows setting + values automatically. +- **Question**: The actual prompt shown to the user, unless it's + automatic. +- **Sequence**: Defines the question order within its branch. +- **Question Type**: + + - **Field** – sets a product template field + - **Custom** – lets the user choose from a custom set of values + - **Logical** – groups multiple field updates under one step and/or + organize questions + +- **Is Conditional**: Only displays the step and children if the + condition is met (requires a non-logical parent). +- **Answer Required**: Forces the user to provide an answer before + proceeding. + +**Apply If (page)**: Setup condition (display if *Is conditional* is +checked ) + +- **Conditional Operator**: Currently supports == and !=. +- **Expected result**: The value to match on the parent question. For: + + - **Custom** – select from predefined values + - **Field** – use the technical value (666 for many2one, + True/\`False\` for booleans, technical value on field selection ie: + product / service / consu, ...) + +**Product Attribute (page)**: Shown if *Question Type* is **Field**. + +- **Field**: Product template field to set + +- **Display Field Name**: Show/hide the field label in the wizard + +- **Default Value**: A string, depending on field type: + + - many2one → record ID (e.g., 1) + - boolean → true for checked/true value, empty string or false for + unchecked/false value + - x2m → valid JSON string (e.g. [[0, 0, {"name": "Box 20", "qty": + 20}]]) + +- **Custom View**: XML snippet to control how the field is rendered: + + :: + + + +**Custom Response (page)**: Shown if *Question Type* is **Custom** + +- List of valid responses +- **Default Answer**: Can be set after the question is saved + +**Values (page)**: Shown if *Question Type* is **Logical** + +- **Default Values**: Only set values that aren't already defined by a + previous step (even False or empty string counts as defined) +- **Values**: Always override values, even if already set by another + step. JSON format (e.g. {"type": "product", "company_id": + current_company_id}) + +**Children Questions**: Executed only if the parent condition is met. + +.. note:: + + If a name is defined, it will be used as the wizard's title. It's a + good idea to ask for the product name first, so users remember what + they’re doing. + +Special company_id cases: + +- company_id is auto-set by the module to the current company; a + different default will be ignored. +- You can use special variable current_company_id that will be replaced + by the id of the company_id already set. this can be used in + + - default values for x2m fields to set the company_id + - in custom view to set the default_company_id in context key + - in logical values for x2m fields + +Usage +===== + +1. Go to **Products > Create via Wizard** or use the create products via + the Create (Wizard) on form view +2. The user is prompted with step-by-step questions +3. Each answer may influence the next question shown +4. At the end, the product is created and opened automatically + +Benefits: + +- New users don't need to know all product form details +- Predefined flows help enforce business logic +- Easy to extend or maintain when product policies change + +Known issues / Roadmap +====================== + +- Allow reuse of field’s values as default for other +- Clicking "Previous" should undo values set in the current step +- Dynamically adapt "expected value" input based on parent field type +- Validate default values (especially for x2m) +- Allow conditions on more than just parent question values +- Support for complex conditions (more than one with AND / OR...) +- Add toolbar button for wizard launch (avoid menu + action dropdown) +- Skip validation/saving when going back with "Previous" +- Connect to an LLM to suggest default values based on previous answers +- managed translated fields +- Split module to make it agnostic from product model ( could be useful + on res.partner as well) + +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 +------- + +* Pierre Verkest + +Contributors +------------ + +- Pierre Verkest + +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-petrus-v| image:: https://github.com/petrus-v.png?size=40px + :target: https://github.com/petrus-v + :alt: petrus-v + +Current `maintainer `__: + +|maintainer-petrus-v| + +This module is part of the `OCA/odoo-pim `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_creation_dynamic_wizard/__init__.py b/product_creation_dynamic_wizard/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/product_creation_dynamic_wizard/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/product_creation_dynamic_wizard/__manifest__.py b/product_creation_dynamic_wizard/__manifest__.py new file mode 100644 index 000000000..2ec096939 --- /dev/null +++ b/product_creation_dynamic_wizard/__manifest__.py @@ -0,0 +1,42 @@ +# Copyright 2025 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product creation dynamic wizard", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "author": "Pierre Verkest , Odoo Community Association (OCA)", + "website": "https://github.com/OCA/odoo-pim", + "maintainers": [ + "petrus-v", + ], + "depends": [ + "product", + "pim_base", + "base_sparse_field", + "web", + ], + "data": [ + "wizards/product_creation_dynamic_wizard.xml", + "views/product_creation_question.xml", + "security/ir.model.access.csv", + ], + "demo": [ + "demo/product_creation_question.xml", + ], + "assets": { + "web.assets_backend": [ + "static/src/js/tree_button.js", + "static/src/js/kanban_button.js", + "static/src/js/form_button.js", + ], + "web.assets_qweb": [ + "static/src/xml/tree_button.xml", + "static/src/xml/kanban_button.xml", + "static/src/xml/form_button.xml", + ], + }, + "installable": True, + "application": False, +} diff --git a/product_creation_dynamic_wizard/demo/product_creation_question.xml b/product_creation_dynamic_wizard/demo/product_creation_question.xml new file mode 100644 index 000000000..5a0cd22c2 --- /dev/null +++ b/product_creation_dynamic_wizard/demo/product_creation_question.xml @@ -0,0 +1,267 @@ + + + + 0 Product name + What would be the product template name? + 0 + + + Product name (created from wizard) + + + + + 1 Product type + Product type? + 1 + + + + + + + + 1.0 Purchase consumable product auto. + 0 + + + + consu + + + + + 1.1 Sale consumable product + Sale consumable product? + 1 + + + + consu + + + + + 1.1.1 Sale consumable color? + What's the color? + 1 + + 546 + + + + + 1.1.2 - product packaging custom view + Setup packaging? + 2 + + + True + + ]]> + + + + + + 1.1.3 - product packaging custom view + 2 + + + False + [[0, 0, {"name": "Box 20", "qty": 20, "company_id": current_company_id}]] + + + + + + 1.2 Purchase service auto. + automatic step: purchase ok + 2 + + + + service + + + + + 1.3 Sale service product auto. + automatic step: sale ok + 3 + + + + + service + + + + + + 2 Custom question + Is this product active? + custom + 2 + + + + + + Yes + + + + + No + + + + + 2.1 active auto. + automatic step: active + 1 + + + + + + + + + 2.2 active auto. + automatic step: active + 2 + + + + != + + + + + + 10 default unset values + automatic step: active + 10 + + + + + + default and force values + logical + 30 + {"color": 987, "weight": 3.1, "volume": 3.1} + {"volume": 3.2, "company_id": current_company_id} + + diff --git a/product_creation_dynamic_wizard/i18n/fr.po b/product_creation_dynamic_wizard/i18n/fr.po new file mode 100644 index 000000000..e38c8849b --- /dev/null +++ b/product_creation_dynamic_wizard/i18n/fr.po @@ -0,0 +1,690 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_creation_dynamic_wizard +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_dynamic_wizard +msgid "" +"A dynamic wizard that helps user to create product based on business " +"configuration" +msgstr "" +"Assistant dynamique qui aide l'utilisateur à créer des produits en fonction " +"de la configuration de son entreprise" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_needaction +msgid "Action Needed" +msgstr "Action requise" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__active +msgid "Active" +msgstr "Actif" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__answer_ids +msgid "Answer" +msgstr "Réponse" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__answer_required +msgid "Answer Required" +msgstr "Réponse requise" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__answer_ids +msgid "Answers for the question" +msgstr "Réponses pour la question" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Apply if" +msgstr "Condition" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Archived" +msgstr "Archivé" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Cancel" +msgstr "Annuler " + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__child_ids +msgid "Child questions" +msgstr "Question enfant" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Children questions" +msgstr "Questions enfants" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__complete_name +msgid "Complete Name" +msgstr "Nom complet" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_operator +msgid "Conditional Operator" +msgstr "Opérateur" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_template_form_view +msgid "Create (Wizard)" +msgstr "Créer (Assistant)" + +#. module: product_creation_dynamic_wizard +#: model:ir.actions.server,name:product_creation_dynamic_wizard.product_creation_dynamic_wizard_action +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_creation_dynamic_wizard +msgid "Create new product" +msgstr "Créer un produit" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_template_form_view +msgid "Create product using wizard" +msgstr "Creation d'un produit avec l'assistant" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__create_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__create_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__create_uid +msgid "Created by" +msgstr "Creé par" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__create_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__create_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py:0 +#, python-format +msgid "Creating %(product_name)s..." +msgstr "Creation de %(product_name)s..." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_company_id +msgid "Current Company" +msgstr "Entreprise courante" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_complete_name +msgid "Current Complete Name" +msgstr "Nom complet courant" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_question +msgid "Current Question" +msgstr "Question courante" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_step +msgid "Current Step" +msgstr "Etape courante" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__custom +msgid "Custom" +msgstr "Personnalisé" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__custom_view +msgid "Custom View" +msgstr "Vue personnalisée" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Custom response" +msgstr "Réponses personnalisées" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__default_answer_id +msgid "Default answer" +msgstr "Réponse par défaut" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__default_field_value +msgid "Default value" +msgstr "Valeur par défaut" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__default_field_value +msgid "Default value for the field" +msgstr "Valeur par défaut pour le champ" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__logical_default_values +msgid "" +"Default values are used only if values are not set. Expected a valid json " +"format, before parsing json special variables are replaced (ie: " +"current_company_id)" +msgstr "" +"Valeur par défaut est utilisée seulement si aucune valeur n'est définie. " +"Valeur attendue au format json, avant de la parser les variables spéciales " +"sont remplacées (ex: current_company_id)" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__disable_create_product_button +msgid "Disable create product button" +msgstr "Désactiver le bouton de création de produit" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__display_field_name +msgid "Display Field Name" +msgstr "Afficher le nom du champ" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__display_name +msgid "Display Name" +msgstr "Libellé" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__answer_required +msgid "Does a response is required on the current question?" +msgstr "La réponse est-elle requise sur la question courante?" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result_answer_id +msgid "Expected answer on parent question" +msgstr "Réponse attendue sur la question parente" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result +msgid "Expected result on parent question" +msgstr "Résultat attendu sur la question parente" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__field_id +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__field +msgid "Field" +msgstr "Champ" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Group By" +msgstr "Groupé par" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__id +msgid "ID" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__display_field_name +msgid "If check, display the label field in the wizard." +msgstr "Si coché, affiche le nom du champ dans l'assistant." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_needaction +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_unread +msgid "If checked, new messages require your attention." +msgstr "Si coché, les nouveaus messages nécessitent votre attention." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_error +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "Si coché, certains messages ont une erreur de livraison." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__is_automatic +msgid "" +"If checked, the question will be automatically processed with the given " +"default value without asking the user for a value." +msgstr "" +"Si coché, la question sera automatiquement traitée avec la valeur par défaut " +"sans demander à l'utilisateur de la saisir." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result +msgid "If parent response is match run the current node and childs" +msgstr "" +"Si la réponse à la question parente correspond, éxécute le noeud courant et " +"ses enfants" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__custom_view +msgid "If set use the definition as it." +msgstr "Si défini, utilise la définition de vue telle quelle." + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#, python-format +msgid "" +"Incorrect JSON data - field; '%(field_name)s':\n" +"Error:\n" +"%(error)s\n" +"Incorrect data: \n" +"%(data)s" +msgstr "" +"Données JSON incorrectes - champ '%(field_name)s':\n" +"Erreur:\n" +"%(error)s\n" +"Données incorrectes:\n" +"%(data)s" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#, python-format +msgid "" +"Incorrect XML data - field '%(field_name)s':\n" +"Error:\n" +"%(error)s\n" +"Incorrect data: \n" +"%(data)s" +msgstr "" +"Données XML incorrectes - champ '%(field_name)s':\n" +"Erreur:\n" +"%(error)s\n" +"Données incorrectes:\n" +"%(data)s" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__is_automatic +msgid "Is Automatic" +msgstr "Est automatique" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_question +msgid "Is conditional" +msgstr "Est conditionnelle" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_custom +msgid "Is this product active?" +msgstr "Le produit est-il actif ?" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__parent_question_type +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__question_type +msgid "" +"Kind of questions: \n" +"* **field**: Used to set a product field value (can be a node as well)\n" +"* **custom**: Usefull for nodes to ask question not related to product " +"field\n" +"* **logical**: Logical nodes that allows to set multiple values as default " +"value or as new value\n" +msgstr "" +"Type de question\n" +"* **Champ**: Utilisé pour les questions relatives au champ d'un produit\n" +"* **Personalisée**: Utilisé pour les questions personnalisées qui ne sont " +"pas liées à un champ de l'article\n" +"* **Logique**: Permet d'organiser les question dans des noeuds logique et " +"définir plusieurs valeurs en même temps\n" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings____last_update +msgid "Last Modified on" +msgstr "Date de dernière modification" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__write_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__write_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__write_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__write_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__logical +msgid "Logical" +msgstr "Logique" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__logical_default_values +msgid "Logical Default Values" +msgstr "Valeurs par défaut" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__logical_values +msgid "Logical Values" +msgstr "Valeurs" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_ids +msgid "Messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__name +msgid "Name" +msgstr "Nom" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Next" +msgstr "Suivant" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_operator +msgid "Operator to use" +msgstr "Opérateur à utiliser" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_id +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Parent" +msgstr "Parent" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_path +msgid "Parent Path" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_question_type +msgid "Parent type" +msgstr "Type de question parent" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Previous" +msgstr "Précédent" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_product +msgid "Product" +msgstr "Produit" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__product_data +msgid "Product Data" +msgstr "Données du produit" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_template +msgid "Product Template" +msgstr "Modèle de produit" + +#. module: product_creation_dynamic_wizard +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_dynamic_wizard_settings +msgid "Product Wizard Settings" +msgstr "Configuration de l'assistant de produit" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Product attribute" +msgstr "Attribut produit" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_answer +msgid "Product creation answer" +msgstr "Réponse aux questions personnalisées" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_question +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_tree_view +msgid "Product creation question" +msgstr "Questions" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Product creation wizard" +msgstr "Assistant de création de produit" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_category +msgid "Product type?" +msgstr "Type de produit ?" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__question_id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__question +msgid "Question" +msgstr "Question" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__question_type +msgid "Question Type" +msgstr "Type de question" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#: model:ir.model.constraint,message:product_creation_dynamic_wizard.constraint_product_creation_question_question_set +#, python-format +msgid "Question is required if step is not automatic" +msgstr "La question est obligatoire si le step n'est pas automatique" + +#. module: product_creation_dynamic_wizard +#: model:ir.actions.act_window,name:product_creation_dynamic_wizard.product_creation_question_action +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_creation_question_action +msgid "Questions" +msgstr "Questions" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_company_id +msgid "" +"Related to the current company_id that will be set on productwhich can be " +"used in: \n" +"* custom view while creating x2m object (ie: " +"`contex=\"{'default_company_id': current_company_id}\"`)\n" +"* in default values while creating x2m objects \n" +"(ie: [[6,0,{'company_id': current_company_id}]])" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu +msgid "Sale consumable product?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Search Product creation question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__sequence +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__sequence +msgid "Sequence" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu_packaging_custom_view +msgid "Setup packaging?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__name +msgid "Technical name used to help organized questions." +msgstr "Nom technique utilisé pour organiser les questions." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__question +msgid "The question displayed to the end user" +msgstr "Question affichée à l'utilisateur" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "" +"This question and its children will be applied only if response to the\n" +" parent questions satisfy the following " +"operator and value." +msgstr "" +"Cette question et ses enfants seront appliqués seulement si la réponse à la\n" +" question parente satisfait l'opérateur et la valeur suivante." + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__logical_values +msgid "" +"Those values will erase existing values. Expected a valid json format, " +"before parsing json special variables are replaced (ie: current_company_id)" +msgstr "" +"Ces valeurs effaceront les valeurs existantes. Attention, la valeur doit " +"être au format valide json, avant de la parser les variables spéciales sont " +"remplacées (ex: current_company_id)" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Values" +msgstr "Valeurs" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_question +msgid "What ever the question depends on a previous fields value" +msgstr "La question dépend de la réponse à la question précédente." + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_name +msgid "What would be the product template name?" +msgstr "Qu'est le nom du produit ?" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu_color +msgid "What's the color?" +msgstr "Quelle est la couleur?" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__wizard_steps +msgid "Wizard Steps" +msgstr "étapes de l'assistant" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "you need to save the question before you can set the default answer." +msgstr "" +"Vous devez sauvegarder la question avant de pouvoir définir la réponse par " +"défaut." + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py:0 +#, python-format +msgid "a new product" +msgstr "un nouveau produit" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_active +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_active_yes +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_default_active_yes +msgid "automatic step: active" +msgstr "étape automatique: actif" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_purchase_service +msgid "automatic step: purchase ok" +msgstr "étape automatique: achat ok" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_service +msgid "automatic step: sale ok" +msgstr "étape automatique: vente ok" diff --git a/product_creation_dynamic_wizard/i18n/product_creation_dynamic_wizard.pot b/product_creation_dynamic_wizard/i18n/product_creation_dynamic_wizard.pot new file mode 100644 index 000000000..86a5c5e89 --- /dev/null +++ b/product_creation_dynamic_wizard/i18n/product_creation_dynamic_wizard.pot @@ -0,0 +1,652 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_creation_dynamic_wizard +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\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_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_dynamic_wizard +msgid "" +"A dynamic wizard that helps user to create product based on business " +"configuration" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__active +msgid "Active" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__answer_ids +msgid "Answer" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__answer_required +msgid "Answer Required" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__answer_ids +msgid "Answers for the question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Apply if" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Archived" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Cancel" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__child_ids +msgid "Child questions" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Children questions" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__complete_name +msgid "Complete Name" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_operator +msgid "Conditional Operator" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_template_form_view +msgid "Create (Wizard)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.actions.server,name:product_creation_dynamic_wizard.product_creation_dynamic_wizard_action +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_creation_dynamic_wizard +msgid "Create new product" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_template_form_view +msgid "Create product using wizard" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__create_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__create_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__create_uid +msgid "Created by" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__create_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__create_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__create_date +msgid "Created on" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py:0 +#, python-format +msgid "Creating %(product_name)s..." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_company_id +msgid "Current Company" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_complete_name +msgid "Current Complete Name" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_question +msgid "Current Question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_step +msgid "Current Step" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__custom +msgid "Custom" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__custom_view +msgid "Custom View" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Custom response" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__default_answer_id +msgid "Default answer" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__default_field_value +msgid "Default value" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__default_field_value +msgid "Default value for the field" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__logical_default_values +msgid "" +"Default values are used only if values are not set. Expected a valid json " +"format, before parsing json special variables are replaced (ie: " +"current_company_id)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__disable_create_product_button +msgid "Disable create product button" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__display_field_name +msgid "Display Field Name" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template__display_name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__display_name +msgid "Display Name" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__answer_required +msgid "Does a response is required on the current question?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result_answer_id +msgid "Expected answer on parent question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result +msgid "Expected result on parent question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__field_id +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__field +msgid "Field" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Group By" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template__id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings__id +msgid "ID" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__display_field_name +msgid "If check, display the label field in the wizard." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_needaction +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_unread +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_error +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__is_automatic +msgid "" +"If checked, the question will be automatically processed with the given " +"default value without asking the user for a value." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_expected_result +msgid "If parent response is match run the current node and childs" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__custom_view +msgid "If set use the definition as it." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#, python-format +msgid "" +"Incorrect JSON data - field; '%(field_name)s':\n" +"Error:\n" +"%(error)s\n" +"Incorrect data: \n" +"%(data)s" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#, python-format +msgid "" +"Incorrect XML data - field '%(field_name)s':\n" +"Error:\n" +"%(error)s\n" +"Incorrect data: \n" +"%(data)s" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__is_automatic +msgid "Is Automatic" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__conditional_question +msgid "Is conditional" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_custom +msgid "Is this product active?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__parent_question_type +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__question_type +msgid "" +"Kind of questions: \n" +"* **field**: Used to set a product field value (can be a node as well)\n" +"* **custom**: Usefull for nodes to ask question not related to product field\n" +"* **logical**: Logical nodes that allows to set multiple values as default value or as new value\n" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_product____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_template____last_update +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_res_config_settings____last_update +msgid "Last Modified on" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__write_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__write_uid +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__write_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__write_date +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__write_date +msgid "Last Updated on" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields.selection,name:product_creation_dynamic_wizard.selection__product_creation_question__question_type__logical +msgid "Logical" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__logical_default_values +msgid "Logical Default Values" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__logical_values +msgid "Logical Values" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_ids +msgid "Messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__name +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__name +msgid "Name" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Next" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_operator +msgid "Operator to use" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_id +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Parent" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_path +msgid "Parent Path" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__parent_question_type +msgid "Parent type" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Previous" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_product +msgid "Product" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__product_data +msgid "Product Data" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_template +msgid "Product Template" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_dynamic_wizard_settings +msgid "Product Wizard Settings" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Product attribute" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_answer +msgid "Product creation answer" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model,name:product_creation_dynamic_wizard.model_product_creation_question +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_tree_view +msgid "Product creation question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.product_creation_dynamic_wizard_view_form +msgid "Product creation wizard" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_category +msgid "Product type?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__question_id +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__question +msgid "Question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__question_type +msgid "Question Type" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/models/product_creation_question.py:0 +#: model:ir.model.constraint,message:product_creation_dynamic_wizard.constraint_product_creation_question_question_set +#, python-format +msgid "Question is required if step is not automatic" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.actions.act_window,name:product_creation_dynamic_wizard.product_creation_question_action +#: model:ir.ui.menu,name:product_creation_dynamic_wizard.menu_product_creation_question_action +msgid "Questions" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__current_company_id +msgid "" +"Related to the current company_id that will be set on productwhich can be used in: \n" +"* custom view while creating x2m object (ie: `contex=\"{'default_company_id': current_company_id}\"`)\n" +"* in default values while creating x2m objects \n" +"(ie: [[6,0,{'company_id': current_company_id}]])" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu +msgid "Sale consumable product?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_search +msgid "Search Product creation question" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_answer__sequence +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__sequence +msgid "Sequence" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu_packaging_custom_view +msgid "Setup packaging?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__name +msgid "Technical name used to help organized questions." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__question +msgid "The question displayed to the end user" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "" +"This question and its children will be applied only if response to the\n" +" parent questions satisfy the following operator and value." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__logical_values +msgid "" +"Those values will erase existing values. Expected a valid json format, " +"before parsing json special variables are replaced (ie: current_company_id)" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_question__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "Values" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,help:product_creation_dynamic_wizard.field_product_creation_question__conditional_question +msgid "What ever the question depends on a previous fields value" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_name +msgid "What would be the product template name?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_consu_color +msgid "What's the color?" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:ir.model.fields,field_description:product_creation_dynamic_wizard.field_product_creation_dynamic_wizard__wizard_steps +msgid "Wizard Steps" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model_terms:ir.ui.view,arch_db:product_creation_dynamic_wizard.view_product_creation_question_from_view +msgid "you need to save the question before you can set the default answer." +msgstr "" + +#. module: product_creation_dynamic_wizard +#: code:addons/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py:0 +#, python-format +msgid "a new product" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_active +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_active_yes +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_default_active_yes +msgid "automatic step: active" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_purchase_service +msgid "automatic step: purchase ok" +msgstr "" + +#. module: product_creation_dynamic_wizard +#: model:product.creation.question,question:product_creation_dynamic_wizard.product_creation_question_sale_service +msgid "automatic step: sale ok" +msgstr "" diff --git a/product_creation_dynamic_wizard/models/__init__.py b/product_creation_dynamic_wizard/models/__init__.py new file mode 100644 index 000000000..a20475642 --- /dev/null +++ b/product_creation_dynamic_wizard/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_creation_answer +from . import product_creation_question +from . import product_template +from . import res_config_settings diff --git a/product_creation_dynamic_wizard/models/product_creation_answer.py b/product_creation_dynamic_wizard/models/product_creation_answer.py new file mode 100644 index 000000000..251e8f6be --- /dev/null +++ b/product_creation_dynamic_wizard/models/product_creation_answer.py @@ -0,0 +1,19 @@ +# Copyright 2025 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ProductCreationAnswer(models.Model): + _name = "product.creation.answer" + _description = "Product creation answer" + + _order = "sequence" + + question_id = fields.Many2one( + "product.creation.question", + required=True, + ondelete="cascade", + ) + name = fields.Char(required=True) + sequence = fields.Integer(required=True, default=10) diff --git a/product_creation_dynamic_wizard/models/product_creation_question.py b/product_creation_dynamic_wizard/models/product_creation_question.py new file mode 100644 index 000000000..f5dd87335 --- /dev/null +++ b/product_creation_dynamic_wizard/models/product_creation_question.py @@ -0,0 +1,260 @@ +# Copyright 2025 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import json + +from lxml import etree + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductCreationQuestion(models.Model): + _name = "product.creation.question" + _inherit = [ + "mail.thread", + ] + _description = "Product creation question" + _parent_name = "parent_id" + _parent_store = True + _rec_name = "complete_name" + _order = "sequence, complete_name" + + _sql_constraints = [ + ( + "question_set", + "CHECK(is_automatic OR COALESCE(question, '') != '')", + _("Question is required if step is not automatic"), + ) + ] + name = fields.Char( + required=True, + index=True, + help="Technical name used to help organized questions.", + ) + question = fields.Char( + translate=True, tracking=True, help="The question displayed to the end user" + ) + complete_name = fields.Char( + compute="_compute_complete_name", store=True, recursive=True + ) + parent_id = fields.Many2one( + "product.creation.question", + index=True, + ondelete="cascade", + ) + parent_path = fields.Char( + index=True, + unaccent=False, + ) + child_ids = fields.One2many( + "product.creation.question", "parent_id", string="Child questions" + ) + sequence = fields.Integer(index=True, default=10) + active = fields.Boolean(default=True, tracking=True) + answer_required = fields.Boolean( + default=True, + tracking=True, + help="Does a response is required on the current question?", + ) + question_type = fields.Selection( + [ + ("field", "Field"), + ("custom", "Custom"), + ("logical", "Logical"), + ], + required=True, + tracking=True, + default="field", + help=( + "Kind of questions: \n" + "* **field**: Used to set a product field value (can be a node as well)\n" + "* **custom**: Usefull for nodes to ask question not " + "related to product field\n" + "* **logical**: Logical nodes that allows to set multiple values as " + "default value or as new value\n" + ), + ) + + # question_type == 'field' fields + field_id = fields.Many2one( + "ir.model.fields", + tracking=True, + domain=[("model", "in", ["product.template", "product.product"])], + ) + display_field_name = fields.Boolean( + help="If check, display the label field in the wizard." + ) + default_field_value = fields.Text( + string="Default value", + tracking=True, + help="Default value for the field", + ) + custom_view = fields.Text(tracking=True, help="If set use the definition as it.") + + # question_type == 'custom' fields + answer_ids = fields.One2many( + "product.creation.answer", + "question_id", + copy=True, + help="Answers for the question", + ) + default_answer_id = fields.Many2one( + "product.creation.answer", + string="Default answer", + tracking=True, + ) + + # question_type == "logical" + logical_default_values = fields.Text( + help=( + "Default values are used only if values are not set. " + "Expected a valid json format, before parsing json " + "special variables are replaced (ie: current_company_id)" + ) + ) + logical_values = fields.Text( + help=( + "Those values will erase existing values. " + "Expected a valid json format, before parsing json " + "special variables are replaced (ie: current_company_id)" + ) + ) + + conditional_question = fields.Boolean( + string="Is conditional", + compute="_compute_conditional_question", + default=False, + store=True, + readonly=False, + tracking=True, + help="What ever the question depends on a previous fields value", + ) + parent_question_type = fields.Selection( + related="parent_id.question_type", string="Parent type" + ) + conditional_operator = fields.Selection( + [ + ("==", "=="), + ("!=", "!="), + ], + required=True, + tracking=True, + default="==", + help="Operator to use", + ) + conditional_expected_result = fields.Char( + string="Expected result on parent question", + tracking=True, + help="If parent response is match run the current node and childs", + ) + conditional_expected_result_answer_id = fields.Many2one( + "product.creation.answer", + tracking=True, + string="Expected answer on parent question", + ) + is_automatic = fields.Boolean( + compute="_compute_is_automatic", + store=True, + readonly=False, + tracking=True, + help=( + "If checked, the question will be automatically " + "processed with the given default value without " + "asking the user for a value." + ), + ) + automatic_save = fields.Boolean( + string="Automatic save", + default=False, + help="If checked, the product will be saved at this step automatically.", + ) + + @api.model + def _validate_xml_view(self, xml_view, fieldname): + if xml_view: + try: + etree.fromstring(xml_view) + except etree.XMLSyntaxError as ex: + raise ValidationError( + _( + "Incorrect XML data - field '%(field_name)s':\n" + "Error:\n" + "%(error)s\n" + "Incorrect data: \n" + "%(data)s" + ) + % { + "field_name": self._fields[fieldname].string, + "data": xml_view, + "error": ex, + } + ) from ex + + @api.model + def _validate_json(self, data, fieldname): + if data: + try: + json.loads(data) + except json.decoder.JSONDecodeError as ex: + raise ValidationError( + _( + "Incorrect JSON data - field; '%(field_name)s':\n" + "Error:\n" + "%(error)s\n" + "Incorrect data: \n" + "%(data)s" + ) + % { + "field_name": self._fields[fieldname].string, + "data": data, + "error": ex, + } + ) from ex + + @api.constrains("custom_view") + def _validate_custom_view(self): + for rec in self: + self._validate_xml_view(rec.custom_view, "custom_view") + + @api.constrains("logical_default_values") + def _validate_logical_default_values(self): + for rec in self: + self._validate_json(rec.logical_default_values, "logical_default_values") + + @api.constrains("logical_values") + def _validate_logical_values(self): + for rec in self: + self._validate_json( + rec.logical_values + and rec.logical_values.replace( + "current_company_id", str(self.env.company.id) + ), + "logical_values", + ) + + @api.depends("question_type") + def _compute_is_automatic(self): + for question in self: + if question.question_type == "logical": + question.is_automatic = True + else: + question.is_automatic = False + + @api.depends("parent_id", "parent_id.question_type") + def _compute_conditional_question(self): + for question in self: + if question.parent_id and question.parent_id.question_type != "logical": + question.conditional_question = question.conditional_question + else: + question.conditional_question = False + + @api.depends("name", "parent_id.complete_name") + def _compute_complete_name(self): + for question in self: + if question.parent_id: + question.complete_name = "{} / {}".format( + question.parent_id.complete_name, question.name + ) + else: + question.complete_name = question.name diff --git a/product_creation_dynamic_wizard/models/product_template.py b/product_creation_dynamic_wizard/models/product_template.py new file mode 100644 index 000000000..e08fce3f2 --- /dev/null +++ b/product_creation_dynamic_wizard/models/product_template.py @@ -0,0 +1,61 @@ +# Copyright 2025 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from lxml import etree + +from odoo import api, models +from odoo.tools import str2bool + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _update_cache(self, values, validate=True): + """Update the cache of ``self`` with ``values``. + + :param values: dict of field values, in any format. + :param validate: whether values must be checked + """ + if self.env.context.get("product_creation_wizard", False): + values = { + key: value + for key, value in values.items() + if not key.startswith("current_") + } + return super()._update_cache(values, validate=validate) + + def action_open_product_creation_dynamic_wizard(self): + return self.env["product.creation.dynamic.wizard"].create({}).get_next_action() + + @api.model + def get_views(self, views, options=None): + result = super().get_views(views, options=options) + self._disable_create_button(result.get("views", {})) + return result + + @api.model + def _disable_create_button(self, views): + if str2bool( + self.env["ir.config_parameter"].get_param( + "product_creation_dynamic_wizard.disable_create_product_button", + default="False", + ), + default=False, + ): + for view in views.values(): + doc = etree.fromstring(view["arch"]) + doc.attrib.update({"create": "0"}) + view["arch"] = etree.tostring(doc, encoding="unicode") + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def action_open_product_creation_dynamic_wizard(self): + return self.env["product.creation.dynamic.wizard"].create({}).get_next_action() + + @api.model + def get_views(self, views, options=None): + result = super().get_views(views, options=options) + self.env["product.template"]._disable_create_button(result.get("views", {})) + return result diff --git a/product_creation_dynamic_wizard/models/res_config_settings.py b/product_creation_dynamic_wizard/models/res_config_settings.py new file mode 100644 index 000000000..0ba782512 --- /dev/null +++ b/product_creation_dynamic_wizard/models/res_config_settings.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + disable_create_product_button = fields.Boolean( + "Disable create product button", + config_parameter="product_creation_dynamic_wizard.disable_create_product_button", + ) diff --git a/product_creation_dynamic_wizard/pyproject.toml b/product_creation_dynamic_wizard/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/product_creation_dynamic_wizard/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_creation_dynamic_wizard/readme/CONFIGURE.md b/product_creation_dynamic_wizard/readme/CONFIGURE.md new file mode 100644 index 000000000..957a4f610 --- /dev/null +++ b/product_creation_dynamic_wizard/readme/CONFIGURE.md @@ -0,0 +1,87 @@ +To configure the dynamic wizard, go to PIM \> Product Wizard Settings \> +Questions as a PIM manager. + +Wizard configuration can be a bit tricky as more features are added. As +of now, available question attributes include: + +- **Name**: Question name, shown in the bottom-right of the wizard to + help with maintenance (especially if a screenshot is taken). +- **Parent question**: Questions can be nested like a tree. If the + parent condition passes, its branch will be processed; otherwise, + child questions are skipped. +- **Is Automatic**: Hides the question from the user and allows setting + values automatically. +- **Question**: The actual prompt shown to the user, unless it's + automatic. +- **Sequence**: Defines the question order within its branch. +- **Question Type**: + - **Field** – sets a product template field + - **Custom** – lets the user choose from a custom set of values + - **Logical** – groups multiple field updates under one step and/or + organize questions +- **Is Conditional**: Only displays the step and children if the + condition is met (requires a non-logical parent). +- **Answer Required**: Forces the user to provide an answer before + proceeding. + +**Apply If (page)**: Setup condition (display if *Is conditional* is +checked ) + +- **Conditional Operator**: Currently supports == and !=. +- **Expected result**: The value to match on the parent question. For: + - **Custom** – select from predefined values + - **Field** – use the technical value (666 for many2one, + True/\`False\` for booleans, technical value on field selection ie: + product / service / consu, ...) + +**Product Attribute (page)**: Shown if *Question Type* is **Field**. + +- **Field**: Product template field to set + +- **Display Field Name**: Show/hide the field label in the wizard + +- **Default Value**: A string, depending on field type: + + - many2one → record ID (e.g., 1) + - boolean → true for checked/true value, empty string or false for + unchecked/false value + - x2m → valid JSON string (e.g. \[\[0, 0, {"name": "Box 20", "qty": + 20}\]\]) + +- **Custom View**: XML snippet to control how the field is rendered: + + + +**Custom Response (page)**: Shown if *Question Type* is **Custom** + +- List of valid responses +- **Default Answer**: Can be set after the question is saved + +**Values (page)**: Shown if *Question Type* is **Logical** + +- **Default Values**: Only set values that aren't already defined by a + previous step (even False or empty string counts as defined) +- **Values**: Always override values, even if already set by another + step. JSON format (e.g. {"type": "product", "company_id": + current_company_id}) + +**Children Questions**: Executed only if the parent condition is met. + +> [!NOTE] +> If a name is defined, it will be used as the wizard's title. It's a +> good idea to ask for the product name first, so users remember what +> they’re doing. + +Special company_id cases: + +- company_id is auto-set by the module to the current company; a + different default will be ignored. +- You can use special variable current_company_id that will be replaced + by the id of the company_id already set. this can be used in + - default values for x2m fields to set the company_id + - in custom view to set the default_company_id in context key + - in logical values for x2m fields diff --git a/product_creation_dynamic_wizard/readme/CONTRIBUTORS.md b/product_creation_dynamic_wizard/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..8a6fef2c4 --- /dev/null +++ b/product_creation_dynamic_wizard/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Pierre Verkest \ diff --git a/product_creation_dynamic_wizard/readme/DESCRIPTION.md b/product_creation_dynamic_wizard/readme/DESCRIPTION.md new file mode 100644 index 000000000..245d4c6e3 --- /dev/null +++ b/product_creation_dynamic_wizard/readme/DESCRIPTION.md @@ -0,0 +1,16 @@ +This module introduces a fully customizable, dynamic wizard to guide +users through product creation. + +As Odoo evolves and integrates more features, the standard product form +becomes increasingly complex. This wizard simplifies the process by +presenting a series of dynamic questions tailored to your business +needs, ensuring better data consistency, faster onboarding, and reduced +user errors. + +## Key Features + +- Fully configurable step-by-step product creation flow +- Each step corresponds to a field, form, or logical decision +- Conditions allow custom paths depending on answers +- Default values can be set and inherited across steps +- Supports custom views, model relations, and field domains diff --git a/product_creation_dynamic_wizard/readme/ROADMAP.md b/product_creation_dynamic_wizard/readme/ROADMAP.md new file mode 100644 index 000000000..ab9399b50 --- /dev/null +++ b/product_creation_dynamic_wizard/readme/ROADMAP.md @@ -0,0 +1,12 @@ +- Allow reuse of field’s values as default for other +- Clicking "Previous" should undo values set in the current step +- Dynamically adapt "expected value" input based on parent field type +- Validate default values (especially for x2m) +- Allow conditions on more than just parent question values +- Support for complex conditions (more than one with AND / OR...) +- Add toolbar button for wizard launch (avoid menu + action dropdown) +- Skip validation/saving when going back with "Previous" +- Connect to an LLM to suggest default values based on previous answers +- managed translated fields +- Split module to make it agnostic from product model ( could be useful + on res.partner as well) diff --git a/product_creation_dynamic_wizard/readme/USAGE.md b/product_creation_dynamic_wizard/readme/USAGE.md new file mode 100644 index 000000000..4b6a840c7 --- /dev/null +++ b/product_creation_dynamic_wizard/readme/USAGE.md @@ -0,0 +1,11 @@ +1. Go to **Products \> Create via Wizard** or use the create products + via the Create (Wizard) on form view +2. The user is prompted with step-by-step questions +3. Each answer may influence the next question shown +4. At the end, the product is created and opened automatically + +Benefits: + +- New users don't need to know all product form details +- Predefined flows help enforce business logic +- Easy to extend or maintain when product policies change diff --git a/product_creation_dynamic_wizard/security/ir.model.access.csv b/product_creation_dynamic_wizard/security/ir.model.access.csv new file mode 100644 index 000000000..cad6b9c0d --- /dev/null +++ b/product_creation_dynamic_wizard/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_creation_question,access_product_creation_question,model_product_creation_question,base.group_user,1,0,0,0 +access_product_creation_question_manager,access_product_creation_question_manager,model_product_creation_question,base.group_system,1,1,1,1 +access_product_creation_answer,access_product_creation_answer,model_product_creation_answer,base.group_user,1,0,0,0 +access_product_creation_answer_manager,access_product_creation_answer_manager,model_product_creation_answer,base.group_system,1,1,1,1 +access_product_creation_dynamic_wizard,access_product_creation_dynamic_wizard,model_product_creation_dynamic_wizard,base.group_user,1,1,1,1 diff --git a/product_creation_dynamic_wizard/static/description/index.html b/product_creation_dynamic_wizard/static/description/index.html new file mode 100644 index 000000000..78bdd6e22 --- /dev/null +++ b/product_creation_dynamic_wizard/static/description/index.html @@ -0,0 +1,568 @@ + + + + + +Product creation dynamic wizard + + + +
+

Product creation dynamic wizard

+ + +

Beta License: AGPL-3 OCA/odoo-pim Translate me on Weblate Try me on Runboat

+

This module introduces a fully customizable, dynamic wizard to guide +users through product creation.

+

As Odoo evolves and integrates more features, the standard product form +becomes increasingly complex. This wizard simplifies the process by +presenting a series of dynamic questions tailored to your business +needs, ensuring better data consistency, faster onboarding, and reduced +user errors.

+
+

Key Features

+
    +
  • Fully configurable step-by-step product creation flow
  • +
  • Each step corresponds to a field, form, or logical decision
  • +
  • Conditions allow custom paths depending on answers
  • +
  • Default values can be set and inherited across steps
  • +
  • Supports custom views, model relations, and field domains
  • +
+

Table of contents

+ +
+

Configuration

+

To configure the dynamic wizard, go to PIM > Product Wizard Settings > +Questions as a PIM manager.

+

Wizard configuration can be a bit tricky as more features are added. As +of now, available question attributes include:

+
    +
  • Name: Question name, shown in the bottom-right of the wizard to +help with maintenance (especially if a screenshot is taken).
  • +
  • Parent question: Questions can be nested like a tree. If the +parent condition passes, its branch will be processed; otherwise, +child questions are skipped.
  • +
  • Is Automatic: Hides the question from the user and allows setting +values automatically.
  • +
  • Question: The actual prompt shown to the user, unless it’s +automatic.
  • +
  • Sequence: Defines the question order within its branch.
  • +
  • Question Type:
      +
    • Field – sets a product template field
    • +
    • Custom – lets the user choose from a custom set of values
    • +
    • Logical – groups multiple field updates under one step and/or +organize questions
    • +
    +
  • +
  • Is Conditional: Only displays the step and children if the +condition is met (requires a non-logical parent).
  • +
  • Answer Required: Forces the user to provide an answer before +proceeding.
  • +
+

Apply If (page): Setup condition (display if Is conditional is +checked )

+
    +
  • Conditional Operator: Currently supports == and !=.
  • +
  • Expected result: The value to match on the parent question. For:
      +
    • Custom – select from predefined values
    • +
    • Field – use the technical value (666 for many2one, +True/`False` for booleans, technical value on field selection ie: +product / service / consu, …)
    • +
    +
  • +
+

Product Attribute (page): Shown if Question Type is Field.

+
    +
  • Field: Product template field to set

    +
  • +
  • Display Field Name: Show/hide the field label in the wizard

    +
  • +
  • Default Value: A string, depending on field type:

    +
      +
    • many2one → record ID (e.g., 1)
    • +
    • boolean → true for checked/true value, empty string or false for +unchecked/false value
    • +
    • x2m → valid JSON string (e.g. [[0, 0, {“name”: “Box 20”, “qty”: +20}]])
    • +
    +
  • +
  • Custom View: XML snippet to control how the field is rendered:

    +
    +<field
    +    name="packaging_ids"
    +    nolabel="1"
    +    context="{'tree_view_ref':'product.product_packaging_tree_view2', 'form_view_ref':'product.product_packaging_form_view2', 'default_name': 'Box of 10', 'default_qty': 10}"
    +/>
    +
    +
  • +
+

Custom Response (page): Shown if Question Type is Custom

+
    +
  • List of valid responses
  • +
  • Default Answer: Can be set after the question is saved
  • +
+

Values (page): Shown if Question Type is Logical

+
    +
  • Default Values: Only set values that aren’t already defined by a +previous step (even False or empty string counts as defined)
  • +
  • Values: Always override values, even if already set by another +step. JSON format (e.g. {“type”: “product”, “company_id”: +current_company_id})
  • +
+

Children Questions: Executed only if the parent condition is met.

+
+

Note

+

If a name is defined, it will be used as the wizard’s title. It’s a +good idea to ask for the product name first, so users remember what +they’re doing.

+
+

Special company_id cases:

+
    +
  • company_id is auto-set by the module to the current company; a +different default will be ignored.
  • +
  • You can use special variable current_company_id that will be replaced +by the id of the company_id already set. this can be used in
      +
    • default values for x2m fields to set the company_id
    • +
    • in custom view to set the default_company_id in context key
    • +
    • in logical values for x2m fields
    • +
    +
  • +
+
+
+

Usage

+
    +
  1. Go to Products > Create via Wizard or use the create products via +the Create (Wizard) on form view
  2. +
  3. The user is prompted with step-by-step questions
  4. +
  5. Each answer may influence the next question shown
  6. +
  7. At the end, the product is created and opened automatically
  8. +
+

Benefits:

+
    +
  • New users don’t need to know all product form details
  • +
  • Predefined flows help enforce business logic
  • +
  • Easy to extend or maintain when product policies change
  • +
+
+
+

Known issues / Roadmap

+
    +
  • Allow reuse of field’s values as default for other
  • +
  • Clicking “Previous” should undo values set in the current step
  • +
  • Dynamically adapt “expected value” input based on parent field type
  • +
  • Validate default values (especially for x2m)
  • +
  • Allow conditions on more than just parent question values
  • +
  • Support for complex conditions (more than one with AND / OR…)
  • +
  • Add toolbar button for wizard launch (avoid menu + action dropdown)
  • +
  • Skip validation/saving when going back with “Previous”
  • +
  • Connect to an LLM to suggest default values based on previous answers
  • +
  • managed translated fields
  • +
  • Split module to make it agnostic from product model ( could be useful +on res.partner as well)
  • +
+
+
+

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.

+
+ +
+
+

Authors

+ +
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

petrus-v

+

This module is part of the OCA/odoo-pim project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+ + diff --git a/product_creation_dynamic_wizard/static/src/js/form_button.js b/product_creation_dynamic_wizard/static/src/js/form_button.js new file mode 100644 index 000000000..b747b0ed0 --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/js/form_button.js @@ -0,0 +1,14 @@ +odoo.define("product_creation_dynamic_wizard.form_button", function (require) { + "use strict"; + var FormController = require("web.FormController"); + FormController.include({ + events: _.extend({}, FormController.prototype.events, { + "click .o_form_button_create_wizard": "_onProductCreationWizardOpen", + }), + _onProductCreationWizardOpen: function () { + this.do_action( + "product_creation_dynamic_wizard.product_creation_dynamic_wizard_action" + ); + }, + }); +}); diff --git a/product_creation_dynamic_wizard/static/src/js/kanban_button.js b/product_creation_dynamic_wizard/static/src/js/kanban_button.js new file mode 100644 index 000000000..ca5cb07b7 --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/js/kanban_button.js @@ -0,0 +1,14 @@ +odoo.define("product_creation_dynamic_wizard.kanban_button", function (require) { + "use strict"; + var KanbanController = require("web.KanbanController"); + KanbanController.include({ + events: _.extend({}, KanbanController.prototype.events, { + "click .o-kanban-button-new-wizard": "_onProductCreationWizardOpen", + }), + _onProductCreationWizardOpen: function () { + this.do_action( + "product_creation_dynamic_wizard.product_creation_dynamic_wizard_action" + ); + }, + }); +}); diff --git a/product_creation_dynamic_wizard/static/src/js/tree_button.js b/product_creation_dynamic_wizard/static/src/js/tree_button.js new file mode 100644 index 000000000..e5ec4489f --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/js/tree_button.js @@ -0,0 +1,17 @@ +odoo.define( + "product_creation_dynamic_wizard.tree_button_create_wizard", + function (require) { + "use strict"; + var ListController = require("web.ListController"); + ListController.include({ + events: _.extend({}, ListController.prototype.events, { + "click .o_list_button_add_wizard": "_onProductCreationWizardOpen", + }), + _onProductCreationWizardOpen: function () { + this.do_action( + "product_creation_dynamic_wizard.product_creation_dynamic_wizard_action" + ); + }, + }); + } +); diff --git a/product_creation_dynamic_wizard/static/src/xml/form_button.xml b/product_creation_dynamic_wizard/static/src/xml/form_button.xml new file mode 100644 index 000000000..db9a49df7 --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/xml/form_button.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/product_creation_dynamic_wizard/static/src/xml/kanban_button.xml b/product_creation_dynamic_wizard/static/src/xml/kanban_button.xml new file mode 100644 index 000000000..9c03b21e7 --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/xml/kanban_button.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/product_creation_dynamic_wizard/static/src/xml/tree_button.xml b/product_creation_dynamic_wizard/static/src/xml/tree_button.xml new file mode 100644 index 000000000..469e2a0dd --- /dev/null +++ b/product_creation_dynamic_wizard/static/src/xml/tree_button.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/product_creation_dynamic_wizard/tests/__init__.py b/product_creation_dynamic_wizard/tests/__init__.py new file mode 100644 index 000000000..4cb14f94a --- /dev/null +++ b/product_creation_dynamic_wizard/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_product_creation_dynamic_wizard +from . import test_product_creation_question +from . import test_product diff --git a/product_creation_dynamic_wizard/tests/fake_product.py b/product_creation_dynamic_wizard/tests/fake_product.py new file mode 100644 index 000000000..e41785a8f --- /dev/null +++ b/product_creation_dynamic_wizard/tests/fake_product.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class FakeProduct(models.Model): + _inherit = "product.product" # pylint: disable=consider-merging-classes-inherited + + product_attribute = fields.Char() diff --git a/product_creation_dynamic_wizard/tests/test_product.py b/product_creation_dynamic_wizard/tests/test_product.py new file mode 100644 index 000000000..edcfc527f --- /dev/null +++ b/product_creation_dynamic_wizard/tests/test_product.py @@ -0,0 +1,78 @@ +from lxml import etree +from parameterized import parameterized + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProduct(TransactionCase): + @parameterized.expand([("product.template"), ("product.product",)]) + def test_action_open_product_creation_dynamic_wizard(self, model): + action = self.env[model].action_open_product_creation_dynamic_wizard() + wizard = self.env["product.creation.dynamic.wizard"].browse(action["res_id"]) + self.assertTrue(wizard.exists()) + self.assertEqual(wizard.current_step, 0) + + @parameterized.expand([("product.template"), ("product.product",)]) + def test_load_views_button_create_disabled(self, model): + self.env["ir.config_parameter"].set_param( + "product_creation_dynamic_wizard.disable_create_product_button", "True" + ) + result = self.env[model].get_views( + [ + (False, "tree"), + (False, "form"), + (False, "kanban"), + (False, "pivot"), + ] + ) + for view in result["views"].values(): + doc = etree.XML(view["arch"]) + self.assertTrue(doc.attrib.get("create", False)) + + @parameterized.expand([("product.template"), ("product.product",)]) + def test_load_views_button_create_not_disabled(self, model): + result = self.env[model].get_views( + [ + (False, "tree"), + (False, "form"), + (False, "kanban"), + (False, "pivot"), + ] + ) + for view in result["views"].values(): + doc = etree.XML(view["arch"]) + self.assertFalse(doc.attrib.get("create", False)) + + def test_update_cache(self): + # in some fields (likes seller_ids) the wizard will call + # onchange that call _update_cache on product.template with + # wizard fields current_*, testing that _update_cache + # using those fields do not raises + template = self.env["product.template"].new() + template.with_context(product_creation_wizard=True)._update_cache( + {"current_wizard_field": "test", "name": "valid product template field"} + ) + self.assertEqual( + self.env.cache.get(template, self.env["product.template"]._fields["name"]), + "valid product template field", + ) + + def test_update_cache_without_context_pass(self): + # hydrating the update cache must be done only on wizard context + template = self.env["product.template"].new() + template._update_cache({"name": "valid product template field"}) + self.assertEqual( + self.env.cache.get(template, self.env["product.template"]._fields["name"]), + "valid product template field", + ) + + def test_update_cache_without_context_failed(self): + # hydrating the update cache must be done only on wizard context + with self.assertRaisesRegex( + ValueError, + r"Invalid field 'current_wizard_field' on model 'product.template'", + ): + self.env["product.template"].new()._update_cache( + {"current_wizard_field": "test", "name": "other field"} + ) diff --git a/product_creation_dynamic_wizard/tests/test_product_creation_dynamic_wizard.py b/product_creation_dynamic_wizard/tests/test_product_creation_dynamic_wizard.py new file mode 100644 index 000000000..4749edfc9 --- /dev/null +++ b/product_creation_dynamic_wizard/tests/test_product_creation_dynamic_wizard.py @@ -0,0 +1,480 @@ +import json + +from lxml import etree +from odoo_test_helper import FakeModelLoader + +from odoo.tests import Form, TransactionCase, tagged + +from odoo.addons.product_creation_dynamic_wizard.wizards import ( + product_creation_dynamic_wizard as W, +) + +Wizard = W.Wizard +WizardStep = W.WizardStep + + +@tagged("post_install", "-at_install") +class TestProductCreationDynamicWizardWithoutDemo(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.env["product.creation.question"].search([]).unlink() + + def test_force_false_value_in_logical_step(self): + self.env["product.creation.question"].create( + { + "name": "Force false purchase ok", + "question_type": "logical", + "sequence": 5, + "logical_values": '{"purchase_ok": false}', + } + ) + self.env["product.creation.question"].create( + { + "name": "default purchase ok should be ignored ignored", + "question_type": "logical", + "sequence": 10, + "logical_default_values": '{"purchase_ok": true}', + } + ) + + product_creation_wizard = self.env["product.creation.dynamic.wizard"].create( + { + "product_data": { + "name": "Test", + "purchase_ok": True, # will be overwrite by logical value + }, + } + ) + + action = product_creation_wizard.get_next_action() + product = self.env["product.template"].browse(action["res_id"]) + self.assertFalse(product.purchase_ok) + + def test_prepare_step_on_custom_step_already_set_shouldnt_use_default(self): + question = self.env["product.creation.question"].create( + { + "name": "purchase ok ?", + "question": "purchase ok ?", + "question_type": "custom", + "sequence": 5, + } + ) + answer_yes = self.env["product.creation.answer"].create( + {"question_id": question.id, "name": "Yes"} + ) + answer_no = self.env["product.creation.answer"].create( + {"question_id": question.id, "name": "No"} + ) + question.default_answer_id = answer_yes + product_creation_wizard = self.env["product.creation.dynamic.wizard"].create({}) + product_creation_wizard.write({"answer_id": answer_no.id}) + + self.assertEqual( + product_creation_wizard._prepare_product_data(), + {}, + ) + + +@tagged("post_install", "-at_install") +class TestProductCreationDynamicWizard(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_creation_wizard = cls.env["product.creation.dynamic.wizard"].create( + {} + ) + cls.product_creation_wizard.get_next_action() + + def test_wizard_data_class_serialization(self): + wizard = Wizard() + wizard.append(WizardStep(record_id=1, parent_index=-1, odoo_env=self.env)) + wizard.append(WizardStep(record_id=123, parent_index=2, odoo_env=self.env)) + + wizard2 = Wizard.from_dict(json.loads(json.dumps(wizard.to_dict())), self.env) + self.assertEqual( + wizard[0].question, + wizard2[0].question, + ) + + def test_wizard_len(self): + wizard = Wizard() + wizard.append(WizardStep(record_id=1, parent_index=-1, odoo_env=self.env)) + wizard.append(WizardStep(record_id=2, parent_index=2, odoo_env=self.env)) + self.assertEqual(len(wizard), 2) + + def test_product_creation_wizard(self): + self.assertEqual( + self.product_creation_wizard.steps[0].question, + "What would be the product template name?", + ) + + def test_product_creation_wizard_default(self): + self.product_creation_wizard._action_create() + product_template = self.product_creation_wizard.product_template_id + self.assertEqual(product_template.name, "Product name (created from wizard)") + + def test_flow_step0_without_record(self): + with Form(self.product_creation_wizard) as form_step0: + form_step0.name = "My Product name" + wizard = form_step0.save() + + wizard.action_open_next() + self.assertEqual(wizard.current_step, 1) + self.assertEqual( + wizard.steps.step_history, + [ + 0, + ], + ) + self.assertEqual(wizard.product_data["name"], "My Product name") + + def test_flow_consu_with_packing_auto(self): + wizard = self.product_creation_wizard.with_context( + active_model=self.product_creation_wizard._name, + active_id=self.product_creation_wizard.id, + ) + wizard.write({"name": "My consu product"}) + wizard.action_open_next() + wizard.write({"type": "consu"}) + wizard.action_open_next() + wizard.write({"sale_ok": False}) + wizard.action_open_next() + wizard.write({"color": 123}) + wizard.action_open_next() + answer = self.env.ref( + "product_creation_dynamic_wizard.product_creation_question_custom_answer_yes" + ) + wizard.write({"answer_id": answer.id}) + action = wizard.action_open_next() + self.assertEqual( + wizard.product_data, + { + "company_id": self.env.company.id, + "color": 123, + "name": "My consu product", + "type": "consu", + "purchase_ok": "True", + "sale_ok": False, + "active": "True", + "packaging_ids": [ + [ + 0, + 0, + { + "name": "Box 20", + "qty": 20, + "company_id": self.env.company.id, + }, + ] + ], + "volume": 3.2, + "weight": 3.1, + }, + ) + self.assertEqual(action["res_model"], "product.template") + packaging = self.env["product.template"].browse(action["res_id"]).packaging_ids + packaging.ensure_one() + self.assertEqual(packaging.name, "Box 20") + self.assertEqual(packaging.qty, 20) + self.assertEqual(packaging.company_id, self.env.company) + + def test_flow_service_product(self): + wizard = self.product_creation_wizard.with_context( + active_model=self.product_creation_wizard._name, + active_id=self.product_creation_wizard.id, + ) + wizard.write({"name": "My service product"}) + wizard.action_open_next() + self.assertEqual(wizard.step.question, "Product type?") + with Form(wizard) as form_step_type: + form_step_type.type = "service" + wizard = form_step_type.save() + + wizard.action_open_next() + self.assertEqual(wizard.step.question, "Is this product active?") + answer = self.env.ref( + "product_creation_dynamic_wizard.product_creation_question_custom_answer_no" + ) + wizard.write({"answer_id": answer.id}) + self.assertEqual(wizard.steps[9].answer_id, answer.id) + self.assertEqual( + wizard.product_data, + { + "company_id": self.env.company.id, + "name": "My service product", + "type": "service", + "purchase_ok": False, + "sale_ok": "True", + }, + ) + action = wizard.action_open_next() + self.assertEqual( + wizard.product_data, + { + "company_id": self.env.company.id, + "name": "My service product", + "type": "service", + "purchase_ok": False, + "sale_ok": "True", + "active": False, + "color": 987, + "volume": 3.2, + "weight": 3.1, + }, + ) + self.assertEqual(action["res_model"], "product.template") + + def test_read_custom_response_on_current_step(self): + self.product_creation_wizard.current_step = 9 + self.assertEqual( + self.product_creation_wizard.step.question, "Is this product active?" + ) + answer = self.env.ref( + "product_creation_dynamic_wizard.product_creation_question_custom_answer_no" + ) + self.product_creation_wizard.write({"answer_id": answer.id}) + read_result = self.product_creation_wizard.read(fields=["answer_id"], load="") + self.assertEqual( + read_result, + [ + { + "id": self.product_creation_wizard.id, + "answer_id": (answer.id, "No"), + } + ], + ) + + def test_read_custom_answer_and_wizard_field(self): + self.product_creation_wizard.current_step = 0 + self.product_creation_wizard.write( + { + "name": "Product test", + "sale_ok": True, + "purchase_ok": True, + } + ) + read_result = self.product_creation_wizard.read( + fields=["name", "current_step", "sale_ok", "color"] + ) + self.assertEqual( + read_result, + [ + { + "id": self.product_creation_wizard.id, + "name": "Product test", + "current_step": 0, + "sale_ok": True, + "color": False, + } + ], + ) + + def test_custom_questions(self): + self.product_creation_wizard.current_step = 9 + self.assertEqual( + self.product_creation_wizard.step.question, "Is this product active?" + ) + wizard = self.env["product.creation.dynamic.wizard"].with_context( + active_model=self.product_creation_wizard._name, + active_id=self.product_creation_wizard.id, + ) + form_view = wizard.get_view() + + doc = etree.XML(form_view["arch"]) + answer_node = None + for node in doc.xpath( + "//form/group[@name='question']/field[@name='answer_id']" + ): + answer_node = node + self.assertIsNotNone(answer_node) + self.assertEqual( + answer_node.attrib.get("options"), + "{'no_create': True, 'no_create_edit': True, 'no_open': True}", + ) + self.assertEqual( + answer_node.attrib.get("domain"), + f'[("question_id", "=", {self.product_creation_wizard.step.id})]', + ) + self.assertEqual(answer_node.get("required"), "1") + + def test_field_with_custom_view_with_default_value(self): + self.product_creation_wizard.current_step = 5 + self.assertEqual( + self.product_creation_wizard.step.name, + "1.1.2 - product packaging custom view", + ) + wizard = self.env["product.creation.dynamic.wizard"].with_context( + active_model=self.product_creation_wizard._name, + active_id=self.product_creation_wizard.id, + ) + form_view = wizard.get_view() + + doc = etree.XML(form_view["arch"]) + packaging_node = None + for node in doc.xpath( + "//form/group[@name='question']/field[@name='packaging_ids']" + ): + packaging_node = node + self.assertIsNotNone(packaging_node) + self.assertEqual( + packaging_node.attrib.get("context"), + "{'tree_view_ref':'product.product_packaging_tree_view2'," + " 'form_view_ref':'product.product_packaging_form_view2'," + " 'default_name': 'Box of 10', 'default_qty': 10," + " 'default_company_id': current_company_id}", + ) + + def test_read_without_field(self): + res = self.product_creation_wizard.read() + self.assertEqual(res[0]["current_step"], 0) + + def test_fields_view_get_tree(self): + self.product_creation_wizard.action_open_next() + result = self.product_creation_wizard.with_context( + active_model=self.product_creation_wizard._name, + active_id=self.product_creation_wizard.id, + ).get_view(view_type="tree") + self.assertFalse(' + + + product.creation.question.tree + product.creation.question + + + + + + + + + + + + + + + + + + + + + + + + product.creation.question.form + product.creation.question + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + product.creation.question.search + product.creation.question + + + + + + + + + + + + + + + Questions + product.creation.question + tree,form + + {'search_default_group_parent': 1,} + + + + +
diff --git a/product_creation_dynamic_wizard/wizards/__init__.py b/product_creation_dynamic_wizard/wizards/__init__.py new file mode 100644 index 000000000..201ed7f5f --- /dev/null +++ b/product_creation_dynamic_wizard/wizards/__init__.py @@ -0,0 +1 @@ +from . import product_creation_dynamic_wizard diff --git a/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py b/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py new file mode 100644 index 000000000..c5ed071f8 --- /dev/null +++ b/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.py @@ -0,0 +1,500 @@ +# Copyright 2025 Foodles (http://www.foodles.co). +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import json +import logging +from uuid import uuid4 + +from lxml import etree + +from odoo import _, api, fields, models + +from odoo.addons.base_sparse_field.models.fields import Serialized + +logger = logging.getLogger(__name__) + + +class WizardStep: + record_id: int + """id of the product.creation.question record""" + env = None + """Odoo environement""" + parent_index: int = -1 + """parent position in the current question tree""" + + answer_id: int | None = None + """in case of custom question this will save the user + answer""" + + def __init__( + self, + record_id: int, + parent_index: int, + answer_id: int | None = None, + odoo_env=None, + ): + self.record_id = record_id + self.parent_index = parent_index + self.answer_id = answer_id + self.env = odoo_env + + @property + def _record(self): + return self.env["product.creation.question"].browse(self.record_id) + + def to_dict(self): + return { + "record_id": self.record_id, + "parent_index": self.parent_index, + "answer_id": self.answer_id, + } + + def __getattr__(self, name): + return getattr(self._record, name) + + +class Wizard: + steps: list[WizardStep] = None + """list of ordered questions""" + + step_history: list[int] = None + """steps processed by the user""" + + def __init__(self, steps: list[WizardStep] = None, step_history: list[int] = None): + if not steps: + steps = [] + if not step_history: + step_history = [] + self.steps = steps + self.step_history = step_history + + def append(self, step: WizardStep): + self.steps.append(step) + + def __len__(self): + return len(self.steps) + + def __getitem__(self, position): + return self.steps[position] + + @staticmethod + def from_dict(data: dict, odoo_env) -> "Wizard": + steps = [ + WizardStep(**step, odoo_env=odoo_env) for step in data.pop("steps", []) + ] + return Wizard(steps=steps, **data) + + def to_dict(self): + return { + "steps": [step.to_dict() for step in self.steps], + "step_history": self.step_history, + } + + +class ProductCreationDynamicWizard(models.TransientModel): + _name = "product.creation.dynamic.wizard" + _description = ( + "A dynamic wizard that helps user to create product " + "based on business configuration" + ) + + wizard_steps = Serialized(default=lambda self: self._default_wizard_steps()) + product_data = Serialized(default=lambda self: self._default_product_data()) + already_written_template_data = Serialized(default={}) + already_written_product_data = Serialized(default={}) + current_step = fields.Integer(default=-1) + current_question = fields.Char(compute="_compute_current_fields") + current_complete_name = fields.Char(compute="_compute_current_fields") + current_company_id = fields.Many2one( + "res.company", + compute="_compute_current_fields", + help=( + "Related to the current company_id that will be set on product" + "which can be used in: \n" + "* custom view while creating x2m object (ie: " + "`contex=\"{'default_company_id': current_company_id}\"`)\n" + "* in default values while creating x2m objects \n" + "(ie: [[6,0,{'company_id': current_company_id}]])" + ), + ) + product_template_id = fields.Many2one( + "product.template", + ) + + @api.model + def _populate_wizard_steps(self, wizard: Wizard, question, parent_index=-1): + index = len(wizard) + wizard.append( + WizardStep( + record_id=question.id, + parent_index=parent_index, + odoo_env=self.env, + ) + ) + for child_question in question.child_ids: + self._populate_wizard_steps(wizard, child_question, parent_index=index) + + def _default_wizard_steps(self): + wizard = Wizard() + root_questions = self.env["product.creation.question"].search( + [("parent_id", "=", False)] + ) + for root_question in root_questions: + self._populate_wizard_steps(wizard, root_question) + return wizard.to_dict() + + @api.model + def _default_product_data(self): + return { + "company_id": self.env.company.id, + "type": "consu", + } + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + result = super().get_view(view_id, view_type, **options) + if view_type == "form": + self._apply_step(result) + return result + + @api.model + def _apply_step_field(self, current_step, node, result): + if current_step.custom_view: + node.append(etree.fromstring(current_step.custom_view)) + else: + node.append( + etree.Element( + "field", + name=current_step.field_id.name, + attrib={ + "required": "1" if current_step.answer_required else "0", + "nolabel": "0" if current_step.display_field_name else "1", + }, + ) + ) + values_model = result["models"] + values_model[self._name].update( + self.env[current_step.field_id.model].fields_get( + allfields=[current_step.field_id.name] + ) + ) + result["models"] = values_model + + @api.model + def _apply_step_custom(self, current_step, node, result): + node.append( + etree.Element( + "field", + name="answer_id", + attrib={ + "domain": f'[("question_id", "=", {current_step.record_id})]', + "options": "{'no_create': True, 'no_create_edit': True, " + "'no_open': True}", + "nolabel": "1", + "required": "1" if current_step.answer_required else "0", + }, + ) + ) + result["models"]["answer_id"] = self.env[ + "product.creation.question" + ].fields_get(allfields=["default_answer_id"])["default_answer_id"] + + @api.model + def _apply_step(self, result): + doc = etree.fromstring(result["arch"]) + current_step = None + if self.env.context.get("active_model") == self._name: + wizard = self.browse(self.env.context.get("active_id")) + if wizard.exists(): + wizard.invalidate_recordset() + current_step = wizard.step + if not current_step: + current_step = self.new().steps[0] + for node in doc.xpath("//form/group[@name='question']"): + if current_step.question_type == "field": + self._apply_step_field(current_step, node, result) + if current_step.question_type == "custom": + self._apply_step_custom(current_step, node, result) + + self.env["ir.ui.view"].postprocess_and_fields( + node, model=self._name, validate=False + ) + + result["arch"] = etree.tostring(doc, encoding="unicode") + + @api.model + def _split_fieldnames(self, fields: list[str]) -> tuple[list[str], list[str]]: + wizard_fields = [] + other_fields = [] + for fieldname in fields: + if fieldname in self._fields: + wizard_fields.append(fieldname) + else: + other_fields.append(fieldname) + return wizard_fields, other_fields + + @property + def steps(self): + """deserialize wizard_steps as Wizard object""" + return Wizard.from_dict(self.wizard_steps or {}, self.env) + + @property + def step(self): + return self.steps[self.current_step] + + def _compute_current_fields(self): + for wizard in self: + wizard.current_question = wizard.step.question + wizard.current_complete_name = wizard.step.complete_name + wizard.current_company_id = int( + wizard.product_data.get("company_id", self.env.company.id) + ) + + def read(self, fields=None, load="_classic_read"): + if not fields: + fields = [] + wizard_fields, other_fields = self._split_fieldnames(fields) + answers_fields_only = bool(other_fields) and not bool(wizard_fields) + if answers_fields_only: + data = [{"id": record.id} for record in self] + else: + data = super().read(fields=wizard_fields, load=load) + for datum in data: + current_wizard = self.browse(datum["id"]) + current_step = current_wizard.step + for fieldname in other_fields: + if current_step.question_type == "field": + datum[fieldname] = current_wizard.product_data.get(fieldname, False) + if current_step.question_type == "custom": + datum[fieldname] = ( + ( + current_step.answer_id, + self.env["product.creation.answer"] + .browse(current_step.answer_id) + .name, + ) + if current_step.answer_id + else False + ) + return data + + def write(self, vals): + wizard_fields, other_fields = self._split_fieldnames(vals.keys()) + res = super().write({k: v for k, v in vals.items() if k in wizard_fields}) + self.write_serialized_data({k: v for k, v in vals.items() if k in other_fields}) + return res + + def write_serialized_data(self, vals): + if not vals: + return + + for rec in self: + answer_id = vals.pop("answer_id", False) + if answer_id: + wizard_data = rec.steps + wizard_data[rec.current_step].answer_id = answer_id + rec.write({"wizard_steps": wizard_data.to_dict()}) + + if vals: + data = rec.product_data + data.update(vals) + rec.write({"product_data": data}) + + def _is_valid_chid_question(self, parent_wizard_step): + """If there is a parent that has not been visited, the current + child question is not a valid question.""" + if parent_wizard_step and self.step.parent_index not in self.steps.step_history: + return False + return True + + def _is_valid_condition(self, parent_wizard_step): + if self.step.conditional_question and parent_wizard_step: + answer = "" + expected = "" + if parent_wizard_step.question_type == "field": + answer = str( + self.product_data.get(parent_wizard_step.field_id.name) + ).lower() + expected = str(self.step.conditional_expected_result).lower() + + if parent_wizard_step.question_type == "custom": + answer = parent_wizard_step.answer_id + expected = self.step.conditional_expected_result_answer_id.id + + if self.step.conditional_operator == "==" and answer != expected: + return False + + if self.step.conditional_operator == "!=" and answer == expected: + return False + return True + + def _prepare_product_data(self): + if self.step.question_type == "field": + field_value = self.product_data.get( + self.step.field_id.name, self.step.default_field_value + ) + if isinstance(field_value, str): + field_value = field_value.replace( + "current_company_id", str(self.current_company_id.id) + ) + try: + # useful for m2m with value such as [(0,0,{...})] + field_value = json.loads(field_value) + except Exception as exc: + logger.warning("Failed to parse field value as JSON: %s", exc) + pass + return {self.step.field_id.name: field_value} + + if self.step.question_type == "custom" and self.step.answer_id is None: + return {"answer_id": self.step.default_answer_id.id} + + if self.step.question_type == "logical": + product_data = {} + logical_default_values = self.step.logical_default_values or "{}" + default_values = json.loads(logical_default_values) + + logical_values = self.step.logical_values or "{}" + logical_values = logical_values.replace( + "current_company_id", str(self.current_company_id.id) + ) + values = json.loads(logical_values) + + for field_name in {*values.keys(), *default_values.keys()}: + product_data[field_name] = values.get( + field_name, + self.product_data.get( + field_name, + default_values.get( + field_name, + ), + ), + ) + return product_data + return {} + + def get_next_action(self): + user_step = False + while not user_step: + self.current_step += 1 + if self.current_step >= len(self.steps): + break + parent_wizard_step = None + if self.step.parent_index >= 0: + parent_wizard_step = self.steps[self.step.parent_index] + + if not self._is_valid_chid_question(parent_wizard_step): + continue + + if not self._is_valid_condition(parent_wizard_step): + continue + + self.write_serialized_data(self._prepare_product_data()) + + if self.step.is_automatic: + self._save_current_step_history() + continue + user_step = True + + if user_step: + self.create_or_update_product() + return self._open_wizard_action() + self.create_or_update_product(force_save=True) + return self.action_open_final_product() + + def _save_current_step_history(self): + wizard_data = self.steps + wizard_data.step_history.append(self.current_step) + self.write({"wizard_steps": wizard_data.to_dict()}) + + def action_open_next(self): + self._save_current_step_history() + return self.get_next_action() + + def action_open_previous(self): + wizard_data = self.steps + while wizard_data.step_history: + self.current_step = wizard_data.step_history.pop() + if not self.step.is_automatic: + break + self.write({"wizard_steps": wizard_data.to_dict()}) + return self._open_wizard_action() + + def _open_wizard_action(self): + self.ensure_one() + an_named_product_title = _("a new product") + return { + "name": _("Creating %(product_name)s...") + % {"product_name": self.product_template_id.name or an_named_product_title}, + "type": "ir.actions.act_window", + "res_model": self._name, + "res_id": self.id, + "view_mode": "form", + "target": "new", + "context": { + "invalidate_cache": str(uuid4()), + "product_creation_wizard": True, + }, + } + + def _split_product_data(self): + template_values = {} + product_values = {} + template_fields = self.env["product.template"]._fields + for fieldname, value in self.product_data.items(): + if fieldname in template_fields: + template_values[fieldname] = value + else: + product_values[fieldname] = value + return template_values, product_values + + def create_or_update_product(self, force_save=False): + if ( + self.current_step < len(self.steps) + and not self.step._record.automatic_save + and not force_save + ): + return + if self.product_template_id: + self._action_write() + else: + self._action_create() + + def _action_write(self): + def compute_diff(new_values, old_values): + return {k: v for k, v in new_values.items() if old_values.get(k) != v} + + def update_dict(old_values, diff): + updated = old_values.copy() + updated.update(diff) + return updated + + template_values, product_values = self._split_product_data() + + template_diff = compute_diff( + template_values, self.already_written_template_data + ) + self.product_template_id.write(template_diff) + self.already_written_template_data = update_dict( + self.already_written_template_data, template_diff + ) + + product_diff = compute_diff(product_values, self.already_written_product_data) + self.product_template_id.product_variant_ids.write(product_diff) + self.already_written_product_data = update_dict( + self.already_written_product_data, product_diff + ) + + def _action_create(self): + template_values, product_values = self._split_product_data() + self.product_template_id = self.env["product.template"].create(template_values) + self.already_written_template_data = template_values.copy() + self.product_template_id.product_variant_ids.write(product_values) + self.already_written_product_data = product_values.copy() + + def action_open_final_product(self): + return { + "type": "ir.actions.act_window", + "res_model": "product.template", + "view_mode": "form", + "res_id": self.product_template_id.id, + } diff --git a/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.xml b/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.xml new file mode 100644 index 000000000..a786a2263 --- /dev/null +++ b/product_creation_dynamic_wizard/wizards/product_creation_dynamic_wizard.xml @@ -0,0 +1,64 @@ + + + + + product_creation_dynamic_wizard.view.form + product.creation.dynamic.wizard + +
+

+ + + + + +
+
+
+ +
+ +
+
+ + + + Create new product + + + form + code + + action = model.action_open_product_creation_dynamic_wizard() + + + + +
diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..3f938691e --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +odoo_test_helper==2.1.1 +parameterized==0.9.0