Repository: OCA/product-configurator (language: python)

Issue:
[16.0][MIG] product_configurator: Migration to 16.0



Discussion / hints:

Fix patch:
diff --git a/.copier-answers.yml b/.copier-answers.yml
index 3fe06f7ac8..170c425818 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,8 +1,8 @@
 # Do NOT update manually; changes here will be overwritten by Copier
-_commit: v1.14.2
+_commit: v1.19.2
 _src_path: gh:oca/oca-addons-repo-template
 ci: GitHub
-dependency_installation_mode: PIP
+convert_readme_fragments_to_markdown: false
 generate_requirements_txt: true
 github_check_license: true
 github_ci_extra_env: {}
@@ -11,6 +11,7 @@ github_enable_makepot: true
 github_enable_stale_action: true
 github_enforce_dev_status_compatibility: true
 include_wkhtmltopdf: false
+odoo_test_flavor: Both
 odoo_version: 16.0
 org_name: Odoo Community Association (OCA)
 org_slug: OCA
@@ -19,6 +20,6 @@ repo_description: 'TODO: add repo description.'
 repo_name: product-configurator
 repo_slug: product-configurator
 repo_website: https://github.com/OCA/product-configurator
-travis_apt_packages: []
-travis_apt_sources: []
+use_pyproject_toml: false
+use_ruff: false
 
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 9429bc688a..fed88d70d2 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -22,6 +22,7 @@ globals:
   odoo: readonly
   openerp: readonly
   owl: readonly
+  luxon: readonly
 
 # Styling is handled by Prettier, so we only need to enable AST rules;
 # see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 23e6e58d6b..38b0ba110e 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -13,8 +13,10 @@ jobs:
   pre-commit:
     runs-on: ubuntu-22.04
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: actions/setup-python@v2
+        with:
+          python-version: "3.11"
       - name: Get python version
         run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
       - uses: actions/cache@v1
@@ -25,6 +27,15 @@ jobs:
         run: pip install pre-commit
       - name: Run pre-commit
         run: pre-commit run --all-files --show-diff-on-failure --color=always
+        env:
+          # Consider valid a PR that changes README fragments but doesn't
+          # change the README.rst file itself. It's not really a problem
+          # because the bot will update it anyway after merge. This way, we
+          # lower the barrier for functional contributors that want to fix the
+          # readme fragments, while still letting developers get README
+          # auto-generated (which also helps functionals when using runboat).
+          # DOCS https://pre-commit.com/#temporarily-disabling-hooks
+          SKIP: oca-gen-addon-readme
       - name: Check that all files generated by pre-commit are in git
         run: |
           newfiles="$(git ls-files --others --exclude-from=.gitignore)"
diff --git a/.gitignore b/.gitignore
index 9c283fd41f..0090721f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ __pycache__/
 *.py[cod]
 /.venv
 /.pytest_cache
+/.ruff_cache
 
 # C extensions
 *.so
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 24cd32d232..fd842de0f4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,6 +12,10 @@ exclude: |
   /static/(src/)?lib/|
   # Repos using Sphinx to generate docs don't need prettying
   ^docs/_templates/.*\.html$|
+  # Don't bother non-technical authors with formatting issues in docs
+  readme/.*\.(rst|md)$|
+  # Ignore build and dist directories in addons
+  /build/|/dist/|
   # You don't usually want a bot to modify your legal texts
   (LICENSE.*|COPYING.*)
 default_language_version:
@@ -33,12 +37,25 @@ repos:
         language: fail
         files: '[a-zA-Z0-9_]*/i18n/en\.po$'
   - repo: https://github.com/oca/maintainer-tools
-    rev: 4cd2b852214dead80822e93e6749b16f2785b2fe
+    rev: f71041f22b8cd68cf7c77b73a14ca8d8cd190a60
     hooks:
       # update the NOT INSTALLABLE ADDONS section above
       - id: oca-update-pre-commit-excluded-addons
       - id: oca-fix-manifest-website
         args: ["https://github.com/OCA/product-configurator"]
+      - id: oca-gen-addon-readme
+        args:
+          - --addons-dir=.
+          - --branch=16.0
+          - --org-name=OCA
+          - --repo-name=product-configurator
+          - --if-source-changed
+          - --keep-source-digest
+  - repo: https://github.com/OCA/odoo-pre-commit-hooks
+    rev: v0.0.25
+    hooks:
+      - id: oca-checks-odoo-module
+      - id: oca-checks-po
   - repo: https://github.com/myint/autoflake
     rev: v1.6.1
     hooks:
@@ -125,7 +142,7 @@ repos:
         name: flake8
         additional_dependencies: ["flake8-bugbear==21.9.2"]
   - repo: https://github.com/OCA/pylint-odoo
-    rev: 7.0.2
+    rev: v8.0.19
     hooks:
       - id: pylint_odoo
         name: pylint with optional checks
diff --git a/.pylintrc b/.pylintrc
index cad9c2f1b2..554913276b 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -5,12 +5,12 @@ load-plugins=pylint_odoo
 score=n
 
 [ODOOLINT]
-readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
-manifest_required_authors=Odoo Community Association (OCA)
-manifest_required_keys=license
-manifest_deprecated_keys=description,active
-license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
-valid_odoo_versions=16.0
+readme-template-url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
+manifest-required-authors=Odoo Community Association (OCA)
+manifest-required-keys=license
+manifest-deprecated-keys=description,active
+license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
+valid-odoo-versions=16.0
 
 [MESSAGES CONTROL]
 disable=all
diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory
index fa4b81c41e..7a0cd4efef 100644
--- a/.pylintrc-mandatory
+++ b/.pylintrc-mandatory
@@ -4,12 +4,12 @@ load-plugins=pylint_odoo
 score=n
 
 [ODOOLINT]
-readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
-manifest_required_authors=Odoo Community Association (OCA)
-manifest_required_keys=license
-manifest_deprecated_keys=description,active
-license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
-valid_odoo_versions=16.0
+readme-template-url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
+manifest-required-authors=Odoo Community Association (OCA)
+manifest-required-keys=license
+manifest-deprecated-keys=description,active
+license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
+valid-odoo-versions=16.0
 
 [MESSAGES CONTROL]
 disable=all
diff --git a/product_configurator/README.md b/product_configurator/README.md
new file mode 100644
index 0000000000..affc7d9035
--- /dev/null
+++ b/product_configurator/README.md
@@ -0,0 +1,25 @@
+#Odoo Product Configurator
+
+This module is Dynamic configuration wizard for Odoo back-end and the foundation for
+external configuration interfaces such 'website_product_configurator'.
+
+By itself this module does not configure custom products but offers the basis for
+generating, validating, updating configurable products using configuration interfaces.
+
+# Features
+
+- Inhibition of automatically created variants.
+- Extension of attribute lines to offer required, custom and multiple selection.
+- Configuration / Compatibility rules between attributes.
+- Separation of attributes in different steps.
+- Images for intermediate and final configurations.
+- Managing active configuration sessions for external configurators
+- Set of helper methods required for any Odoo configuration module.
+
+# Usage
+
+This module is Dynamic configuration wizard for Odoo back-end and the foundation for
+external configuration interfaces such 'website_product_configurator'.
+
+By itself this module does not configure custom products but offers the basis for
+generating, validating, updating configurable products using configuration interfaces.
diff --git a/product_configurator/README.rst b/product_configurator/README.rst
new file mode 100644
index 0000000000..8f87ad37e9
--- /dev/null
+++ b/product_configurator/README.rst
@@ -0,0 +1,80 @@
+====================
+Product Configurator
+====================
+
+.. 
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! This file is generated by oca-gen-addon-readme !!
+   !! changes will be overwritten.                   !!
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+   !! source digest: sha256:6adc7da27d5c08fae323a693949845877a252c0604b9f14345ceb6685c59fb55
+   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+    :target: https://odoo-community.org/page/development-status
+    :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+    :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+    :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--configurator-lightgray.png?logo=github
+    :target: https://github.com/OCA/product-configurator/tree/16.0/product_configurator
+    :alt: OCA/product-configurator
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+    :target: https://translation.odoo-community.org/projects/product-configurator-16-0/product-configurator-16-0-product_configurator
+    :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+    :target: https://runboat.odoo-community.org/builds?repo=OCA/product-configurator&target_branch=16.0
+    :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module has all the mechanics to support product configuration. It serves as a base
+dependency for configuration interfaces.
+
+**Table of contents**
+
+.. contents::
+   :local:
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues <https://github.com/OCA/product-configurator/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 <https://github.com/OCA/product-configurator/issues/new?body=module:%20product_configurator%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Pledra
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+   :alt: Odoo Community Association
+   :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+.. |maintainer-PCatinean| image:: https://github.com/PCatinean.png?size=40px
+    :target: https://github.com/PCatinean
+    :alt: PCatinean
+
+Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
+
+|maintainer-PCatinean| 
+
+This module is part of the `OCA/product-configurator <https://github.com/OCA/product-configurator/tree/16.0/product_configurator>`_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/product_configurator/__init__.py b/product_configurator/__init__.py
new file mode 100644
index 0000000000..ad2c86ac0b
--- /dev/null
+++ b/product_configurator/__init__.py
@@ -0,0 +1,4 @@
+from . import models
+from . import wizard
+
+from .init_hook import post_init_hook
diff --git a/product_configurator/__manifest__.py b/product_configurator/__manifest__.py
new file mode 100644
index 0000000000..4015979783
--- /dev/null
+++ b/product_configurator/__manifest__.py
@@ -0,0 +1,53 @@
+{
+    "name": "Product Configurator",
+    "version": "16.0.1.0.0",
+    "category": "Generic Modules/Base",
+    "summary": "Base for product configuration interface modules",
+    "author": "Pledra, Odoo Community Association (OCA)",
+    "license": "AGPL-3",
+    "website": "https://github.com/OCA/product-configurator",
+    "external_dependencies": {
+        "python": [
+            "mako",
+        ]
+    },
+    "depends": ["account"],
+    "data": [
+        "security/configurator_security.xml",
+        "security/ir.model.access.csv",
+        "views/res_config_settings_view.xml",
+        "data/menu_configurable_product.xml",
+        "data/product_attribute.xml",
+        "data/ir_sequence_data.xml",
+        "data/ir_config_parameter_data.xml",
+        "views/product_view.xml",
+        "views/product_attribute_view.xml",
+        "views/product_config_view.xml",
+        "wizard/product_configurator_view.xml",
+    ],
+    "assets": {
+        "web.assets_backend": [
+            "/product_configurator/static/src/scss/form_widget.scss",
+            "/product_configurator/static/src/js/form_widgets.js",
+            "/product_configurator/static/src/js/boolean_button_widget.esm.js",
+            "/product_configurator/static/src/js/boolean_button_widget.xml",
+            "/product_configurator/static/src/js/relational_fields.js",
+        ]
+    },
+    "demo": [
+        "demo/product_template.xml",
+        "demo/product_attribute.xml",
+        "demo/product_config_domain.xml",
+        "demo/product_config_lines.xml",
+        "demo/product_config_step.xml",
+        "demo/config_image_ids.xml",
+    ],
+    "images": ["static/description/cover.png"],
+    "post_init_hook": "post_init_hook",
+    "qweb": ["static/xml/create_button.xml"],
+    "development_status": "Beta",
+    "maintainers": ["PCatinean"],
+    "installable": True,
+    "application": True,
+    "auto_install": False,
+}
diff --git a/product_configurator/data/ir_config_parameter_data.xml b/product_configurator/data/ir_config_parameter_data.xml
new file mode 100644
index 0000000000..f686211787
--- /dev/null
+++ b/product_configurator/data/ir_config_parameter_data.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo noupdate="1">
+    <record id="manager_product_configuration_settings" model="ir.config_parameter">
+        <field
+            name="key"
+        >product_configurator.manager_product_configuration_settings</field>
+        <field name="value">True</field>
+    </record>
+</odoo>
diff --git a/product_configurator/data/ir_sequence_data.xml b/product_configurator/data/ir_sequence_data.xml
new file mode 100644
index 0000000000..805d5c689e
--- /dev/null
+++ b/product_configurator/data/ir_sequence_data.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo noupdate="1">
+    <record id="seq_config_session" model="ir.sequence">
+        <field name="name">Configuration Session</field>
+        <field name="code">product.config.session</field>
+        <field name="prefix">CS</field>
+        <field name="padding">4</field>
+    </record>
+</odoo>
diff --git a/product_configurator/data/menu_configurable_product.xml b/product_configurator/data/menu_configurable_product.xml
new file mode 100644
index 0000000000..63e21736ca
--- /dev/null
+++ b/product_configurator/data/menu_configurable_product.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <menuitem
+        id="menu_product_configurable"
+        name="Configurator"
+        web_icon="product_configurator,static/description/icon.png"
+        sequence="20"
+        groups="product_configurator.group_product_configurator_manager"
+    />
+    <menuitem
+        id="menu_product_configurable_product_main"
+        name="Configurable Products"
+        parent="menu_product_configurable"
+        sequence="10"
+    />
+    <menuitem
+        id="menu_product_configurable_settings"
+        name="Configuration"
+        parent="menu_product_configurable"
+        sequence="20"
+    />
+
+    <record id="product_configurable_template_action" model="ir.actions.act_window">
+        <field name="name">Configurable Templates</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.template</field>
+        <field name="view_mode">kanban,tree,form</field>
+        <field name="view_id" ref="product.product_template_kanban_view" />
+        <field
+            name="context"
+        >{'default_config_ok': True, 'custom_create_variant': True, 'search_default_filter_config_ok': 1}</field>
+    </record>
+
+    <menuitem
+        action="product_configurable_template_action"
+        id="menu_product_configurable_template_action"
+        parent="menu_product_configurable_product_main"
+        sequence="20"
+    />
+
+    <record id="product_configurable_variant_action" model="ir.actions.act_window">
+        <field name="name">Configured Variants</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.product</field>
+        <field name="view_mode">kanban,form,tree</field>
+        <field name="search_view_id" ref="product.product_search_form_view" />
+        <field name="view_id" eval="False" /> <!-- Force empty -->
+        <field
+            name="context"
+        >{'default_config_ok': True, 'custom_create_variant': True, 'search_default_filter_config_ok': 1}</field>
+    </record>
+
+    <menuitem
+        id="menu_product_configurable_variants_action"
+        action="product_configurable_variant_action"
+        name="Configurable Variants"
+        parent="menu_product_configurable_product_main"
+        sequence="25"
+    />
+
+    <record
+        id="action_product_configurator_configuration"
+        model="ir.actions.act_window"
+    >
+        <field name="name">Settings</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">res.config.settings</field>
+        <field name="view_mode">form</field>
+        <field name="target">inline</field>
+        <field
+            name="view_id"
+            ref="product_configurator.configurator_settings_view_form"
+        />
+        <field name="context">{'module' : 'product_configurator'}</field>
+    </record>
+
+    <menuitem
+        id="menu_action_product_configurator_configuration"
+        action="action_product_configurator_configuration"
+        name="Settings"
+        active="False"
+        parent="menu_product_configurable_settings"
+        sequence="0"
+    />
+
+    <record id="product_config_steps_action" model="ir.actions.act_window">
+        <field name="name">Configuration Steps</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.config.step</field>
+        <field name="view_mode">tree,form</field>
+    </record>
+
+    <menuitem
+        id="menu_product_config_steps_action"
+        action="product_config_steps_action"
+        name="Configuration Steps"
+        parent="menu_product_configurable_settings"
+        sequence="30"
+    />
+
+    <record id="product_config_domain_action" model="ir.actions.act_window">
+        <field name="name">Configuration Restrictions</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.config.domain</field>
+        <field name="view_mode">tree,form</field>
+    </record>
+
+    <menuitem
+        id="menu_product_config_domain_action"
+        action="product_config_domain_action"
+        name="Configuration Restrictions"
+        parent="menu_product_configurable_settings"
+        sequence="40"
+    />
+
+    <record id="product_config_session" model="ir.actions.act_window">
+        <field name="name">Configuration Sessions</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.config.session</field>
+        <field name="view_mode">tree,form</field>
+    </record>
+
+    <menuitem
+        id="menu_product_config_session"
+        action="product_config_session"
+        name="Configuration Sessions"
+        parent="menu_product_configurable_settings"
+        sequence="50"
+    />
+
+</odoo>
diff --git a/product_configurator/data/product_attribute.xml b/product_configurator/data/product_attribute.xml
new file mode 100644
index 0000000000..5b7df2a417
--- /dev/null
+++ b/product_configurator/data/product_attribute.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo noupdate="1">
+
+    <record id="custom_attribute" model="product.attribute">
+        <field name="name">Custom</field>
+        <field name="active" eval="False" />
+    </record>
+
+    <record id="custom_attribute_value" model="product.attribute.value">
+        <field name="name">Custom</field>
+        <field name="attribute_id" ref="custom_attribute" />
+        <field name="active" eval="True" />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/config_image_ids.xml b/product_configurator/demo/config_image_ids.xml
new file mode 100644
index 0000000000..47d6d2f0b2
--- /dev/null
+++ b/product_configurator/demo/config_image_ids.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Image Lines -->
+
+    <record id="config_image_1" model="product.config.image">
+        <field name="name">Coupé Red</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_red')
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_2" model="product.config.image">
+        <field name="name">Coupé Silver</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-silver.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_silver')
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_3" model="product.config.image">
+        <field name="name">Coupé Black</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-black.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_black')
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_5" model="product.config.image">
+        <field name="name">Coupé Red Rims 384</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_red'),
+            ref('product_attribute_value_rims_384'),
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_6" model="product.config.image">
+        <field name="name">Coupé Red Rims 387</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_red'),
+            ref('product_attribute_value_rims_387'),
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_7" model="product.config.image">
+        <field name="name">Coupé Silver Rims 384</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_silver'),
+            ref('product_attribute_value_rims_384'),
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_8" model="product.config.image">
+        <field name="name">Coupé Silver Rims 387</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_silver'),
+            ref('product_attribute_value_rims_387'),
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_9" model="product.config.image">
+        <field name="name">Coupé Black Rims 384</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_black'),
+            ref('product_attribute_value_rims_384'),
+        ])]"
+        />
+    </record>
+
+    <record id="config_image_10" model="product.config.image">
+        <field name="name">Coupé Black Rims 387</field>
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg"
+        />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_attribute_value_black'),
+            ref('product_attribute_value_rims_387'),
+        ])]"
+        />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/product_attribute.xml b/product_configurator/demo/product_attribute.xml
new file mode 100644
index 0000000000..ded2454ebf
--- /dev/null
+++ b/product_configurator/demo/product_attribute.xml
@@ -0,0 +1,408 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- == FUEL == -->
+
+    <record id="product_attribute_fuel" model="product.attribute">
+        <field name="name">Fuel</field>
+    </record>
+
+    <record id="product_attribute_value_gasoline" model="product.attribute.value">
+        <field name="name">Gasoline</field>
+        <field name="attribute_id" ref="product_attribute_fuel" />
+    </record>
+
+    <record id="product_attribute_value_diesel" model="product.attribute.value">
+        <field name="name">Diesel</field>
+        <field name="attribute_id" ref="product_attribute_fuel" />
+    </record>
+
+    <!-- == ENGINE == -->
+
+    <record id="product_attribute_engine" model="product.attribute">
+        <field name="name">Engine</field>
+    </record>
+
+    <!-- Gasoline Engines -->
+
+    <record id="product_attribute_value_218i" model="product.attribute.value">
+        <field name="name">218i</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_218i_coupe" />
+    </record>
+
+    <record id="product_attribute_value_220i" model="product.attribute.value">
+        <field name="name">220i</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_220i_coupe" />
+    </record>
+
+    <record id="product_attribute_value_228i" model="product.attribute.value">
+        <field name="name">228i</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_228i_coupe" />
+    </record>
+
+    <record id="product_attribute_value_m235i" model="product.attribute.value">
+        <field name="name">M235i</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_m235i_coupe" />
+    </record>
+
+    <record id="product_attribute_value_m235i_xdrive" model="product.attribute.value">
+        <field name="name">M235i xDrive</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_m2351_xdrive_coupe" />
+    </record>
+
+    <!-- Diesel Engines -->
+
+    <record id="product_attribute_value_218d" model="product.attribute.value">
+        <field name="name">218d</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_218d_coupe" />
+    </record>
+
+    <record id="product_attribute_value_220d" model="product.attribute.value">
+        <field name="name">220d</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_228i_coupe" />
+    </record>
+
+    <record id="product_attribute_value_220d_xdrive" model="product.attribute.value">
+        <field name="name">220d xDrive</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_220d_xdrive_coupe" />
+    </record>
+
+    <record id="product_attribute_value_225d" model="product.attribute.value">
+        <field name="name">225d</field>
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="product_id" ref="product_engine_225d_coupe" />
+    </record>
+
+    <!-- == LINES == -->
+
+    <record id="product_attribute_model_line" model="product.attribute">
+        <field name="name">Lines</field>
+    </record>
+
+    <record id="product_attribute_value_sport_line" model="product.attribute.value">
+        <field name="name">Sport Line</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_sport_line" />
+    </record>
+
+    <record
+        id="product_attribute_value_model_sport_line"
+        model="product.attribute.value"
+    >
+        <field name="name">Model Sport Line</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_model_sport_line" />
+    </record>
+
+    <record id="product_attribute_value_luxury_line" model="product.attribute.value">
+        <field name="name">Luxury Line</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_luxury_line" />
+    </record>
+
+    <record
+        id="product_attribute_value_model_luxury_line"
+        model="product.attribute.value"
+    >
+        <field name="name">Model Luxury Line</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_model_luxury_line" />
+    </record>
+
+    <record id="product_attribute_value_model_m_sport" model="product.attribute.value">
+        <field name="name">Model M Sport</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_model_m_sport" />
+    </record>
+
+    <record
+        id="product_attribute_value_model_advantage"
+        model="product.attribute.value"
+    >
+        <field name="name">Model Advantage</field>
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field name="product_id" ref="product_bmw_model_advantage" />
+    </record>
+
+    <!-- == COLOR == -->
+
+    <record id="product_attribute_color" model="product.attribute">
+        <field name="name">Paint Color</field>
+    </record>
+
+    <record id="product_attribute_value_red" model="product.attribute.value">
+        <field name="name">Red</field>
+        <field name="attribute_id" ref="product_attribute_color" />
+    </record>
+
+    <record id="product_attribute_value_silver" model="product.attribute.value">
+        <field name="name">Silver</field>
+        <field name="attribute_id" ref="product_attribute_color" />
+        <field name="product_id" ref="product_paint_silver" />
+    </record>
+
+    <record id="product_attribute_value_black" model="product.attribute.value">
+        <field name="name">Black</field>
+        <field name="attribute_id" ref="product_attribute_color" />
+    </record>
+
+    <!-- == RIMS == -->
+
+    <record id="product_attribute_rims" model="product.attribute">
+        <field name="name">Rims</field>
+    </record>
+
+    <record id="product_attribute_value_rims_378" model="product.attribute.value">
+        <field name="name">V-spoke 16"</field>
+        <field name="attribute_id" ref="product_attribute_rims" />
+    </record>
+
+    <record id="product_attribute_value_rims_387" model="product.attribute.value">
+        <field name="name">V-spoke 18"</field>
+        <field name="attribute_id" ref="product_attribute_rims" />
+    </record>
+
+    <record id="product_attribute_value_rims_384" model="product.attribute.value">
+        <field name="name">Double-spoke 18"</field>
+        <field name="attribute_id" ref="product_attribute_rims" />
+    </record>
+
+    <!-- == TAPISTRY == -->
+
+    <record id="product_attribute_tapistry" model="product.attribute">
+        <field name="name">Tapistry</field>
+    </record>
+
+    <record id="product_attribute_value_tapistry_black" model="product.attribute.value">
+        <field name="name">Black</field>
+        <field name="attribute_id" ref="product_attribute_tapistry" />
+    </record>
+
+    <record
+        id="product_attribute_value_tapistry_oyster_black"
+        model="product.attribute.value"
+    >
+        <field name="name">Oyster/Black</field>
+        <field name="attribute_id" ref="product_attribute_tapistry" />
+    </record>
+
+    <record
+        id="product_attribute_value_tapistry_coral_red_black"
+        model="product.attribute.value"
+    >
+        <field name="name">Coral Red/Black</field>
+        <field name="attribute_id" ref="product_attribute_tapistry" />
+    </record>
+
+    <!-- == TRANSMISSION == -->
+
+    <record id="product_attribute_transmission" model="product.attribute">
+        <field name="name">Transmission</field>
+    </record>
+
+    <record id="product_attribute_value_steptronic" model="product.attribute.value">
+        <field name="name">Automatic (Steptronic)</field>
+        <field name="attribute_id" ref="product_attribute_transmission" />
+        <field name="product_id" ref="product_2_series_transmission_steptronic" />
+    </record>
+
+    <record
+        id="product_attribute_value_steptronic_sport"
+        model="product.attribute.value"
+    >
+        <field name="name">Automatic Sport (Steptronic)</field>
+        <field name="attribute_id" ref="product_attribute_transmission" />
+        <field name="product_id" ref="product_2_series_transmission_steptronic_sport" />
+    </record>
+
+    <!-- == Options == -->
+
+    <record id="product_attribute_options" model="product.attribute">
+        <field name="name">Options</field>
+    </record>
+
+    <record id="product_attribute_value_armrest" model="product.attribute.value">
+        <field name="name">Armrest</field>
+        <field name="attribute_id" ref="product_attribute_options" />
+        <field name="product_id" ref="product_2_series_armrest" />
+    </record>
+
+    <record id="product_attribute_value_smoker_package" model="product.attribute.value">
+        <field name="name">Smoker Package</field>
+        <field name="attribute_id" ref="product_attribute_options" />
+        <field name="product_id" ref="product_2_series_smoker_package" />
+    </record>
+
+    <record id="product_attribute_value_sunroof" model="product.attribute.value">
+        <field name="name">Sunroof</field>
+        <field name="attribute_id" ref="product_attribute_options" />
+        <field name="product_id" ref="product_2_series_sunroof" />
+    </record>
+
+    <record id="product_attribute_value_tow_hook" model="product.attribute.value">
+        <field name="name">Tow hook</field>
+        <field name="attribute_id" ref="product_attribute_options" />
+        <field name="product_id" ref="product_2_series_towhook" />
+    </record>
+
+
+    <!-- == Attribute Lines == -->
+
+    <record
+        id="product_attribute_line_2_series_fuel"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_fuel" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_gasoline'),
+            ref('product_configurator.product_attribute_value_diesel')]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_engine"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_218i'),
+            ref('product_configurator.product_attribute_value_220i'),
+            ref('product_configurator.product_attribute_value_228i'),
+            ref('product_configurator.product_attribute_value_m235i'),
+            ref('product_configurator.product_attribute_value_m235i_xdrive'),
+            ref('product_configurator.product_attribute_value_218d'),
+            ref('product_configurator.product_attribute_value_220d'),
+            ref('product_configurator.product_attribute_value_220d_xdrive'),
+            ref('product_configurator.product_attribute_value_225d'),
+            ]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_model_line"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_model_line" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_sport_line'),
+            ref('product_configurator.product_attribute_value_model_sport_line'),
+            ref('product_configurator.product_attribute_value_luxury_line'),
+            ref('product_configurator.product_attribute_value_model_luxury_line'),
+            ref('product_configurator.product_attribute_value_model_m_sport'),
+            ref('product_configurator.product_attribute_value_model_advantage'),
+            ]
+        )]"
+        />
+        <field name="required" eval="False" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_color"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_color" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_red'),
+            ref('product_configurator.product_attribute_value_black'),
+            ref('product_configurator.product_attribute_value_silver')]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_rims"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_rims" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_rims_378'),
+            ref('product_configurator.product_attribute_value_rims_387'),
+            ref('product_configurator.product_attribute_value_rims_384')]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_tapistry"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_tapistry" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_tapistry_black'),
+            ref('product_configurator.product_attribute_value_tapistry_oyster_black'),
+            ref('product_configurator.product_attribute_value_tapistry_coral_red_black')]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_transmission"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_transmission" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_steptronic'),
+            ref('product_configurator.product_attribute_value_steptronic_sport'),
+            ]
+        )]"
+        />
+        <field name="required" eval="True" />
+    </record>
+
+    <record
+        id="product_attribute_line_2_series_options"
+        model="product.template.attribute.line"
+    >
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_id" ref="product_attribute_options" />
+        <field
+            name="value_ids"
+            eval="[(6,0,[
+            ref('product_configurator.product_attribute_value_armrest'),
+            ref('product_configurator.product_attribute_value_smoker_package'),
+            ref('product_configurator.product_attribute_value_sunroof'),
+            ref('product_configurator.product_attribute_value_tow_hook'),
+            ]
+        )]"
+        />
+        <field name="required" eval="True" />
+        <field name="multi" eval="True" />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/product_config_domain.xml b/product_configurator/demo/product_config_domain.xml
new file mode 100644
index 0000000000..e57d75f2aa
--- /dev/null
+++ b/product_configurator/demo/product_config_domain.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Configuration Domain and domain Lines -->
+
+    <!-- Gasoline Engines -->
+
+    <record id="product_config_domain_gasoline" model="product.config.domain">
+        <field name="name">Gasoline</field>
+    </record>
+
+    <record id="product_config_domain_line_1" model="product.config.domain.line">
+        <field name="domain_id" ref="product_config_domain_gasoline" />
+        <field name="attribute_id" ref="product_attribute_fuel" />
+        <field name="condition">in</field>
+        <field name="operator">and</field>
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_value_gasoline')])]"
+        />
+    </record>
+
+    <!-- Diesel Engines -->
+
+    <record id="product_config_domain_diesel" model="product.config.domain">
+        <field name="name">Diesel</field>
+    </record>
+
+    <record id="product_config_domain_line_2" model="product.config.domain.line">
+        <field name="domain_id" ref="product_config_domain_diesel" />
+        <field name="attribute_id" ref="product_attribute_fuel" />
+        <field name="condition">in</field>
+        <field name="operator">and</field>
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_value_diesel')])]"
+        />
+    </record>
+
+
+    <!-- Model Lines -->
+
+    <record id="product_config_domain_218_engine" model="product.config.domain">
+        <field name="name">218i Engine</field>
+    </record>
+
+    <record id="product_config_domain_line_3" model="product.config.domain.line">
+        <field name="domain_id" ref="product_config_domain_218_engine" />
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="condition">in</field>
+        <field name="operator">and</field>
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_value_218i')])]"
+        />
+    </record>
+
+
+
+
+    <record id="product_config_domain_luxury_lines" model="product.config.domain">
+        <field name="name">Luxury Lines</field>
+    </record>
+
+    <record id="product_config_domain_line_4" model="product.config.domain.line">
+        <field name="domain_id" ref="product_config_domain_luxury_lines" />
+        <field name="attribute_id" ref="product_attribute_engine" />
+        <field name="condition">in</field>
+        <field name="operator">and</field>
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_value_220i'),
+            ref('product_attribute_value_228i'),
+            ref('product_attribute_value_218d'),
+            ref('product_attribute_value_220d'),
+            ref('product_attribute_value_220d_xdrive')])]"
+        />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/product_config_lines.xml b/product_configurator/demo/product_config_lines.xml
new file mode 100644
index 0000000000..616bb7225e
--- /dev/null
+++ b/product_configurator/demo/product_config_lines.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Configuration Restriction Lines -->
+
+    <record id="product_config_line_gasoline_engines" model="product.config.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_line_id" ref="product_attribute_line_2_series_engine" />
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_value_218i'),
+            ref('product_attribute_value_220i'),
+            ref('product_attribute_value_228i'),
+            ref('product_attribute_value_m235i'),
+            ref('product_attribute_value_m235i_xdrive')])]"
+        />
+        <field name="domain_id" ref="product_config_domain_gasoline" />
+    </record>
+
+    <record id="product_config_line_diesel_engines" model="product.config.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="attribute_line_id" ref="product_attribute_line_2_series_engine" />
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+          ref('product_attribute_value_218d'),
+          ref('product_attribute_value_220d'),
+          ref('product_attribute_value_220d_xdrive'),
+          ref('product_attribute_value_225d')])]"
+        />
+        <field name="domain_id" ref="product_config_domain_diesel" />
+    </record>
+
+    <record id="product_config_line_218_lines" model="product.config.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="attribute_line_id"
+            ref="product_attribute_line_2_series_model_line"
+        />
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+          ref('product_attribute_value_sport_line'),
+          ref('product_attribute_value_luxury_line')])]"
+        />
+        <field name="domain_id" ref="product_config_domain_218_engine" />
+    </record>
+
+    <record id="product_config_line_luxury_lines" model="product.config.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field
+            name="attribute_line_id"
+            ref="product_attribute_line_2_series_model_line"
+        />
+        <field
+            name="value_ids"
+            eval="[(6, 0, [
+          ref('product_attribute_value_model_sport_line'),
+          ref('product_attribute_value_model_luxury_line'),
+          ref('product_attribute_value_model_m_sport'),
+          ref('product_attribute_value_model_advantage')])]"
+        />
+        <field name="domain_id" ref="product_config_domain_luxury_lines" />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/product_config_step.xml b/product_configurator/demo/product_config_step.xml
new file mode 100644
index 0000000000..797b7ebdc8
--- /dev/null
+++ b/product_configurator/demo/product_config_step.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Configuration Steps -->
+
+    <record id="config_step_engine" model="product.config.step">
+        <field name="name">Engine</field>
+    </record>
+
+    <record id="config_step_body" model="product.config.step">
+        <field name="name">Body</field>
+    </record>
+
+    <record id="config_step_lines" model="product.config.step">
+        <field name="name">Lines</field>
+    </record>
+
+    <record id="config_step_interior" model="product.config.step">
+        <field name="name">Interior</field>
+    </record>
+
+    <record id="config_step_extras" model="product.config.step">
+        <field name="name">Extras</field>
+    </record>
+
+    <!-- Configuration Step Lines -->
+
+    <record id="2_series_config_step_body" model="product.config.step.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="config_step_id" ref="config_step_body" />
+        <field
+            name="attribute_line_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_line_2_series_color'),
+            ref('product_attribute_line_2_series_rims')])]"
+        />
+    </record>
+
+    <record id="2_series_config_step_lines" model="product.config.step.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="config_step_id" ref="config_step_lines" />
+        <field
+            name="attribute_line_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_line_2_series_model_line')])]"
+        />
+    </record>
+
+    <record id="2_series_config_step_interior" model="product.config.step.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="config_step_id" ref="config_step_interior" />
+        <field
+            name="attribute_line_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_line_2_series_tapistry')])]"
+        />
+    </record>
+
+   <record id="2_series_config_step_engine" model="product.config.step.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="config_step_id" ref="config_step_engine" />
+        <field
+            name="attribute_line_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_line_2_series_engine'),
+            ref('product_attribute_line_2_series_fuel')])]"
+        />
+    </record>
+
+   <record id="2_series_config_step_extras" model="product.config.step.line">
+        <field name="product_tmpl_id" ref="bmw_2_series" />
+        <field name="config_step_id" ref="config_step_extras" />
+        <field
+            name="attribute_line_ids"
+            eval="[(6, 0, [
+            ref('product_attribute_line_2_series_transmission'),
+            ref('product_attribute_line_2_series_options')])]"
+        />
+    </record>
+
+</odoo>
diff --git a/product_configurator/demo/product_template.xml b/product_configurator/demo/product_template.xml
new file mode 100644
index 0000000000..e972842155
--- /dev/null
+++ b/product_configurator/demo/product_template.xml
@@ -0,0 +1,270 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Product Category -->
+
+    <record id="product_category_bmw" model="product.category">
+        <field name="parent_id" ref="product.product_category_all" />
+        <field name="name">BMW</field>
+    </record>
+
+    <!-- Configurable Product Template -->
+
+    <record id="bmw_2_series" model="product.template">
+        <field name="name">2 Series</field>
+        <field name="config_ok" eval="True" />
+        <field name="type">consu</field>
+        <field name="categ_id" ref="product_category_bmw" />
+        <field name="list_price" eval="25000" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/2-series-coupe.jpg"
+        />
+    </record>
+
+    <!-- Related Product: added on attribute value -->
+
+    <record id="product_bmw_sport_line" model="product.product">
+        <field name="name">Sport Line</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="0" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-sport-line.jpg"
+        />
+    </record>
+
+    <record id="product_bmw_luxury_line" model="product.product">
+        <field name="name">Luxury Line</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="0" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-luxury-line.jpg"
+        />
+    </record>
+
+    <record id="product_bmw_model_sport_line" model="product.product">
+        <field name="name">Model Sport Line</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="2666" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-sport-line.jpg"
+        />
+    </record>
+
+    <record id="product_bmw_model_luxury_line" model="product.product">
+        <field name="name">Model Luxury Line</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="3844" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-luxury-line.jpg"
+        />
+    </record>
+
+    <record id="product_bmw_model_m_sport" model="product.product">
+        <field name="name">Model M Sport</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="4526" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-m-sport.jpg"
+        />
+    </record>
+
+    <record id="product_bmw_model_advantage" model="product.product">
+        <field name="name">Model Advantage</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="992" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-advantage.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_transmission_steptronic" model="product.product">
+        <field name="name">Automatic Transmission Steptronic</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="0" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-transmission-steptronic.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_transmission_steptronic_sport" model="product.product">
+        <field name="name">Sport Automatic Transmission Steptronic</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="156" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-transmission-steptronic-sport.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_sunroof" model="product.product">
+        <field name="name">Sunroof</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="842" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-sunroof.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_armrest" model="product.product">
+        <field name="name">Armrest</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="0" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-armrest.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_towhook" model="product.product">
+        <field name="name">Towhook</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="842" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-towhook.jpg"
+        />
+    </record>
+
+    <record id="product_2_series_smoker_package" model="product.product">
+        <field name="name">Smoker Package</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="32" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-smoker-package.jpg"
+        />
+    </record>
+
+    <record id="product_engine_218i_coupe" model="product.product">
+        <field name="name">218i Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="4078" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_220i_coupe" model="product.product">
+        <field name="name">220i Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="7240" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_228i_coupe" model="product.product">
+        <field name="name">228i Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="12634" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_m235i_coupe" model="product.product">
+        <field name="name">M235i Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="23236" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_m2351_xdrive_coupe" model="product.product">
+        <field name="name">M235i xDrive Coupe</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="23236" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_218d_coupe" model="product.product">
+        <field name="name">218d Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="7116" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_220d_coupe" model="product.product">
+        <field name="name">220d Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="9596" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_220d_xdrive_coupe" model="product.product">
+        <field name="name">220d xDrive Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="16181" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_engine_225d_coupe" model="product.product">
+        <field name="name">225d Coupé</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="16987" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-engine.jpg"
+        />
+    </record>
+
+    <record id="product_paint_silver" model="product.product">
+        <field name="name">Silver Paint</field>
+        <field name="type">consu</field>
+        <field name="lst_price" eval="726" />
+        <field
+            name="image_1920"
+            type="base64"
+            file="product_configurator/static/img/product-paint-silver.jpg"
+        />
+    </record>
+
+</odoo>
diff --git a/product_configurator/i18n/product_configurator.pot b/product_configurator/i18n/product_configurator.pot
new file mode 100644
index 0000000000..5ca1e1bfea
--- /dev/null
+++ b/product_configurator/i18n/product_configurator.pot
@@ -0,0 +1,1540 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# 	* product_configurator
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-06-11 06:00+0000\n"
+"PO-Revision-Date: 2023-06-11 06:00+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"\n"
+"Attribute/s: %s"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"\n"
+"Restriction: %s"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"\n"
+"Value/s: %s\n"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.bmw_2_series
+msgid "2 Series"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_218d
+msgid "218d"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_218d_coupe_product_template
+msgid "218d Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_218i
+msgid "218i"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_218i_coupe_product_template
+msgid "218i Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d
+msgid "220d"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_220d_coupe_product_template
+msgid "220d Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_220d_xdrive
+msgid "220d xDrive"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_220d_xdrive_coupe_product_template
+msgid "220d xDrive Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_220i
+msgid "220i"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_220i_coupe_product_template
+msgid "220i Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_225d
+msgid "225d"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_225d_coupe_product_template
+msgid "225d Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_228i
+msgid "228i"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_228i_coupe_product_template
+msgid "228i Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_kanban_view_inherited
+msgid "<i class=\"fa fa-wrench fa-lg\" title=\"Configure Product\"/>"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__active
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__active
+msgid "Active"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"All attribute values used in attribute value lines must be defined in the "
+"attribute lines of the template"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__val_custom
+msgid "Allow custom value for this attribute?"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__custom
+msgid "Allow custom values for this attribute?"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__multi
+#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__multi
+msgid "Allow selection of multiple values for this attribute?"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "An error occurred while parsing value for attribute %s"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form
+msgid ""
+"Are you sure? This will remove your current configuration for this template!"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_armrest
+#: model:product.template,name:product_configurator.product_2_series_armrest_product_template
+msgid "Armrest"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__binary
+msgid "Attachment"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attachment_ids
+msgid "Attachments"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__attribute_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__attribute_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__attribute_id
+msgid "Attribute"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_line_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_line_ids
+msgid "Attribute Dependencies"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attribute_line_id
+msgid "Attribute Line"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_line_val_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_line_val_ids
+msgid "Attribute Line Val"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__attr_line_val_ids
+msgid "Attribute Line Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__attribute_line_ids
+msgid "Attribute Lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_attribute_value
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_id
+msgid "Attribute Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__attribute_value_line_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__attribute_value_line_ids
+msgid "Attribute Value Lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.variants_action
+#: model:ir.ui.menu,name:product_configurator.menu_variants_action_configuration
+msgid "Attribute Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_value__weight_extra
+msgid "Attribute Weight Extra"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"Attribute custom type is binary, attachments are the only accepted values "
+"with this custom field type"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"Attribute custom type must be 'binary' for saving attachments to custom "
+"value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__image
+msgid "Attribute value image (Display on website for radio buttons)"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.action_attributes_view
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__attribute_line_ids
+#: model:ir.ui.menu,name:product_configurator.menu_attribute_action_configuration
+msgid "Attributes"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic
+msgid "Automatic (Steptronic)"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_steptronic_sport
+msgid "Automatic Sport (Steptronic)"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_product_template
+msgid "Automatic Transmission Steptronic"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form
+msgid "Back"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_black
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_black
+msgid "Black"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.step,name:product_configurator.config_step_body
+msgid "Body"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute_value__active
+msgid ""
+"By unchecking the active field you can disable a attribute value without "
+"deleting it"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__active
+msgid ""
+"By unchecking the active field you can disable a attribute without deleting "
+"it"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_ok
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_ok
+msgid "Can be Configured"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"Cannot conduct search on an empty config session without product_tmpl_id "
+"kwarg"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Cannot have a configuration step defined twice."
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/wizard/product_configurator.py:0
+#, python-format
+msgid ""
+"Changing the product template while having an active configuration will "
+"erase reset/clear all values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__char
+msgid "Char"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__color
+#: model:product.attribute,name:product_configurator.product_attribute_color
+msgid "Color"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__condition
+msgid "Condition"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configurable"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_product_main
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view
+msgid "Configurable Products"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"Configurable Products cannot have duplicates (identical attribute values)"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_tmpl_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_tmpl_id
+msgid "Configurable Template"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.product_configurable_template_action
+#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_template_action
+msgid "Configurable Templates"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_variants_action
+msgid "Configurable Variants"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__value_ids
+#: model:ir.ui.menu,name:product_configurator.menu_product_configurable_settings
+msgid "Configuration"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_image_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_image_ids
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configuration Images"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_step_line_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__config_step_line_ids
+msgid "Configuration Lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_name
+msgid "Configuration Name"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.product_config_domain_action
+#: model:ir.ui.menu,name:product_configurator.menu_product_config_domain_action
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configuration Restrictions"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_session_id
+msgid "Configuration Session"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__name
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__name
+msgid "Configuration Session Number"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.product_config_session
+#: model:ir.ui.menu,name:product_configurator.menu_product_config_session
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view
+msgid "Configuration Sessions"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__config_step_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_name
+#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.config_step_tree_view
+msgid "Configuration Step"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__config_step
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step
+msgid "Configuration Step ID"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.product_config_steps_action
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__config_step_ids
+#: model:ir.ui.menu,name:product_configurator.menu_product_config_steps_action
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configuration Steps"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Configuration cannot have the same value inserted twice"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.ui.menu,name:product_configurator.menu_product_configurable
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configurator"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Configure Product"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.product_configurable_variant_action
+msgid "Configured Variants"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_coral_red_black
+msgid "Coral Red/Black"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_3
+msgid "Coupé Black"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_9
+msgid "Coupé Black Rims 384"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_10
+msgid "Coupé Black Rims 387"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_1
+msgid "Coupé Red"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_5
+msgid "Coupé Red Rims 384"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_6
+msgid "Coupé Red Rims 387"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_2
+msgid "Coupé Silver"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_7
+msgid "Coupé Silver Rims 384"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.image,name:product_configurator.config_image_8
+msgid "Coupé Silver Rims 387"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__create_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__currency_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__currency_id
+msgid "Currency"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__custom
+#: model:product.attribute,name:product_configurator.custom_attribute
+#: model:product.attribute.value,name:product_configurator.custom_attribute_value
+msgid "Custom"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__val_custom
+msgid "Custom Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__custom_value_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__custom_value_ids
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_session_form_view
+msgid "Custom Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_config_session_custom_value__value
+msgid "Custom value held as string"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__date
+msgid "Date"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__datetime
+msgid "DateTime"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__default_val
+msgid "Default Val"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid ""
+"Default values for each attribute line must exist in the attribute values "
+"(%(attr_name)s: %(default_val)s)"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Default values provided generate an invalid configuration"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__description
+msgid "Description"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__required
+msgid ""
+"Determines the required value of this attribute though it can be change on "
+"the template level"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_diesel
+msgid "Diesel"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__display_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_domain_line
+msgid "Domain Line for Config Restrictions"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_domain
+msgid "Domain for Config Restrictions"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__done
+msgid "Done"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_384
+msgid "Double-spoke 18\""
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_config_session__state__draft
+msgid "Draft"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_engine
+#: model:product.config.step,name:product_configurator.config_step_engine
+msgid "Engine"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid "Error while calculating mako product name: %s"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.step,name:product_configurator.config_step_extras
+msgid "Extras"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__custom_type
+msgid "Field Type"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Finished configuration session must have a product_id linked"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__float
+msgid "Float"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"Following Attribute/Value from restriction are not present in template "
+"attributes/values. Please make sure you are adding right restriction"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_fuel
+msgid "Fuel"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_gasoline
+msgid "Gasoline"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_product__mako_tmpl_name
+#: model:ir.model.fields,help:product_configurator.field_product_template__mako_tmpl_name
+msgid "Generate Name based on Mako Template"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__id
+msgid "ID"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__image
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__image
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1920
+msgid "Image"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_1024
+msgid "Image 1024"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_128
+msgid "Image 128"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_256
+msgid "Image 256"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__image_512
+msgid "Image 512"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__implied_ids
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template
+msgid "Inherited"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__integer
+msgid "Integer"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.config.step,name:product_configurator.config_step_interior
+msgid "Interior"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Invalid Configuration"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__config_preset_ok
+msgid "Is Preset"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_template_attribute_line__required
+msgid "Is this attribute required?"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line____last_update
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_uid
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__write_date
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_model_line
+#: model:product.config.step,name:product_configurator.config_step_lines
+msgid "Lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_luxury_line
+#: model:product.template,name:product_configurator.product_bmw_luxury_line_product_template
+msgid "Luxury Line"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i
+msgid "M235i"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_m235i_coupe_product_template
+msgid "M235i Coupé"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_m235i_xdrive
+msgid "M235i xDrive"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_engine_m2351_xdrive_coupe_product_template
+msgid "M235i xDrive Coupe"
+msgstr ""
+
+#. module: product_configurator
+#: model:res.groups,name:product_configurator.group_product_configurator_manager
+msgid "Manager"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_dummy
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight_dummy
+msgid "Manual Weight"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_template__weight_dummy
+msgid "Manual setting of product template weight"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__max_val
+msgid "Max Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__max_val
+msgid "Maximum value allowed"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid "Maximum value must be greater than Minimum value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__min_val
+msgid "Min Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__min_val
+msgid "Minimum value allowed"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_advantage
+#: model:product.template,name:product_configurator.product_bmw_model_advantage_product_template
+msgid "Model Advantage"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_luxury_line
+#: model:product.template,name:product_configurator.product_bmw_model_luxury_line_product_template
+msgid "Model Luxury Line"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_m_sport
+#: model:product.template,name:product_configurator.product_bmw_model_m_sport_product_template
+msgid "Model M Sport"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_model_sport_line
+#: model:product.template,name:product_configurator.product_bmw_model_sport_line_product_template
+msgid "Model Sport Line"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__multi
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__multi
+msgid "Multi"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step__name
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__name
+msgid "Name"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "New"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form
+msgid "Next"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid ""
+"On the product %(product)s you cannot associate the value %(value)s with the"
+" attribute %(attr)s because they do not match."
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__operator
+msgid "Operators"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_options
+msgid "Options"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_tapistry_oyster_black
+msgid "Oyster/Black"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_preset_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_preset_id
+msgid "Preset"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__price
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__price
+msgid "Price"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_template
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value__product_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__product_tmpl_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__product_id
+msgid "Product"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_attribute
+msgid "Product Attribute"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_attribute_value_line
+msgid "Product Attribute Value Line"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view
+msgid "Product Attribute Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_image
+msgid "Product Config Image"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_line
+msgid "Product Config Restrictions"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_session
+msgid "Product Config Session"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_session_custom_value
+msgid "Product Config Session Custom Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_step_line
+msgid "Product Config Step Lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_config_step
+msgid "Product Config Steps"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.module.category,name:product_configurator.product_config_category
+#: model_terms:ir.ui.view,arch_db:product_configurator.configurator_settings_view_form
+msgid "Product Configurator"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_img
+msgid "Product Img"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_tmpl_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__product_tmpl_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__product_tmpl_id
+msgid "Product Template"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_template_attribute_line
+msgid "Product Template Attribute Line"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_template_attribute_value
+msgid "Product Template Attribute Value"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/wizard/product_configurator.py:0
+#, python-format
+msgid "Product Template does not have any attribute lines defined"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__product_value_ids
+msgid "Product Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_product
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__product_id
+msgid "Product Variant"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_product_configurator
+msgid "Product configuration Wizard"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Product created via configuration wizard"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_form_view_custom_vals_inherit
+msgid "Reconfigure Product"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_red
+msgid "Red"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__required
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__required
+msgid "Required"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Required attribute '%s' is empty"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form
+msgid "Reset"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__domain_line_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__domain_id
+msgid "Restrictions"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"Restrictions added make the current default values generate an invalid configuration.                      \n"
+"%s"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_rims
+msgid "Rims"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__domain_id
+msgid "Rule"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_config_domain_form_view_template
+msgid "Rules"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__search_ok
+msgid "Searchable"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid "Selected custom field type '%s' is not searchable"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid "Selected custom value '%(name)s' must be at least %(min_val)s"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid ""
+"Selected custom value '%(name)s' must be between %(min_val)s and %(max_val)s"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid "Selected custom value '%(name)s' must be lower than %(max_value)s"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__sequence
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__sequence
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_image__sequence
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__sequence
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_step_line__sequence
+#: model:ir.model.fields,field_description:product_configurator.field_product_template_attribute_line__sequence
+msgid "Sequence"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__cfg_session_id
+msgid "Session"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_configurator__product_id
+msgid "Set only when re-configuring a existing variant"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_config_domain_line__sequence
+msgid "Set the order of operations for evaluation domain lines"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.actions.act_window,name:product_configurator.action_product_configurator_configuration
+#: model:ir.ui.menu,name:product_configurator.menu_action_product_configurator_configuration
+msgid "Settings"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_silver
+msgid "Silver"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_paint_silver_product_template
+msgid "Silver Paint"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_smoker_package
+#: model:product.template,name:product_configurator.product_2_series_smoker_package_product_template
+msgid "Smoker Package"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid ""
+"Sorry, you are not allowed to create/change this kind of document. For more "
+"information please contact your manager."
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_2_series_transmission_steptronic_sport_product_template
+msgid "Sport Automatic Transmission Steptronic"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_sport_line
+#: model:product.template,name:product_configurator.product_bmw_sport_line_product_template
+msgid "Sport Line"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_search_view
+msgid "Standard Products"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__state
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__state
+msgid "State"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_sunroof
+#: model:product.template,name:product_configurator.product_2_series_sunroof_product_template
+msgid "Sunroof"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_tapistry
+msgid "Tapistry"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__template_attribute_value_ids
+msgid "Template Attribute Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__template_attribute_ids
+msgid "Template Attributes"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields.selection,name:product_configurator.selection__product_attribute__custom_type__text
+msgid "Textarea"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid ""
+"The attribute %(attr)s must have at least one value for the product "
+"%(product)s."
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute_value_line__attribute_id
+msgid ""
+"The attribute cannot be changed once the value is used on at least one "
+"product."
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"The following custom values are not permitted according to the product template - %s.\n"
+"\n"
+"It is possible that a change has been made to allowed custom values while your configuration was in process. Please reset your current session and start over or contact your administrator in order to proceed."
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"The following multi values are not permitted according to the product template - %s.\n"
+"\n"
+"It is possible that a change has been made to allowed multi values while your configuration was in process. Please reset your current session and start over or contact your administrator in order to proceed."
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "The following values are not available:"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__custom_type
+msgid "The type of the custom field generated in the frontend"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/wizard/product_configurator.py:0
+#, python-format
+msgid "There was a problem rendering the view (dynamic_form not found)"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_configurator_form
+msgid ""
+"To reset/change the Preset Please close and start the configuration again"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_tow_hook
+msgid "Tow hook"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.template,name:product_configurator.product_2_series_towhook_product_template
+msgid "Towhook"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain__trans_implied_ids
+msgid "Transitively inherits"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute,name:product_configurator.product_attribute_transmission
+msgid "Transmission"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute__uom_id
+msgid "Unit of Measure"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__user_id
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__user_id
+#: model:res.groups,name:product_configurator.group_product_configurator
+msgid "User"
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_378
+msgid "V-spoke 16\""
+msgstr ""
+
+#. module: product_configurator
+#: model:product.attribute.value,name:product_configurator.product_attribute_value_rims_387
+msgid "V-spoke 18\""
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__value_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session_custom_value__value
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__value_ids
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_attribute_value_form_view
+msgid "Value"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_domain_line__value_ids
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_line__value_ids
+msgid "Values"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_attribute_value_line__value_ids
+msgid "Values Configuration"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "Values entered for line '%s' generate a incompatible configuration"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid ""
+"Values must belong to the attribute of the corresponding attribute_line set "
+"on the configuration line"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_attribute.py:0
+#, python-format
+msgid ""
+"Values provided to the attribute value line are incompatible with the "
+"current rules"
+msgstr ""
+
+#. module: product_configurator
+#: model_terms:ir.ui.view,arch_db:product_configurator.product_template_only_form_view_inherited
+msgid "Variant Name"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__mako_tmpl_name
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__mako_tmpl_name
+msgid "Variant name"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model,name:product_configurator.model_ir_ui_view
+msgid "View"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_config_session__weight
+#: model:ir.model.fields,field_description:product_configurator.field_product_configurator__weight
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight
+#: model:ir.model.fields,field_description:product_configurator.field_product_template__weight
+msgid "Weight"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,field_description:product_configurator.field_product_product__weight_extra
+msgid "Weight Extra"
+msgstr ""
+
+#. module: product_configurator
+#: model:ir.model.fields,help:product_configurator.field_product_attribute__search_ok
+msgid ""
+"When checking for variants with the same configuration, do we include this "
+"field in the search?"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product.py:0
+#, python-format
+msgid "You cannot have a duplicate configuration for the same value"
+msgstr ""
+
+#. module: product_configurator
+#. odoo-python
+#: code:addons/product_configurator/models/product_config.py:0
+#: code:addons/product_configurator/models/product_config.py:0
+#, python-format
+msgid "You must select at least one attribute in order to configure a product"
+msgstr ""
diff --git a/product_configurator/init_hook.py b/product_configurator/init_hook.py
new file mode 100644
index 0000000000..5e18812a13
--- /dev/null
+++ b/product_configurator/init_hook.py
@@ -0,0 +1,10 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def post_init_hook(cr, registry):
+    """Transfer existing weight values to weight_dummy after installation
+    since now the weight field is computed
+    """
+    cr.execute("UPDATE product_product SET weight_dummy = weight")
diff --git a/product_configurator/models/__init__.py b/product_configurator/models/__init__.py
new file mode 100644
index 0000000000..e2129a2d88
--- /dev/null
+++ b/product_configurator/models/__init__.py
@@ -0,0 +1,4 @@
+from . import product_config
+from . import product_attribute
+from . import product
+from . import ir_ui_view
diff --git a/product_configurator/models/ir_ui_view.py b/product_configurator/models/ir_ui_view.py
new file mode 100644
index 0000000000..79412b8f09
--- /dev/null
+++ b/product_configurator/models/ir_ui_view.py
@@ -0,0 +1,12 @@
+from odoo import models
+
+
+class View(models.Model):
+    _inherit = "ir.ui.view"
+
+    def _validate_tag_button(self, node, name_manager, node_info):
+        special = node.get("special")
+        if special and special == "no_save":
+            return
+        else:
+            return super()._validate_tag_button(node, name_manager, node_info)
diff --git a/product_configurator/models/product.py b/product_configurator/models/product.py
new file mode 100644
index 0000000000..b8d0354e8a
--- /dev/null
+++ b/product_configurator/models/product.py
@@ -0,0 +1,578 @@
+import logging
+from io import StringIO
+
+from mako.runtime import Context
+from mako.template import Template
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class ProductTemplate(models.Model):
+    _inherit = "product.template"
+
+    @api.depends("product_variant_ids.product_tmpl_id")
+    def _compute_product_variant_count(self):
+        """For configurable products return the number of variants configured or
+        1 as many views and methods trigger only when a template has at least
+        one variant attached. Since we create them from the template we should
+        have access to them always"""
+        result = super(ProductTemplate, self)._compute_product_variant_count()
+        for product_tmpl in self:
+            config_ok = product_tmpl.config_ok
+            variant_count = product_tmpl.product_variant_count
+            if config_ok and not variant_count:
+                product_tmpl.product_variant_count = 1
+        return result
+
+    @api.depends("attribute_line_ids.value_ids")
+    def _compute_template_attr_vals(self):
+        """Compute all attribute values added in attribute line on
+        product template"""
+        for product_tmpl in self:
+            if product_tmpl.config_ok:
+                value_ids = product_tmpl.attribute_line_ids.mapped("value_ids")
+                product_tmpl.attribute_line_val_ids = value_ids
+            else:
+                product_tmpl.attribute_line_val_ids = False
+
+    @api.constrains("attribute_line_ids", "attribute_value_line_ids")
+    def check_attr_value_ids(self):
+        """Check attribute lines don't have some attribute value that
+        is not present in attribute lines of that product template"""
+        for product_tmpl in self:
+            if not product_tmpl.env.context.get("check_constraint", True):
+                continue
+            attr_val_lines = product_tmpl.attribute_value_line_ids
+            attr_val_ids = attr_val_lines.mapped("value_ids")
+            if not attr_val_ids <= product_tmpl.attribute_line_val_ids:
+                raise ValidationError(
+                    _(
+                        "All attribute values used in attribute value lines "
+                        "must be defined in the attribute lines of the "
+                        "template"
+                    )
+                )
+
+    @api.constrains("attribute_value_line_ids")
+    def _validate_unique_config(self):
+        """Check for duplicate configurations for the same
+        attribute value in image lines"""
+        for template in self:
+            attr_val_line_vals = template.attribute_value_line_ids.read(
+                ["value_id", "value_ids"], load=False
+            )
+            attr_val_line_vals = [
+                (line["value_id"], tuple(line["value_ids"]))
+                for line in attr_val_line_vals
+            ]
+            if len(set(attr_val_line_vals)) != len(attr_val_line_vals):
+                raise ValidationError(
+                    _("You cannot have a duplicate configuration for the same value")
+                )
+
+    config_ok = fields.Boolean(string="Can be Configured")
+
+    config_line_ids = fields.One2many(
+        comodel_name="product.config.line",
+        inverse_name="product_tmpl_id",
+        string="Attribute Dependencies",
+        copy=False,
+    )
+
+    config_image_ids = fields.One2many(
+        comodel_name="product.config.image",
+        inverse_name="product_tmpl_id",
+        string="Configuration Images",
+        copy=True,
+    )
+
+    attribute_value_line_ids = fields.One2many(
+        comodel_name="product.attribute.value.line",
+        inverse_name="product_tmpl_id",
+        string="Attribute Value Lines",
+        copy=True,
+    )
+
+    attribute_line_val_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        compute="_compute_template_attr_vals",
+        store=False,
+    )
+
+    config_step_line_ids = fields.One2many(
+        comodel_name="product.config.step.line",
+        inverse_name="product_tmpl_id",
+        string="Configuration Lines",
+        copy=False,
+    )
+
+    mako_tmpl_name = fields.Text(
+        string="Variant name",
+        help="Generate Name based on Mako Template",
+        copy=True,
+    )
+
+    # We are calculating weight of variants based on weight of
+    # product-template so that no need of compute and inverse on this
+    weight = fields.Float(
+        compute="_compute_weight",
+        inverse="_set_weight",  # pylint: disable=C8110
+        search="_search_weight",
+        store=False,
+    )
+    weight_dummy = fields.Float(
+        string="Manual Weight",
+        digits="Stock Weight",
+        help="Manual setting of product template weight",
+    )
+
+    def _compute_weight(self):
+        config_products = self.filtered(lambda template: template.config_ok)
+        for product in config_products:
+            product.weight = product.weight_dummy
+        standard_products = self - config_products
+        return super(ProductTemplate, standard_products)._compute_weight()
+
+    def _set_weight(self):
+        for product_tmpl in self:
+            product_tmpl.weight_dummy = product_tmpl.weight
+            if not product_tmpl.config_ok:
+                super(ProductTemplate, product_tmpl)._set_weight()
+        return
+
+    def _search_weight(self, operator, value):
+        return [("weight_dummy", operator, value)]
+
+    def _check_default_values(self):
+        default_val_ids = (
+            self.attribute_line_ids.filtered(lambda l: l.default_val)
+            .mapped("default_val")
+            .ids
+        )
+
+        cfg_session_obj = self.env["product.config.session"]
+        try:
+            cfg_session_obj.validate_configuration(
+                value_ids=default_val_ids, product_tmpl_id=self.id, final=False
+            )
+        except ValidationError as exc:
+            raise exc
+        except Exception as exc:
+            raise ValidationError(
+                _("Default values provided generate an invalid configuration")
+            ) from exc
+
+    @api.constrains("config_line_ids", "attribute_line_ids")
+    def _check_default_value_domains(self):
+        for template in self:
+            try:
+                template._check_default_values()
+            except ValidationError as exc:
+                raise ValidationError(
+                    _(
+                        "Restrictions added make the current default values "
+                        "generate an invalid configuration.\
+                      \n%s"
+                    )
+                    % (exc.args[0])
+                ) from exc
+
+    def toggle_config(self):
+        for record in self:
+            record.config_ok = not record.config_ok
+
+    def _create_variant_ids(self):
+        """Prevent configurable products from creating variants as these serve
+        only as a template for the product configurator"""
+        templates = self.filtered(lambda t: not t.config_ok)
+        if not templates:
+            return None
+        return super(ProductTemplate, templates)._create_variant_ids()
+
+    def unlink(self):
+        """- Prevent the removal of configurable product templates
+            from variants
+        - Patch for check access rights of user(configurable products)"""
+        configurable_templates = self.filtered(lambda template: template.config_ok)
+        if configurable_templates:
+            configurable_templates[:1].check_config_user_access()
+        for config_template in configurable_templates:
+            variant_unlink = config_template.env.context.get(
+                "unlink_from_variant", False
+            )
+            if variant_unlink:
+                self -= config_template
+        res = super(ProductTemplate, self).unlink()
+        return res
+
+    def copy(self, default=None):
+        """Copy restrictions, config Steps and attribute lines
+        ith product template"""
+        if not default:
+            default = {}
+        self = self.with_context(check_constraint=False)
+        res = super(ProductTemplate, self).copy(default=default)
+
+        # Attribute lines
+        attribute_line_dict = {}
+        for line in res.attribute_line_ids:
+            attribute_line_dict.update({line.attribute_id.id: line.id})
+
+        # Restrictions
+        for line in self.config_line_ids:
+            old_restriction = line.domain_id
+            new_restriction = old_restriction.copy()
+            config_line_default = {
+                "product_tmpl_id": res.id,
+                "domain_id": new_restriction.id,
+            }
+            new_attribute_line_id = attribute_line_dict.get(
+                line.attribute_line_id.attribute_id.id, False
+            )
+            if not new_attribute_line_id:
+                continue
+            config_line_default.update({"attribute_line_id": new_attribute_line_id})
+            line.copy(config_line_default)
+
+        # Config steps
+        config_step_line_default = {"product_tmpl_id": res.id}
+        for line in self.config_step_line_ids:
+            new_attribute_line_ids = [
+                attribute_line_dict.get(old_attr_line.attribute_id.id)
+                for old_attr_line in line.attribute_line_ids
+                if old_attr_line.attribute_id.id in attribute_line_dict
+            ]
+            if new_attribute_line_ids:
+                config_step_line_default.update(
+                    {"attribute_line_ids": [(6, 0, new_attribute_line_ids)]}
+                )
+            line.copy(config_step_line_default)
+        return res
+
+    def configure_product(self):
+        """launches a product configurator wizard with a linked
+        template in order to configure new product."""
+        return self.with_context(product_tmpl_id_readonly=True).create_config_wizard(
+            click_next=False
+        )
+
+    def create_config_wizard(
+        self,
+        model_name="product.configurator",
+        extra_vals=None,
+        click_next=True,
+    ):
+        """create product configuration wizard
+        - return action to launch wizard
+        - click on next step based on value of click_next"""
+        wizard_obj = self.env[model_name]
+        wizard_vals = {"product_tmpl_id": self.id}
+        if extra_vals:
+            wizard_vals.update(extra_vals)
+        wizard = wizard_obj.create(wizard_vals)
+        if click_next:
+            action = wizard.action_next_step()
+        else:
+            wizard_obj = wizard_obj.with_context(
+                wizard_model=model_name,
+                allow_preset_selection=True,
+            )
+            action = wizard_obj.get_wizard_action(wizard=wizard)
+        return action
+
+    @api.model
+    def _check_config_group_rights(self):
+        """Return True/False from system parameter
+        - Signals access rights needs to check or not
+        :Params: return : boolean"""
+        ICPSudo = self.env["ir.config_parameter"].sudo()
+        manager_product_configuration_settings = ICPSudo.get_param(
+            "product_configurator.manager_product_configuration_settings"
+        )
+        return manager_product_configuration_settings
+
+    @api.model
+    def check_config_user_access(self):
+        """Check user have access to perform action(create/write/delete)
+        on configurable products"""
+        if not self._check_config_group_rights():
+            return True
+        config_manager = self.env.user.has_group(
+            "product_configurator.group_product_configurator_manager"
+        )
+        user_root = self.env.ref("base.user_root")
+        user_admin = self.env.ref("base.user_admin")
+        if (
+            config_manager
+            or self.env.user.id in [user_root.id, user_admin.id]
+            or self.env.su
+        ):
+            return True
+        raise ValidationError(
+            _(
+                "Sorry, you are not allowed to create/change this kind of "
+                "document. For more information please contact your manager."
+            )
+        )
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        """Patch for check access rights of user(configurable products)"""
+        for vals in vals_list:
+            config_ok = vals.get("config_ok", False)
+            if config_ok:
+                self.check_config_user_access()
+        return super(ProductTemplate, self).create(vals_list)
+
+    def write(self, vals):
+        """Patch for check access rights of user(configurable products)"""
+        change_config_ok = "config_ok" in vals
+        configurable_templates = self.filtered(lambda template: template.config_ok)
+        if change_config_ok or configurable_templates:
+            self[:1].check_config_user_access()
+
+        return super(ProductTemplate, self).write(vals)
+
+    @api.constrains("config_line_ids")
+    def _check_config_line_domain(self):
+        attribute_line_ids = self.attribute_line_ids
+        tmpl_value_ids = attribute_line_ids.mapped("value_ids")
+        tmpl_attribute_ids = attribute_line_ids.mapped("attribute_id")
+        error_message = False
+        for domain_id in self.config_line_ids.mapped("domain_id"):
+            domain_attr_ids = domain_id.domain_line_ids.mapped("attribute_id")
+            domain_value_ids = domain_id.domain_line_ids.mapped("value_ids")
+            invalid_value_ids = domain_value_ids - tmpl_value_ids
+            invalid_attribute_ids = domain_attr_ids - tmpl_attribute_ids
+            if not invalid_attribute_ids and not invalid_value_ids:
+                continue
+            if not error_message:
+                error_message = _(
+                    "Following Attribute/Value from restriction "
+                    "are not present in template attributes/values. "
+                    "Please make sure you are adding right restriction"
+                )
+            error_message += _("\nRestriction: %s", domain_id.name)
+            error_message += (
+                invalid_attribute_ids
+                and _(
+                    "\nAttribute/s: %s", ", ".join(invalid_attribute_ids.mapped("name"))
+                )
+                or ""
+            )
+            error_message += (
+                invalid_value_ids
+                and _("\nValue/s: %s\n", ", ".join(invalid_value_ids.mapped("name")))
+                or ""
+            )
+        if error_message:
+            raise ValidationError(error_message)
+
+
+class ProductProduct(models.Model):
+    _inherit = "product.product"
+    _rec_name = "config_name"
+
+    @api.constrains("product_template_attribute_value_ids")
+    def _check_duplicate_product(self):
+        """Check for prducts with same attribute values/custom values"""
+        for product in self:
+            if not product.config_ok:
+                continue
+
+            # At the moment, I don't have enough confidence with my
+            # understanding of binary attributes, so will leave these
+            # as not matching...
+            # In theory, they should just work, if they are set to "non search"
+            # in custom field def!
+            # TODO: Check the logic with binary attributes
+            config_session_obj = product.env["product.config.session"]
+            ptav_ids = product.product_template_attribute_value_ids.mapped(
+                "product_attribute_value_id"
+            )
+            duplicates = config_session_obj.search_variant(
+                product_tmpl_id=product.product_tmpl_id,
+                value_ids=ptav_ids.ids,
+            ).filtered(lambda p: p.id != product.id)
+
+            if duplicates:
+                raise ValidationError(
+                    _(
+                        "Configurable Products cannot have duplicates "
+                        "(identical attribute values)"
+                    )
+                )
+
+    def _get_config_name(self):
+        """Name for configured products
+        :param: return : String"""
+        self.ensure_one()
+        return self.name
+
+    def _get_mako_context(self, buf):
+        """Return context needed for computing product name based
+        on mako-tamplate define on it's product template"""
+        self.ensure_one()
+        ptav_ids = self.product_template_attribute_value_ids.mapped(
+            "product_attribute_value_id"
+        )
+        return Context(
+            buf,
+            product=self,
+            attribute_values=ptav_ids,
+            steps=self.product_tmpl_id.config_step_line_ids,
+            template=self.product_tmpl_id,
+        )
+
+    def _get_mako_tmpl_name(self):
+        """Compute and return product name based on mako-tamplate
+        define on it's product template"""
+        self.ensure_one()
+        if self.mako_tmpl_name:
+            try:
+                mytemplate = Template(self.mako_tmpl_name or "")
+                buf = StringIO()
+                ctx = self._get_mako_context(buf)
+                mytemplate.render_context(ctx)
+                return buf.getvalue()
+            except Exception:
+                _logger.error(
+                    _("Error while calculating mako product name: %s")
+                    % self.display_name
+                )
+        return self.display_name
+
+    @api.depends("product_template_attribute_value_ids.weight_extra")
+    def _compute_product_weight_extra(self):
+        for product in self:
+            product.weight_extra = sum(
+                product.mapped("product_template_attribute_value_ids.weight_extra")
+            )
+
+    def _compute_product_weight(self):
+        for product in self:
+            if product.config_ok:
+                tmpl_weight = product.product_tmpl_id.weight
+                product.weight = tmpl_weight + product.weight_extra
+            else:
+                product.weight = product.weight_dummy
+
+    def _search_product_weight(self, operator, value):
+        return [("weight_dummy", operator, value)]
+
+    def _inverse_product_weight(self):
+        """Store weight in dummy field"""
+        self.weight_dummy = self.weight
+
+    config_name = fields.Char(
+        string="Configuration Name", compute="_compute_config_name"
+    )
+    weight_extra = fields.Float(compute="_compute_product_weight_extra")
+    weight_dummy = fields.Float(string="Manual Weight", digits="Stock Weight")
+    weight = fields.Float(
+        compute="_compute_product_weight",
+        inverse="_inverse_product_weight",
+        search="_search_product_weight",
+        store=False,
+    )
+
+    # product preset
+    config_preset_ok = fields.Boolean(string="Is Preset")
+
+    def _compute_config_name(self):
+        """Compute the name of the configurable products and use template
+        name for others"""
+        for product in self:
+            if product.config_ok:
+                product.config_name = product._get_config_name()
+            else:
+                product.config_name = product.name
+
+    def reconfigure_product(self):
+        """launches a product configurator wizard with a linked
+        template and variant in order to re-configure an existing product.
+        It is essentially a shortcut to pre-fill configuration
+        data of a variant"""
+        self.ensure_one()
+
+        extra_vals = {"product_id": self.id}
+        return self.product_tmpl_id.create_config_wizard(extra_vals=extra_vals)
+
+    @api.model
+    def check_config_user_access(self, mode):
+        """Check user have access to perform action(create/write/delete)
+        on configurable products"""
+        if not self.env["product.template"]._check_config_group_rights():
+            return True
+        config_manager = self.env.user.has_group(
+            "product_configurator.group_product_configurator_manager"
+        )
+        config_user = self.env.user.has_group(
+            "product_configurator.group_product_configurator"
+        )
+        user_root = self.env.ref("base.user_root")
+        user_admin = self.env.ref("base.user_admin")
+        if (
+            config_manager
+            or (config_user and mode not in ["delete"])
+            or self.env.user.id in [user_root.id, user_admin.id]
+        ):
+            return True
+        raise ValidationError(
+            _(
+                "Sorry, you are not allowed to create/change this kind of "
+                "document. For more information please contact your manager."
+            )
+        )
+
+    def unlink(self):
+        """- Signal unlink from product variant through context so
+        removal can be stopped for configurable templates
+        - check access rights of user(configurable products)"""
+        config_product = any(p.config_ok for p in self)
+        if config_product:
+            self.env["product.product"].check_config_user_access(mode="delete")
+        ctx = dict(self.env.context, unlink_from_variant=True)
+        self.env.context = ctx
+        return super(ProductProduct, self).unlink()
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        """Patch for check access rights of user(configurable products)"""
+        for vals in vals_list:
+            config_ok = vals.get("config_ok", False)
+            if config_ok:
+                self.check_config_user_access(mode="create")
+        return super(ProductProduct, self).create(vals_list)
+
+    def write(self, vals):
+        """Patch for check access rights of user(configurable products)"""
+        change_config_ok = "config_ok" in vals
+        configurable_products = self.filtered(lambda product: product.config_ok)
+        if change_config_ok or configurable_products:
+            self[:1].check_config_user_access(mode="write")
+
+        return super(ProductProduct, self).write(vals)
+
+    def _compute_product_price_extra(self):
+        standard_products = self.filtered(lambda product: not product.config_ok)
+        config_products = self - standard_products
+        if standard_products:
+            result = super(
+                ProductProduct, standard_products
+            )._compute_product_price_extra()
+        else:
+            result = None
+        for product in config_products:
+            attribute_value_obj = self.env["product.attribute.value"]
+            value_ids = (
+                product.product_template_attribute_value_ids.product_attribute_value_id
+            )
+            extra_prices = attribute_value_obj.get_attribute_value_extra_prices(
+                product_tmpl_id=product.product_tmpl_id.id, pt_attr_value_ids=value_ids
+            )
+            product.price_extra = sum(extra_prices.values())
+        return result
diff --git a/product_configurator/models/product_attribute.py b/product_configurator/models/product_attribute.py
new file mode 100644
index 0000000000..de1f9336de
--- /dev/null
+++ b/product_configurator/models/product_attribute.py
@@ -0,0 +1,442 @@
+from ast import literal_eval
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class ProductAttribute(models.Model):
+    _inherit = "product.attribute"
+    _order = "sequence"
+
+    def copy(self, default=None):
+        """Add ' (Copy)' in name to prevent attribute
+        having same name while copying"""
+        if not default:
+            default = {}
+        new_attrs = self.env["product.attribute"]
+        for attr in self:
+            default.update({"name": attr.name + " (copy)"})
+            new_attrs += super(ProductAttribute, attr).copy(default)
+        return new_attrs
+
+    @api.model
+    def _get_nosearch_fields(self):
+        """Return a list of custom field types that do not support searching"""
+        return ["binary"]
+
+    @api.onchange("custom_type")
+    def onchange_custom_type(self):
+        if self.custom_type in self._get_nosearch_fields():
+            self.search_ok = False
+        if self.custom_type not in ("integer", "float"):
+            self.min_val = False
+            self.max_val = False
+
+    @api.onchange("val_custom")
+    def onchange_val_custom_field(self):
+        if not self.val_custom:
+            self.custom_type = False
+
+    CUSTOM_TYPES = [
+        ("char", "Char"),
+        ("integer", "Integer"),
+        ("float", "Float"),
+        ("text", "Textarea"),
+        ("color", "Color"),
+        ("binary", "Attachment"),
+        ("date", "Date"),
+        ("datetime", "DateTime"),
+    ]
+
+    active = fields.Boolean(
+        default=True,
+        help="By unchecking the active field you can "
+        "disable a attribute without deleting it",
+    )
+    min_val = fields.Integer(string="Min Value", help="Minimum value allowed")
+    max_val = fields.Integer(string="Max Value", help="Maximum value allowed")
+
+    # TODO: Exclude self from result-set of dependency
+    val_custom = fields.Boolean(
+        string="Custom Value", help="Allow custom value for this attribute?"
+    )
+    custom_type = fields.Selection(
+        selection=CUSTOM_TYPES,
+        string="Field Type",
+        help="The type of the custom field generated in the frontend",
+    )
+    description = fields.Text(translate=True)
+    search_ok = fields.Boolean(
+        string="Searchable",
+        help="When checking for variants with "
+        "the same configuration, do we "
+        "include this field in the search?",
+    )
+    required = fields.Boolean(
+        default=True,
+        help="Determines the required value of this "
+        "attribute though it can be change on "
+        "the template level",
+    )
+    multi = fields.Boolean(
+        help="Allow selection of multiple values for this attribute?",
+    )
+    uom_id = fields.Many2one(comodel_name="uom.uom", string="Unit of Measure")
+    image = fields.Binary()
+
+    # TODO prevent the same attribute from being defined twice on the
+    # attribute lines
+
+    @api.constrains("custom_type", "search_ok")
+    def check_searchable_field(self):
+        for attribute in self:
+            nosearch_fields = attribute._get_nosearch_fields()
+            if attribute.custom_type in nosearch_fields and attribute.search_ok:
+                raise ValidationError(
+                    _(
+                        "Selected custom field type '%s' is not searchable",
+                        attribute.custom_type,
+                    )
+                )
+
+    def validate_custom_val(self, val):
+        """Pass in a desired custom value and ensure it is valid.
+        Probably should check type, etc., but let's assume fine for the moment.
+        """
+        self.ensure_one()
+        if self.custom_type in ("integer", "float"):
+            minv = self.min_val
+            maxv = self.max_val
+            val = literal_eval(str(val))
+            if minv and maxv and (val < minv or val > maxv):
+                raise ValidationError(
+                    _(
+                        "Selected custom value '%(name)s' must be "
+                        "between %(min_val)s and %(max_val)s",
+                        **{
+                            "name": self.name,
+                            "min_val": self.min_val,
+                            "max_val": self.max_val,
+                        },
+                    )
+                )
+            elif minv and val < minv:
+                raise ValidationError(
+                    _(
+                        "Selected custom value '%(name)s' must be at least %(min_val)s",
+                        **{"name": self.name, "min_val": self.min_val},
+                    )
+                )
+            elif maxv and val > maxv:
+                raise ValidationError(
+                    _(
+                        "Selected custom value '%(name)s' must be lower than %(max_value)s",
+                        **{"name": self.name, "max_val": self.max_val + 1},
+                    )
+                )
+
+    @api.constrains("min_val", "max_val")
+    def _check_constraint_min_max_value(self):
+        """Prevent to add Maximun value less than minimum value"""
+        for attribute in self:
+            if attribute.custom_type not in ("integer", "float"):
+                continue
+            minv = attribute.min_val
+            maxv = attribute.max_val
+            if maxv and minv and maxv < minv:
+                raise ValidationError(
+                    _("Maximum value must be greater than Minimum value")
+                )
+
+
+class ProductAttributeLine(models.Model):
+    _inherit = "product.template.attribute.line"
+    _order = "product_tmpl_id, sequence, id"
+    # TODO: Order by dependencies first and then sequence so dependent fields
+    # do not come before master field
+
+    @api.onchange("attribute_id")
+    def onchange_attribute(self):
+        """Set default value of required/multi/cutom from attribute"""
+        self.value_ids = False
+        self.required = self.attribute_id.required
+        self.multi = self.attribute_id.multi
+        self.custom = self.attribute_id.val_custom
+        # TODO: Remove all dependencies pointed towards the attribute being
+        # changed
+
+    @api.onchange("value_ids")
+    def onchange_values(self):
+        if self.default_val and self.default_val not in self.value_ids:
+            self.default_val = None
+
+    custom = fields.Boolean(help="Allow custom values for this attribute?")
+    required = fields.Boolean(help="Is this attribute required?")
+    multi = fields.Boolean(
+        help="Allow selection of multiple values for this attribute?",
+    )
+    default_val = fields.Many2one(comodel_name="product.attribute.value")
+
+    sequence = fields.Integer(default=10)
+
+    @api.constrains("value_ids", "default_val")
+    def _check_default_values(self):
+        """default value should not be outside of the
+        values selected in attribute line"""
+        for line in self.filtered(lambda l: l.default_val):
+            if line.default_val not in line.value_ids:
+                raise ValidationError(
+                    _(
+                        "Default values for each attribute line must exist in "
+                        "the attribute values (%(attr_name)s: %(default_val)s)",
+                        **{
+                            "attr_name": line.attribute_id.name,
+                            "default_val": line.default_val.name,
+                        },
+                    )
+                )
+
+    @api.constrains("active", "value_ids", "attribute_id")
+    def _check_valid_values(self):
+        """Overwrite to save attribute line without
+        values when custom is true"""
+        for ptal in self:
+            # Customization
+            if ptal.active and not ptal.value_ids and not ptal.custom:
+                # Old code
+                # if ptal.active and not ptal.value_ids:
+                # Customization End
+                raise ValidationError(
+                    _(
+                        "The attribute %(attr)s must have at least one value for "
+                        "the product %(product)s.",
+                        **{
+                            "attr": ptal.attribute_id.display_name,
+                            "product": ptal.product_tmpl_id.display_name,
+                        },
+                    )
+                )
+            for pav in ptal.value_ids:
+                if pav.attribute_id != ptal.attribute_id:
+                    raise ValidationError(
+                        _(
+                            "On the product %(product)s you cannot associate the "
+                            "value %(value)s with the attribute %(attr)s because they "
+                            "do not match.",
+                            **{
+                                "product": ptal.product_tmpl_id.display_name,
+                                "value": pav.display_name,
+                                "attr": ptal.attribute_id.display_name,
+                            },
+                        )
+                    )
+        return True
+
+
+class ProductAttributeValue(models.Model):
+    _inherit = "product.attribute.value"
+
+    def copy(self, default=None):
+        """Add ' (Copy)' in name to prevent attribute
+        having same name while copying"""
+        if not default:
+            default = {}
+        default.update({"name": self.name + " (copy)"})
+        product = super(ProductAttributeValue, self).copy(default)
+        return product
+
+    active = fields.Boolean(
+        default=True,
+        help="By unchecking the active field you can "
+        "disable a attribute value without deleting it",
+    )
+    product_id = fields.Many2one(comodel_name="product.product")
+    image = fields.Binary(
+        attachment=True,
+        help="Attribute value image (Display on website for radio buttons)",
+    )
+
+    @api.model
+    def get_attribute_value_extra_prices(
+        self, product_tmpl_id, pt_attr_value_ids, pricelist=None
+    ):
+        extra_prices = {}
+        if not pricelist:
+            pricelist = self.env.user.partner_id.property_product_pricelist
+
+        related_product_av_ids = self.env["product.attribute.value"].search(
+            [("id", "in", pt_attr_value_ids.ids), ("product_id", "!=", False)]
+        )
+        extra_prices = {
+            av.id: av.product_id.with_context(
+                pricelist=pricelist.id
+            )._get_contextual_price()
+            for av in related_product_av_ids
+        }
+        remaining_av_ids = pt_attr_value_ids - related_product_av_ids
+        pe_lines = self.env["product.template.attribute.value"].search(
+            [
+                ("product_attribute_value_id", "in", remaining_av_ids.ids),
+                ("product_tmpl_id", "=", product_tmpl_id),
+            ]
+        )
+        for line in pe_lines:
+            attr_val_id = line.product_attribute_value_id
+            if attr_val_id.id not in extra_prices:
+                extra_prices[attr_val_id.id] = 0
+            extra_prices[attr_val_id.id] += line.price_extra
+        return extra_prices
+
+    def name_get(self):
+        res = super(ProductAttributeValue, self).name_get()
+        if not self._context.get("show_price_extra"):
+            return res
+        product_template_id = self.env.context.get("active_id", False)
+
+        price_precision = self.env["decimal.precision"].precision_get("Product Price")
+        extra_prices = self.get_attribute_value_extra_prices(
+            product_tmpl_id=product_template_id, pt_attr_value_ids=self
+        )
+
+        res_prices = []
+        for val in res:
+            price_extra = extra_prices.get(val[0])
+            if price_extra:
+                val = (
+                    val[0],
+                    "%s ( +%s )"
+                    % (
+                        val[1],
+                        ("{0:,.%sf}" % (price_precision)).format(price_extra),
+                    ),
+                )
+            res_prices.append(val)
+        return res_prices
+
+    @api.model
+    def name_search(self, name="", args=None, operator="ilike", limit=100):
+        """Use name_search as a domain restriction for the frontend to show
+        only values set on the product template taking all the configuration
+        restrictions into account.
+
+        TODO: This only works when activating the selection not when typing
+        """
+        product_tmpl_id = self.env.context.get("_cfg_product_tmpl_id")
+        if product_tmpl_id:
+            # TODO: Avoiding browse here could be a good performance enhancer
+            product_tmpl = self.env["product.template"].browse(product_tmpl_id)
+            tmpl_vals = product_tmpl.attribute_line_ids.mapped("value_ids")
+            attr_restrict_ids = []
+            preset_val_ids = []
+            new_args = []
+            for arg in args:
+                # Restrict values only to value_ids set on product_template
+                if arg[0] == "id" and arg[1] == "not in":
+                    preset_val_ids = arg[2]
+                    # TODO: Check if all values are available for configuration
+                else:
+                    new_args.append(arg)
+            val_ids = set(tmpl_vals.ids)
+            if preset_val_ids:
+                val_ids -= set(arg[2])
+            val_ids = self.env["product.config.session"].values_available(
+                val_ids, preset_val_ids, product_tmpl_id=product_tmpl_id
+            )
+            new_args.append(("id", "in", val_ids))
+            mono_tmpl_lines = product_tmpl.attribute_line_ids.filtered(
+                lambda l: not l.multi
+            )
+            for line in mono_tmpl_lines:
+                line_val_ids = set(line.mapped("value_ids").ids)
+                if line_val_ids & set(preset_val_ids):
+                    attr_restrict_ids.append(line.attribute_id.id)
+            if attr_restrict_ids:
+                new_args.append(("attribute_id", "not in", attr_restrict_ids))
+            args = new_args
+        res = super(ProductAttributeValue, self).name_search(
+            name=name, args=args, operator=operator, limit=limit
+        )
+        return res
+
+    # TODO: Prevent unlinking custom options by overriding unlink
+
+    # _sql_constraints = [
+    #    ('unique_custom', 'unique(id,allow_custom_value)',
+    #    'Only one custom value per dimension type is allowed')
+    # ]
+
+
+class ProductAttributePrice(models.Model):
+    _inherit = "product.template.attribute.value"
+    # Leverage product.template.attribute.value to compute the extra weight
+    # each attribute adds
+
+    weight_extra = fields.Float(string="Attribute Weight Extra", digits="Stock Weight")
+
+
+class ProductAttributeValueLine(models.Model):
+    _name = "product.attribute.value.line"
+    _description = "Product Attribute Value Line"
+    _order = "sequence"
+
+    sequence = fields.Integer(default=10)
+    product_tmpl_id = fields.Many2one(
+        comodel_name="product.template",
+        string="Product Template",
+        ondelete="cascade",
+        required=True,
+    )
+    value_id = fields.Many2one(
+        comodel_name="product.attribute.value",
+        required="True",
+        string="Attribute Value",
+    )
+    attribute_id = fields.Many2one(
+        comodel_name="product.attribute", related="value_id.attribute_id"
+    )
+    value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        relation="product_attribute_value_product_attribute_value_line_rel",
+        column1="product_attribute_value_line_id",
+        column2="product_attribute_value_id",
+        string="Values Configuration",
+    )
+    product_value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        relation="product_attr_values_attr_values_rel",
+        column1="product_val_id",
+        column2="attr_val_id",
+        compute="_compute_get_value_id",
+        store=True,
+    )
+
+    @api.depends(
+        "product_tmpl_id",
+        "product_tmpl_id.attribute_line_ids",
+        "product_tmpl_id.attribute_line_ids.value_ids",
+    )
+    def _compute_get_value_id(self):
+        for attr_val_line in self:
+            template = attr_val_line.product_tmpl_id
+            value_list = template.attribute_line_ids.mapped("value_ids")
+            attr_val_line.product_value_ids = [(6, 0, value_list.ids)]
+
+    @api.constrains("value_ids")
+    def _validate_configuration(self):
+        """Ensure that the passed configuration in value_ids is a valid"""
+        cfg_session_obj = self.env["product.config.session"]
+        for attr_val_line in self:
+            value_ids = attr_val_line.value_ids.ids
+            value_ids.append(attr_val_line.value_id.id)
+            valid = cfg_session_obj.validate_configuration(
+                value_ids=value_ids,
+                product_tmpl_id=attr_val_line.product_tmpl_id.id,
+                final=False,
+            )
+            if not valid:
+                raise ValidationError(
+                    _(
+                        "Values provided to the attribute value line are "
+                        "incompatible with the current rules"
+                    )
+                )
diff --git a/product_configurator/models/product_config.py b/product_configurator/models/product_config.py
new file mode 100644
index 0000000000..9ff3ec7f68
--- /dev/null
+++ b/product_configurator/models/product_config.py
@@ -0,0 +1,1669 @@
+import logging
+from ast import literal_eval
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools.misc import flatten, formatLang
+
+_logger = logging.getLogger(__name__)
+
+
+class ProductConfigDomain(models.Model):
+    _name = "product.config.domain"
+    _description = "Domain for Config Restrictions"
+
+    @api.depends("implied_ids")
+    def _get_trans_implied(self):
+        """Computes the transitive closure of relation implied_ids"""
+
+        def linearize(domains):
+            trans_domains = domains
+            for domain in domains:
+                implied_domains = domain.implied_ids - domain
+                if implied_domains:
+                    trans_domains |= linearize(implied_domains)
+            return trans_domains
+
+        for domain in self:
+            domain.trans_implied_ids = linearize(domain)
+
+    def compute_domain(self):
+        """Returns a list of domains defined on a
+        product.config.domain_line_ids and all implied_ids"""
+        # TODO: Enable the usage of OR operators between implied_ids
+        # TODO: Add implied_ids sequence field to enforce order of operations
+        # TODO: Prevent circular dependencies
+        computed_domain = []
+        for domain in self:
+            lines = domain.trans_implied_ids.mapped("domain_line_ids").sorted()
+            if not lines:
+                continue
+            for line in lines[:-1]:
+                if line.operator == "or":
+                    computed_domain.append("|")
+                computed_domain.append(
+                    (line.attribute_id.id, line.condition, line.value_ids.ids)
+                )
+            # ensure 2 operands follow the last operator
+            computed_domain.append(
+                (
+                    lines[-1].attribute_id.id,
+                    lines[-1].condition,
+                    lines[-1].value_ids.ids,
+                )
+            )
+        return computed_domain
+
+    name = fields.Char(required=True)
+    domain_line_ids = fields.One2many(
+        comodel_name="product.config.domain.line",
+        inverse_name="domain_id",
+        string="Restrictions",
+        required=True,
+        copy=True,
+    )
+    implied_ids = fields.Many2many(
+        comodel_name="product.config.domain",
+        relation="product_config_domain_implied_rel",
+        string="Inherited",
+        column1="domain_id",
+        column2="parent_id",
+    )
+    trans_implied_ids = fields.Many2many(
+        comodel_name="product.config.domain",
+        compute=_get_trans_implied,
+        column1="domain_id",
+        column2="parent_id",
+        string="Transitively inherits",
+    )
+
+
+class ProductConfigDomainLine(models.Model):
+    _name = "product.config.domain.line"
+    _order = "sequence"
+    _description = "Domain Line for Config Restrictions"
+
+    def _get_domain_conditions(self):
+        operators = [("in", "In"), ("not in", "Not In")]
+
+        return operators
+
+    def _get_domain_operators(self):
+        andor = [("and", "And"), ("or", "Or")]
+
+        return andor
+
+    @api.depends("attribute_id")
+    def _compute_template_attribute_value_ids(self):
+        for domain in self:
+            domain.template_attribute_value_ids = (
+                domain._get_allowed_attribute_value_ids()
+            )
+
+    def _compute_attribute_id_domain(self):
+        if "product_attribute_ids" in self.env.context:
+            return [("id", "in", self.env.context["product_attribute_ids"][0][2])]
+        return []
+
+    def _get_allowed_attribute_value_ids(self):
+        self.ensure_one()
+        product_template = self.env["product.template"]
+        if self.env.context.get("product_tmpl_id"):
+            product_template = product_template.browse(
+                self.env.context.get("product_tmpl_id")
+            )
+        template_lines = product_template.attribute_line_ids
+        attribute_values = self.attribute_id.value_ids
+        return (
+            product_template
+            and (template_lines.mapped("value_ids") & attribute_values)
+            or attribute_values
+        )
+
+    template_attribute_value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        string="Template Attribute Values",
+        compute="_compute_template_attribute_value_ids",
+    )
+    attribute_id = fields.Many2one(
+        comodel_name="product.attribute",
+        string="Attribute",
+        required=True,
+        domain=lambda self: self._compute_attribute_id_domain(),
+    )
+    domain_id = fields.Many2one(
+        comodel_name="product.config.domain", required=True, string="Rule"
+    )
+    condition = fields.Selection(selection=_get_domain_conditions, required=True)
+    value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        relation="product_config_domain_line_attr_rel",
+        column1="line_id",
+        column2="attribute_id",
+        string="Values",
+        required=True,
+    )
+    operator = fields.Selection(
+        selection=_get_domain_operators,
+        string="Operators",
+        default="and",
+        required=True,
+    )
+    sequence = fields.Integer(
+        default=1,
+        help="Set the order of operations for evaluation domain lines",
+    )
+
+
+class ProductConfigLine(models.Model):
+    _name = "product.config.line"
+    _description = "Product Config Restrictions"
+    _order = "product_tmpl_id, sequence, id"
+
+    # TODO: Prevent config lines having dependencies that are not set in other
+    # config lines
+    # TODO: Prevent circular depdencies: Length -> Color, Color -> Length
+
+    @api.onchange("attribute_line_id")
+    def onchange_attribute(self):
+        self.value_ids = False
+        self.domain_id = False
+
+    @api.depends(
+        "product_tmpl_id",
+        "attribute_line_id",
+        "product_tmpl_id.attribute_line_ids",
+        "product_tmpl_id.config_line_ids",
+    )
+    def _compute_template_attribute_ids(self):
+        for config_line in self:
+            product_template = config_line.product_tmpl_id
+            attribute_line_ids = product_template.attribute_line_ids
+            config_line.template_attribute_ids = attribute_line_ids.mapped(
+                "attribute_id"
+            )
+
+    template_attribute_ids = fields.Many2many(
+        comodel_name="product.attribute",
+        string="Template Attributes",
+        compute="_compute_template_attribute_ids",
+    )
+    product_tmpl_id = fields.Many2one(
+        comodel_name="product.template",
+        string="Product Template",
+        ondelete="cascade",
+        required=True,
+    )
+    attribute_line_id = fields.Many2one(
+        comodel_name="product.template.attribute.line",
+        string="Attribute Line",
+        ondelete="cascade",
+        required=True,
+    )
+    # TODO: Find a more elegant way to restrict the value_ids
+    attr_line_val_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        related="attribute_line_id.value_ids",
+        string="Attribute Line Values",
+    )
+    value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        relation="cfg_line_attr_val_id_rel",
+        column1="cfg_line_id",
+        column2="attr_val_id",
+        string="Values",
+    )
+    domain_id = fields.Many2one(
+        comodel_name="product.config.domain",
+        required=True,
+        string="Restrictions",
+    )
+    sequence = fields.Integer(default=10)
+
+    @api.constrains("value_ids")
+    def check_value_attributes(self):
+        """Values selected in config lines must belong to the
+        attribute exist on linked attribute line"""
+        for line in self:
+            value_attributes = line.value_ids.mapped("attribute_id")
+            if value_attributes != line.attribute_line_id.attribute_id:
+                raise ValidationError(
+                    _(
+                        "Values must belong to the attribute of the "
+                        "corresponding attribute_line set on the "
+                        "configuration line"
+                    )
+                )
+
+
+class ProductConfigImage(models.Model):
+    _name = "product.config.image"
+    _inherit = ["image.mixin"]
+    _description = "Product Config Image"
+    _order = "sequence"
+
+    name = fields.Char(required=True, translate=True)
+    product_tmpl_id = fields.Many2one(
+        comodel_name="product.template",
+        string="Product",
+        ondelete="cascade",
+        required=True,
+    )
+    sequence = fields.Integer(default=10)
+    value_ids = fields.Many2many(
+        comodel_name="product.attribute.value", string="Configuration"
+    )
+
+    @api.constrains("value_ids")
+    def _check_value_ids(self):
+        """Check combination of values is possible according to given
+        restrictions on linked product template"""
+        cfg_session_obj = self.env["product.config.session"]
+        for cfg_img in self:
+            try:
+                cfg_session_obj.validate_configuration(
+                    value_ids=cfg_img.value_ids.ids,
+                    product_tmpl_id=cfg_img.product_tmpl_id.id,
+                    final=False,
+                )
+            except ValidationError as exc:
+                raise ValidationError(
+                    _(
+                        "Values entered for line '%s' generate "
+                        "a incompatible configuration",
+                        cfg_img.name,
+                    )
+                ) from exc
+
+
+class ProductConfigStep(models.Model):
+    _name = "product.config.step"
+    _description = "Product Config Steps"
+
+    # TODO: Prevent values which have dependencies to be set in a
+    #       step with higher sequence than the dependency
+
+    name = fields.Char(required=True, translate=True)
+
+
+class ProductConfigStepLine(models.Model):
+    _name = "product.config.step.line"
+    _description = "Product Config Step Lines"
+    _order = "sequence, config_step_id, id"
+
+    name = fields.Char(related="config_step_id.name")
+    config_step_id = fields.Many2one(
+        comodel_name="product.config.step",
+        string="Configuration Step",
+        required=True,
+    )
+    attribute_line_ids = fields.Many2many(
+        comodel_name="product.template.attribute.line",
+        relation="config_step_line_attr_id_rel",
+        column1="cfg_line_id",
+        column2="attr_id",
+        string="Attribute Lines",
+    )
+    product_tmpl_id = fields.Many2one(
+        comodel_name="product.template",
+        string="Product Template",
+        ondelete="cascade",
+        required=True,
+    )
+    sequence = fields.Integer(default=10)
+
+    @api.constrains("config_step_id")
+    def _check_config_step(self):
+        """Prevent to add same step more than once on same product template"""
+        for config_step in self:
+            cfg_step_lines = config_step.product_tmpl_id.config_step_line_ids
+            cfg_steps = cfg_step_lines.filtered(
+                lambda line: line != config_step
+            ).mapped("config_step_id")
+            if config_step.config_step_id in cfg_steps:
+                raise ValidationError(
+                    _("Cannot have a configuration step defined twice.")
+                )
+
+
+class ProductConfigSession(models.Model):
+    _name = "product.config.session"
+    _description = "Product Config Session"
+
+    @api.depends(
+        "value_ids",
+        "product_tmpl_id.list_price",
+        "product_tmpl_id.attribute_line_ids",
+        "product_tmpl_id.attribute_line_ids.value_ids",
+        "product_tmpl_id.attribute_line_ids.product_template_value_ids",
+        "product_tmpl_id.attribute_line_ids." "product_template_value_ids.price_extra",
+    )
+    def _compute_cfg_price(self):
+        for session in self:
+            if session.product_tmpl_id:
+                price = session.get_cfg_price()
+            else:
+                price = 0.00
+            session.price = price
+
+    def get_custom_value_id(self):
+        """Return record set of attribute value 'custom'"""
+        custom_ext_id = "product_configurator.custom_attribute_value"
+        custom_val_id = self.env.ref(custom_ext_id)
+        return custom_val_id
+
+    @api.model
+    def _get_custom_vals_dict(self):
+        """Retrieve session custom values as a dictionary of the form
+        {attribute_id: parsed_custom_value}"""
+        custom_vals = {}
+        for val in self.custom_value_ids:
+            if val.attribute_id.custom_type in ["float", "integer"]:
+                custom_vals[val.attribute_id.id] = literal_eval(val.value)
+            elif val.attribute_id.custom_type == "binary":
+                custom_vals[val.attribute_id.id] = val.attachment_ids
+            else:
+                custom_vals[val.attribute_id.id] = val.value
+        return custom_vals
+
+    def _compute_config_step_name(self):
+        """Get the config.step.line name using the string stored in config_step
+        field of the session"""
+        cfg_step_line_obj = self.env["product.config.step.line"]
+        cfg_session_step_lines = self.mapped("config_step")
+        cfg_step_line_ids = set()
+        for step in cfg_session_step_lines:
+            try:
+                cfg_step_line_ids.add(int(step))
+            except ValueError:
+                _logger.debug("Step from session not valid")
+        cfg_step_lines = cfg_step_line_obj.browse(cfg_step_line_ids)
+        for session in self:
+            try:
+                config_step = int(session.config_step)
+                config_step_line = cfg_step_lines.filtered(
+                    lambda x: x.id == config_step
+                )
+                session.config_step_name = config_step_line.name
+            except Exception:
+                _logger.debug("Invalid session data ignored")
+            if not session.config_step_name:
+                session.config_step_name = session.config_step
+
+    @api.model
+    def get_cfg_weight(self, value_ids=None, custom_vals=None):
+        """Computes the weight of the configured product based on the
+        configuration passed in via value_ids and custom_values
+
+        :param value_ids: list of attribute value_ids
+        :param custom_vals: dictionary of custom attribute values
+        :returns: final configuration weight"""
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if custom_vals is None:
+            custom_vals = {}
+
+        product_tmpl = self.product_tmpl_id
+
+        self = self.with_context(active_id=product_tmpl.id)
+
+        value_ids = flatten(value_ids)
+
+        weight_extra = 0.0
+        product_attr_val_obj = self.env["product.template.attribute.value"]
+        product_tmpl_attr_values = product_attr_val_obj.search(
+            [
+                ("product_tmpl_id", "in", product_tmpl.ids),
+                ("product_attribute_value_id", "in", value_ids),
+            ]
+        )
+        for product_tmpl_attr_val in product_tmpl_attr_values:
+            weight_extra += product_tmpl_attr_val.weight_extra
+
+        return product_tmpl.weight + weight_extra
+
+    @api.depends(
+        "value_ids",
+        "product_tmpl_id",
+        "product_tmpl_id.attribute_line_ids",
+        "product_tmpl_id.attribute_line_ids.value_ids",
+        "product_tmpl_id.attribute_line_ids.product_template_value_ids",
+        "product_tmpl_id.attribute_line_ids.product_template_value_ids" ".weight_extra",
+    )
+    def _compute_cfg_weight(self):
+        for cfg_session in self:
+            cfg_session.weight = cfg_session.get_cfg_weight()
+
+    def _compute_currency_id(self):
+        main_company = self.env["res.company"]._get_main_company()
+        for session in self:
+            template = session.product_tmpl_id
+            session.currency_id = (
+                template.company_id.sudo().currency_id.id or main_company.currency_id.id
+            )
+
+    name = fields.Char(string="Configuration Session Number", readonly=True)
+    config_step = fields.Char(string="Configuration Step ID")
+    config_step_name = fields.Char(
+        compute="_compute_config_step_name", string="Configuration Step"
+    )
+    product_id = fields.Many2one(
+        comodel_name="product.product",
+        name="Configured Variant",
+        ondelete="cascade",
+    )
+    product_tmpl_id = fields.Many2one(
+        comodel_name="product.template",
+        domain=[("config_ok", "=", True)],
+        string="Configurable Template",
+        required=True,
+    )
+    value_ids = fields.Many2many(
+        comodel_name="product.attribute.value",
+        relation="product_config_session_attr_values_rel",
+        column1="cfg_session_id",
+        column2="attr_val_id",
+    )
+    user_id = fields.Many2one(comodel_name="res.users", required=True, string="User")
+    custom_value_ids = fields.One2many(
+        comodel_name="product.config.session.custom.value",
+        inverse_name="cfg_session_id",
+        string="Custom Values",
+    )
+    price = fields.Float(
+        compute="_compute_cfg_price",
+        store=True,
+        digits="Product Price",
+    )
+    currency_id = fields.Many2one(
+        comodel_name="res.currency",
+        string="Currency",
+        compute="_compute_currency_id",
+    )
+    state = fields.Selection(
+        required=True,
+        selection=[("draft", "Draft"), ("done", "Done")],
+        default="draft",
+    )
+    weight = fields.Float(compute="_compute_cfg_weight", digits="Stock Weight")
+    # Product preset
+    product_preset_id = fields.Many2one(
+        comodel_name="product.product",
+        string="Preset",
+        domain="[('product_tmpl_id', '=', product_tmpl_id),\
+            ('config_preset_ok', '=', True)]",
+    )
+
+    def action_confirm(self, product_id=None):
+        for session in self:
+            if product_id is None:
+                product_id = session.create_get_variant()
+            session.write({"state": "done", "product_id": product_id.id})
+        return True
+
+    @api.constrains("state")
+    def _check_product_id(self):
+        for session in self.filtered(lambda s: s.state == "done"):
+            if not session.product_id:
+                raise ValidationError(
+                    _("Finished configuration session must have a " "product_id linked")
+                )
+
+    def update_session_configuration_value(self, vals, product_tmpl_id=None):
+        """Update value of configuration in current session
+
+        :param: vals: Dictionary of fields(of configution wizard) and values
+        :param: product_tmpl_id: record set of preoduct template
+        :return: True/False
+        """
+        self.ensure_one()
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+
+        product_configurator_obj = self.env["product.configurator"]
+        field_prefix = product_configurator_obj._prefixes.get("field_prefix")
+        custom_field_prefix = product_configurator_obj._prefixes.get(
+            "custom_field_prefix"
+        )
+
+        custom_val = self.get_custom_value_id()
+
+        attr_val_dict = {}
+        custom_val_dict = {}
+        for attr_line in product_tmpl_id.attribute_line_ids:
+            attr_id = attr_line.attribute_id.id
+            field_name = field_prefix + str(attr_id)
+            custom_field_name = custom_field_prefix + str(attr_id)
+
+            if field_name not in vals and custom_field_name not in vals:
+                continue
+
+            # Add attribute values from the client except custom attribute
+            # If a custom value is being written, but field name is not in
+            #   the write dictionary, then it must be a custom value!
+            if vals.get(field_name, custom_val.id) != custom_val.id:
+                if attr_line.multi and isinstance(vals[field_name], list):
+                    if not vals[field_name]:
+                        field_val = None
+                    else:
+                        field_val = vals[field_name][0][2]
+                elif not attr_line.multi and isinstance(vals[field_name], int):
+                    field_val = vals[field_name]
+                else:
+                    raise UserError(
+                        _(
+                            "An error occurred while parsing value for attribute %s",
+                            attr_line.attribute_id.name,
+                        )
+                    )
+                attr_val_dict.update({attr_id: field_val})
+                # Ensure there is no custom value stored if we have switched
+                # from custom value to selected attribute value.
+                if attr_line.custom:
+                    custom_val_dict.update({attr_id: False})
+            elif attr_line.custom:
+                val = vals.get(custom_field_name, False)
+                if attr_line.attribute_id.custom_type == "binary":
+                    # TODO: Add widget that enables multiple file uploads
+                    val = [{"name": "custom", "datas": vals[custom_field_name]}]
+                custom_val_dict.update({attr_id: val})
+                # Ensure there is no standard value stored if we have switched
+                # from selected value to custom value.
+                attr_val_dict.update({attr_id: False})
+
+        self.update_config(attr_val_dict, custom_val_dict)
+
+    def update_config(self, attr_val_dict=None, custom_val_dict=None):
+        """Update the session object with the given value_ids and custom values.
+
+        Use this method instead of write in order to prevent incompatible
+        configurations as this removed duplicate values for the same attribute.
+
+        :param attr_val_dict: Dictionary of the form {
+            int (attribute_id): attribute_value_id OR [attribute_value_ids]
+        }
+
+        :custom_val_dict: Dictionary of the form {
+            int (attribute_id): {
+                'value': 'custom val',
+                OR
+                'attachment_ids': {
+                    [{
+                        'name': 'attachment name',
+                        'datas': base64_encoded_string
+                    }]
+                }
+            }
+        }
+
+        """
+        if attr_val_dict is None:
+            attr_val_dict = {}
+        if custom_val_dict is None:
+            custom_val_dict = {}
+        update_vals = {}
+
+        value_ids = self.value_ids.ids
+        for attr_id, vals in attr_val_dict.items():
+            attr_val_ids = self.value_ids.filtered(
+                lambda x: x.attribute_id.id == int(attr_id)
+            ).ids
+            # Remove all values for this attribute and add vals from dict
+            value_ids = list(set(value_ids) - set(attr_val_ids))
+            if not vals:
+                continue
+            if isinstance(vals, list):
+                value_ids += vals
+            elif isinstance(vals, int):
+                value_ids.append(vals)
+
+        if value_ids != self.value_ids.ids:
+            update_vals.update({"value_ids": [(6, 0, value_ids)]})
+
+        # Remove all custom values included in the custom_vals dict
+        self.custom_value_ids.filtered(
+            lambda x: x.attribute_id.id in custom_val_dict.keys()
+        ).unlink()
+
+        if custom_val_dict:
+            binary_field_ids = (
+                self.env["product.attribute"]
+                .search(
+                    [
+                        ("id", "in", list(custom_val_dict.keys())),
+                        ("custom_type", "=", "binary"),
+                    ]
+                )
+                .ids
+            )
+        else:
+            binary_field_ids = []
+
+        for attr_id, vals in custom_val_dict.items():
+            if not vals:
+                continue
+
+            if "custom_value_ids" not in update_vals:
+                update_vals["custom_value_ids"] = []
+
+            custom_vals = {"attribute_id": attr_id}
+
+            if attr_id in binary_field_ids:
+                attachments = [
+                    (
+                        0,
+                        0,
+                        {"name": val.get("name"), "datas": val.get("datas")},
+                    )
+                    for val in vals
+                ]
+                custom_vals.update({"attachment_ids": attachments})
+            else:
+                custom_vals.update({"value": vals})
+
+            update_vals["custom_value_ids"].append((0, 0, custom_vals))
+        self.write(update_vals)
+
+    def write(self, vals):
+        """Validate configuration when writing new values to session"""
+        # TODO: Issue warning when writing to value_ids or custom_val_ids
+        res = super(ProductConfigSession, self).write(vals)
+        if not self.product_tmpl_id:
+            return res
+        value_ids = self.value_ids.ids
+        avail_val_ids = self.values_available(value_ids)
+        if set(value_ids) - set(avail_val_ids):
+            self.value_ids = [(6, 0, avail_val_ids)]
+        try:
+            self.validate_configuration(final=False)
+        except ValidationError as exc:
+            raise exc
+        except Exception as exc:
+            raise ValidationError(_("Invalid Configuration")) from exc
+        return res
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        for vals in vals_list:
+            vals["name"] = self.env["ir.sequence"].next_by_code(
+                "product.config.session"
+            ) or _("New")
+            product_tmpl = (
+                self.env["product.template"]
+                .browse(vals.get("product_tmpl_id"))
+                .exists()
+            )
+            if product_tmpl:
+                default_val_ids = (
+                    product_tmpl.attribute_line_ids.filtered(lambda l: l.default_val)
+                    .mapped("default_val")
+                    .ids
+                )
+                value_ids = vals.get("value_ids")
+                if value_ids:
+                    default_val_ids += value_ids[0][2]
+                try:
+                    self.validate_configuration(
+                        value_ids=default_val_ids,
+                        final=False,
+                        product_tmpl_id=product_tmpl.id,
+                    )
+                    # TODO: Remove if cond when PR with
+                    # raise error on github is merged
+                except ValidationError as exc:
+                    raise exc
+                except Exception as exc:
+                    raise ValidationError(
+                        _(
+                            "Default values provided generate an invalid "
+                            "configuration"
+                        )
+                    ) from exc
+                vals.update({"value_ids": [(6, 0, default_val_ids)]})
+        return super(ProductConfigSession, self).create(vals_list)
+
+    def create_get_variant(self, value_ids=None, custom_vals=None):
+        """Creates a new product variant with the attributes passed
+        via value_ids and custom_values or retrieves an existing
+        one based on search result
+
+            :param value_ids: list of product.attribute.values ids
+            :param custom_vals: dict {product.attribute.id: custom_value}
+
+            :returns: new/existing product.product recordset
+
+        """
+        if self.product_tmpl_id.config_ok:
+            self.validate_configuration()
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+
+        try:
+            self.validate_configuration()
+        except ValidationError as exc:
+            raise exc
+        except Exception as exc:
+            raise ValidationError(_("Invalid Configuration")) from exc
+
+        duplicates = self.search_variant(
+            value_ids=value_ids, product_tmpl_id=self.product_tmpl_id
+        )
+        if duplicates:
+            return duplicates[:1]
+
+        vals = self.get_variant_vals(value_ids, custom_vals)
+        product_obj = (
+            self.env["product.product"].sudo().with_context(mail_create_nolog=True)
+        )
+        variant = product_obj.sudo().create(vals)
+
+        variant.message_post(
+            body=_("Product created via configuration wizard"),
+            author_id=self.env.user.partner_id.id,
+        )
+
+        return variant
+
+    def _get_option_values(self, pricelist, value_ids=None):
+        """Return only attribute values that have products attached with a
+        price set to them"""
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        value_obj = self.env["product.attribute.value"].with_context(
+            pricelist=pricelist.id
+        )
+        values = (
+            value_obj.sudo()
+            .browse(value_ids)
+            .filtered(lambda x: x.product_id._get_contextual_price())
+        )
+        return values
+
+    def get_components_prices(self, prices, pricelist, value_ids=None):
+        """Return prices of the components which make up the final
+        configured variant"""
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+        vals = self._get_option_values(pricelist, value_ids)
+        for val in vals:
+            prices["vals"].append(
+                (
+                    val.attribute_id.name,
+                    val.product_id.name,
+                    val.product_id._get_contextual_price(),
+                )
+            )
+            product = val.product_id.with_context(pricelist=pricelist.id)
+            product_prices = product.taxes_id.sudo().compute_all(
+                price_unit=product._get_contextual_price(),
+                currency=pricelist.currency_id,
+                quantity=1,
+                product=self,
+                partner=self.env.user.partner_id,
+            )
+
+            total_included = product_prices["total_included"]
+            taxes = total_included - product_prices["total_excluded"]
+            prices["taxes"] += taxes
+            prices["total"] += total_included
+        return prices
+
+    @api.model
+    def get_cfg_price(self, value_ids=None, custom_vals=None):
+        """Computes the price of the configured product based on the
+            configuration passed in via value_ids and custom_values
+
+        :param value_ids: list of attribute value_ids
+        :param custom_vals: dictionary of custom attribute values
+        :returns: final configuration price"""
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if custom_vals is None:
+            custom_vals = {}
+
+        product_tmpl = self.product_tmpl_id
+        self = self.with_context(active_id=product_tmpl.id)
+
+        value_ids = self.flatten_val_ids(value_ids)
+
+        price_extra = 0.0
+        attr_val_obj = self.env["product.attribute.value"]
+        av_ids = attr_val_obj.browse(value_ids)
+        extra_prices = attr_val_obj.get_attribute_value_extra_prices(
+            product_tmpl_id=product_tmpl.id, pt_attr_value_ids=av_ids
+        )
+        price_extra = sum(extra_prices.values())
+        return product_tmpl.list_price + price_extra
+
+    def _get_config_image(self, value_ids=None, custom_vals=None, size=None):
+        """
+        Retreive the image object that most closely resembles the configuration
+        code sent via value_ids list
+
+        The default image object is the template (self)
+        :param value_ids: a list representing the ids of attribute values
+                         (usually stored in the user's session)
+        :param custom_vals: dictionary of custom attribute values
+        :returns: path to the selected image
+        """
+        # TODO: Also consider custom values for image change
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+
+        img_obj = self.product_tmpl_id
+        max_matches = 0
+        value_ids = self.flatten_val_ids(value_ids)
+        for line in self.product_tmpl_id.config_image_ids:
+            matches = len(set(line.value_ids.ids) & set(value_ids))
+            if matches > max_matches:
+                img_obj = line
+                max_matches = matches
+        return img_obj
+
+    def get_config_image(self, value_ids=None, custom_vals=None, size=None):
+        """
+        Retreive the image object that most closely resembles the configuration
+        code sent via value_ids list
+        For more information check _get_config_image
+        """
+        config_image_id = self._get_config_image(
+            value_ids=value_ids, custom_vals=custom_vals
+        )
+        return config_image_id.image_1920
+
+    @api.model
+    def get_variant_vals(self, value_ids=None, custom_vals=None, **kwargs):
+        """Hook to alter the values of the product variant before creation
+
+        :param value_ids: list of product.attribute.values ids
+        :param custom_vals: dict {product.attribute.id: custom_value}
+
+        :returns: dictionary of values to pass to product.create() method
+        """
+        self.ensure_one()
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+
+        image = self.get_config_image(value_ids)
+        ptav_ids = self.env["product.template.attribute.value"].search(
+            [
+                ("product_tmpl_id", "=", self.product_tmpl_id.id),
+                ("product_attribute_value_id", "in", value_ids),
+            ]
+        )
+        vals = {
+            "product_tmpl_id": self.product_tmpl_id.id,
+            "product_template_attribute_value_ids": [(6, 0, ptav_ids.ids)],
+            "taxes_id": [(6, 0, self.product_tmpl_id.taxes_id.ids)],
+            "image_1920": image,
+        }
+        return vals
+
+    def get_session_search_domain(self, product_tmpl_id, state="draft", parent_id=None):
+        """Return domain to search session linked to given
+        product template and current login user"""
+        domain = [
+            ("product_tmpl_id", "=", product_tmpl_id),
+            ("user_id", "=", self.env.uid),
+            ("state", "=", state),
+        ]
+        if parent_id:
+            domain.append(("parent_id", "=", parent_id))
+        return domain
+
+    def get_session_vals(self, product_tmpl_id, parent_id=None, user_id=None):
+        """Return the values for creating session"""
+        if not user_id:
+            user_id = self.env.user.id
+        vals = {"product_tmpl_id": product_tmpl_id, "user_id": user_id}
+        if parent_id:
+            vals.update(parent_id=parent_id)
+        return vals
+
+    def get_next_step(
+        self,
+        state,
+        product_tmpl_id=False,
+        value_ids=False,
+        custom_value_ids=False,
+    ):
+        """Find and return next step if it exists. This usually
+        implies the next configuration step (if any) defined via the
+        config_step_line_ids on the product.template.
+        """
+
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+        if value_ids is False:
+            value_ids = self.value_ids
+        if custom_value_ids is False:
+            custom_value_ids = self.custom_value_ids
+        if not state:
+            state = self.config_step
+
+        cfg_step_lines = product_tmpl_id.config_step_line_ids
+        if not cfg_step_lines:
+            if (value_ids or custom_value_ids) and state != "select":
+                return False
+            elif not (value_ids or custom_value_ids) and state != "select":
+                raise UserError(
+                    _(
+                        "You must select at least one "
+                        "attribute in order to configure a product"
+                    )
+                )
+            else:
+                return "configure"
+
+        adjacent_steps = self.get_adjacent_steps()
+        next_step = adjacent_steps.get("next_step")
+        open_step_lines = list(
+            map(lambda x: "%s" % (x), self.get_open_step_lines().ids)
+        )
+
+        session_config_step = self.config_step
+        if (
+            session_config_step
+            and state != session_config_step
+            and session_config_step in open_step_lines
+        ):
+            next_step = self.config_step
+        else:
+            next_step = str(next_step.id) if next_step else None
+        if next_step:
+            pass
+        elif not (value_ids or custom_value_ids):
+            raise UserError(
+                _(
+                    "You must select at least one "
+                    "attribute in order to configure a product"
+                )
+            )
+        else:
+            return False
+        return next_step
+
+    # TODO: Should be renamed to get_active_step_line
+
+    @api.model
+    def get_active_step(self):
+        """Attempt to return product.config.step.line object that has the id
+        of the config session step stored as string"""
+        cfg_step_line_obj = self.env["product.config.step.line"]
+
+        try:
+            cfg_step_line_id = int(self.config_step)
+        except ValueError:
+            cfg_step_line_id = None
+
+        if cfg_step_line_id:
+            return cfg_step_line_obj.browse(cfg_step_line_id)
+        return cfg_step_line_obj
+
+    @api.model
+    def get_open_step_lines(self, value_ids=None):
+        """
+        Returns a recordset of configuration step lines open for access given
+        the configuration passed through value_ids
+
+        e.g: Field A and B from configuration step 2 depend on Field C
+        from configuration step 1. Since fields A and B require action from
+        the previous step, configuration step 2 is deemed closed and redirect
+        is made for configuration step 1.
+
+        :param value_ids: list of value.ids representing the
+                          current configuration
+        :returns: recordset of accesible configuration steps
+        """
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        open_step_lines = self.env["product.config.step.line"]
+
+        for cfg_line in self.product_tmpl_id.config_step_line_ids:
+            for attr_line in cfg_line.attribute_line_ids:
+                available_vals = self.values_available(
+                    attr_line.value_ids.ids, value_ids
+                )
+                # TODO: Refactor when adding restriction to custom values
+                if available_vals or attr_line.custom:
+                    open_step_lines |= cfg_line
+                    break
+
+        return open_step_lines.sorted()
+
+    @api.model
+    def get_all_step_lines(self, product_tmpl_id=None):
+        """
+        Returns a recordset of configuration step lines of product_tmpl_id
+
+        :param product_tmpl_id: record-set of product.template
+        :returns: recordset of all configuration steps
+        """
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+
+        open_step_lines = product_tmpl_id.config_step_line_ids
+        return open_step_lines.sorted()
+
+    @api.model
+    def get_adjacent_steps(self, value_ids=None, active_step_line_id=None):
+        """Returns the previous and next steps given the configuration passed
+        via value_ids and the active step line passed via cfg_step_line_id."""
+
+        # If there is no open step return empty dictionary
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if not active_step_line_id:
+            active_step_line_id = self.get_active_step().id
+
+        config_step_lines = self.product_tmpl_id.config_step_line_ids
+
+        if not config_step_lines:
+            return {}
+
+        active_cfg_step_line = config_step_lines.filtered(
+            lambda l: l.id == active_step_line_id
+        )
+
+        open_step_lines = self.get_open_step_lines(value_ids)
+
+        if not active_cfg_step_line:
+            return {"next_step": open_step_lines[0]}
+
+        nr_steps = len(open_step_lines)
+
+        adjacent_steps = {}
+
+        for i, cfg_step in enumerate(open_step_lines):
+            if cfg_step == active_cfg_step_line:
+                adjacent_steps.update(
+                    {
+                        "next_step": None
+                        if i + 1 == nr_steps
+                        else open_step_lines[i + 1],
+                        "previous_step": None if i == 0 else open_step_lines[i - 1],
+                    }
+                )
+        return adjacent_steps
+
+    def check_and_open_incomplete_step(self, value_ids=None, custom_value_ids=None):
+        """Check and open incomplete step if any
+        :param value_ids: recordset of product.attribute.value
+        """
+        if value_ids is None:
+            value_ids = self.value_ids
+        if custom_value_ids is None:
+            custom_value_ids = self.custom_value_ids
+        custom_attr_selected = custom_value_ids.mapped("attribute_id")
+        open_step_lines = self.get_open_step_lines()
+        step_to_open = False
+        for step in open_step_lines:
+            unset_attr_line = step.attribute_line_ids.filtered(
+                lambda attr_line: attr_line.required
+                and not any([value in value_ids for value in attr_line.value_ids])
+                and not (
+                    attr_line.custom and attr_line.attribute_id in custom_attr_selected
+                )
+            )
+            check_val_ids = unset_attr_line.mapped("value_ids")
+            avail_val_ids = self.values_available(
+                check_val_ids.ids,
+                value_ids.ids,
+                product_tmpl_id=self.product_tmpl_id,
+            )
+            if unset_attr_line and avail_val_ids:
+                step_to_open = step
+                break
+        if step_to_open:
+            return "%s" % (step_to_open.id)
+        return False
+
+    @api.model
+    def get_variant_search_domain(self, product_tmpl_id, value_ids=None):
+        """Method called by search_variant used to search duplicates in the
+        database"""
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        domain = [
+            ("product_tmpl_id", "=", product_tmpl_id.id),
+            ("config_ok", "=", True),
+        ]
+        pta_value_ids = self.env["product.template.attribute.value"].search(
+            [
+                ("product_tmpl_id", "=", product_tmpl_id.id),
+                ("product_attribute_value_id", "in", value_ids),
+            ]
+        )
+        for value_id in pta_value_ids:
+            domain.append(("product_template_attribute_value_ids", "=", value_id.id))
+        return domain
+
+    def validate_domains_against_sels(self, domains, value_ids=None, custom_vals=None):
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        # process domains as shown in this wikipedia pseudocode:
+        # https://en.wikipedia.org/wiki/Polish_notation#Order_of_operations
+        stack = []
+        for domain in reversed(domains):
+            if type(domain) == tuple:
+                # evaluate operand and push to stack
+                if domain[1] == "in":
+                    if not set(domain[2]) & set(value_ids):
+                        stack.append(False)
+                        continue
+                else:
+                    if set(domain[2]) & set(value_ids):
+                        stack.append(False)
+                        continue
+                stack.append(True)
+            else:
+                # evaluate operator and previous 2 operands
+                # compute_domain() only inserts 'or' operators
+                # compute_domain() enforces 2 operands per operator
+                operand1 = stack.pop()
+                operand2 = stack.pop()
+                stack.append(operand1 or operand2)
+
+        # 'and' operator is implied for remaining stack elements
+        avail = True
+        while stack:
+            avail &= stack.pop()
+        return avail
+
+    @api.model
+    def values_available(
+        self,
+        check_val_ids=None,
+        value_ids=None,
+        custom_vals=None,
+        product_tmpl_id=None,
+    ):
+        """Determines whether the attr_values from the product_template
+        are available for selection given the configuration ids and the
+        dependencies set on the product template
+
+        :param check_val_ids: list of attribute value ids to check for
+                              availability
+        :param value_ids: list of attribute value ids
+        :param custom_vals: custom values dict {attr_id: custom_val}
+
+        :returns: list of available attribute values
+        """
+        if check_val_ids is None:
+            check_val_ids = self.value_ids.ids
+        elif check_val_ids:
+            check_val_ids = check_val_ids.copy()
+        if not self.product_tmpl_id:
+            product_tmpl = self.env["product.template"].browse(product_tmpl_id)
+        else:
+            product_tmpl = self.product_tmpl_id
+
+        product_tmpl.ensure_one()
+
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+        elif value_ids:
+            value_ids = value_ids.copy()
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+
+        avail_val_ids = []
+        for attr_val_id in check_val_ids:
+            config_lines = product_tmpl.config_line_ids.filtered(
+                lambda l: attr_val_id in l.value_ids.ids
+            )
+            domains = config_lines.mapped("domain_id").compute_domain()
+            avail = self.validate_domains_against_sels(domains, value_ids, custom_vals)
+            if avail:
+                avail_val_ids.append(attr_val_id)
+            elif attr_val_id in value_ids:
+                value_ids.remove(attr_val_id)
+
+        return avail_val_ids
+
+    @api.model
+    def get_extra_attribute_line_ids(self, product_template_id):
+        """Retrieve attribute lines defined on the product_template_id
+        which are not assigned to configuration steps"""
+
+        extra_attribute_line_ids = (
+            product_template_id.attribute_line_ids
+            - product_template_id.config_step_line_ids.mapped("attribute_line_ids")
+        )
+        return extra_attribute_line_ids
+
+    def check_attributes_configuration(
+        self, attribute_line_ids, custom_vals, value_ids, final=True
+    ):
+        for line in attribute_line_ids:
+            # Validate custom values
+            attr = line.attribute_id
+            if attr.id in custom_vals:
+                attr.validate_custom_val(custom_vals[attr.id])
+            if final:
+                common_vals = set(value_ids) & set(line.value_ids.ids)
+                custom_val = custom_vals.get(attr.id)
+                avail_val_ids = self.values_available(
+                    line.value_ids.ids,
+                    value_ids,
+                    product_tmpl_id=self.product_tmpl_id,
+                )
+                if (
+                    line.required
+                    and avail_val_ids
+                    and not common_vals
+                    and not custom_val
+                ):
+                    # TODO: Verify custom value type to be correct
+                    raise ValidationError(
+                        _("Required attribute '%s' is empty", attr.name)
+                    )
+
+    @api.model
+    def validate_configuration(
+        self,
+        value_ids=None,
+        custom_vals=None,
+        product_tmpl_id=False,
+        final=True,
+    ):
+        """Verifies if the configuration values passed via value_ids and
+        custom_vals are valid
+
+        :param value_ids: list of attribute value ids
+        :param custom_vals: custom values dict {attr_id: custom_val}
+        :param final: boolean marker to check required attributes.
+                      pass false to check non-final configurations
+
+        :returns: Error dict with reason of validation failure
+                  or True
+        """
+        # TODO: Raise ConfigurationError with reason
+        # Check if required values are missing for final configuration
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        if product_tmpl_id:
+            product_tmpl = self.env["product.template"].browse(product_tmpl_id)
+        else:
+            product_tmpl = self.product_tmpl_id
+
+        product_tmpl.ensure_one()
+
+        if custom_vals is None:
+            custom_vals = self._get_custom_vals_dict()
+        open_step_lines = self.get_open_step_lines()
+        attribute_line_ids = open_step_lines.mapped("attribute_line_ids")
+        attribute_line_ids += self.get_extra_attribute_line_ids(
+            product_template_id=product_tmpl
+        )
+        self.check_attributes_configuration(
+            attribute_line_ids, custom_vals, value_ids, final=final
+        )
+
+        # Check if all all the values passed are not restricted
+        avail_val_ids = self.values_available(
+            value_ids, value_ids, product_tmpl_id=product_tmpl_id
+        )
+        if set(value_ids) - set(avail_val_ids):
+            restrict_val = list(set(value_ids) - set(avail_val_ids))
+            product_att_values = self.env["product.attribute.value"].browse(
+                restrict_val
+            )
+            group_by_attr = {}
+            for val in product_att_values:
+                if val.attribute_id in group_by_attr:
+                    group_by_attr[val.attribute_id] += val
+                else:
+                    group_by_attr[val.attribute_id] = val
+
+            message = "The following values are not available:%s"
+            message_vals = ""
+            for attr, val in group_by_attr.items():
+                message_vals += "\n%s: %s" % (
+                    attr.name,
+                    ", ".join(val.mapped("name")),
+                )
+            raise ValidationError(_(message, message_vals))
+
+        # Check if custom values are allowed
+        custom_attr_ids = (
+            product_tmpl.attribute_line_ids.filtered("custom")
+            .mapped("attribute_id")
+            .ids
+        )
+        if not set(custom_vals.keys()) <= set(custom_attr_ids):
+            custom_attrs_with_error = list(
+                set(custom_vals.keys()) - set(custom_attr_ids)
+            )
+            custom_attrs_with_error = self.env["product.attribute"].browse(
+                custom_attrs_with_error
+            )
+            error_message = (
+                "The following custom values are not permitted "
+                "according to the product template - %s.\n\nIt is possible "
+                "that a change has been made to allowed custom values "
+                "while your configuration was in process. Please reset your "
+                "current session and start over or contact your administrator"
+                " in order to proceed."
+            )
+            message_vals = ""
+            for attr_id in custom_attrs_with_error:
+                message_vals += "\n%s: %s" % (
+                    attr_id.name,
+                    custom_vals.get(attr_id.id),
+                )
+            raise ValidationError(_(error_message, message_vals))
+
+        # Check if there are multiple values passed for non-multi attributes
+        mono_attr_lines = product_tmpl.attribute_line_ids.filtered(
+            lambda l: not l.multi
+        )
+        attrs_with_error = {}
+        for line in mono_attr_lines:
+            if len(set(line.value_ids.ids) & set(value_ids)) > 1:
+                wrong_vals = self.env["product.attribute.value"].browse(
+                    set(line.value_ids.ids) & set(value_ids)
+                )
+                attrs_with_error[line.attribute_id] = wrong_vals
+        if attrs_with_error:
+            error_message = (
+                "The following multi values are not permitted "
+                "according to the product template - %s.\n\nIt is possible "
+                "that a change has been made to allowed multi values "
+                "while your configuration was in process. Please reset your "
+                "current session and start over or contact your administrator"
+                " in order to proceed."
+            )
+            message_vals = ""
+            for attr_id, vals in attrs_with_error.items():
+                message_vals += "\n%s: %s" % (
+                    attr_id.name,
+                    ", ".join(vals.mapped("name")),
+                )
+            raise ValidationError(_(error_message, message_vals))
+        return True
+
+    @api.model
+    def search_variant(self, value_ids=None, product_tmpl_id=None):
+        """Searches product.variants with given value_ids and custom values
+        given in the custom_vals dict
+
+        :param value_ids: list of product.attribute.values ids
+        :param custom_vals: dict {product.attribute.id: custom_value}
+
+        :returns: product.product recordset of products matching domain
+        """
+        if value_ids is None:
+            value_ids = self.value_ids.ids
+
+        custom_value_id = self.get_custom_value_id()
+        value_ids = list(set(value_ids) - set(custom_value_id.ids))
+
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+            if not product_tmpl_id:
+                raise ValidationError(
+                    _(
+                        "Cannot conduct search on an empty config session "
+                        "without product_tmpl_id kwarg"
+                    )
+                )
+
+        domain = self.get_variant_search_domain(
+            product_tmpl_id=product_tmpl_id, value_ids=value_ids
+        )
+        products = self.env["product.product"].search(domain)
+
+        # At this point, we might have found products with all of the passed
+        # in values, but it might have more attributes!  These are NOT
+        # matches
+        more_attrs = products.filtered(
+            lambda p: len(p.product_template_attribute_value_ids) != len(value_ids)
+        )
+        products -= more_attrs
+        return products
+
+    def search_session(self, product_tmpl_id, parent_id=None):
+        domain = self.get_session_search_domain(
+            product_tmpl_id=product_tmpl_id, parent_id=parent_id
+        )
+        session = self.search(domain, order="create_date desc", limit=1)
+        return session
+
+    @api.model
+    def create_get_session(
+        self, product_tmpl_id, parent_id=None, force_create=False, user_id=None
+    ):
+        if not force_create:
+            session = self.search_session(
+                product_tmpl_id=product_tmpl_id, parent_id=parent_id
+            )
+            if session:
+                return session
+        vals = self.get_session_vals(
+            product_tmpl_id=product_tmpl_id,
+            parent_id=parent_id,
+            user_id=user_id,
+        )
+        return self.create(vals)
+
+    # TODO: Disallow duplicates
+
+    def flatten_val_ids(self, value_ids):
+        """Return a list of value_ids from a list with a mix of ids
+        and list of ids (multiselection)
+
+        :param value_ids: list of value ids or mix of ids and list of ids
+                           (e.g: [1, 2, 3, [4, 5, 6]])
+        :returns: flattened list of ids ([1, 2, 3, 4, 5, 6])"""
+        flat_val_ids = set(flatten(value_ids))
+        return list(flat_val_ids)
+
+    def formatPrices(self, prices=None, dp="Product Price"):
+        if prices is None:
+            prices = {}
+        dp = None
+        prices["taxes"] = formatLang(self.env, prices["taxes"], monetary=True, dp=dp)
+        prices["total"] = formatLang(self.env, prices["total"], monetary=True, dp=dp)
+        prices["vals"] = [
+            (v[0], v[1], formatLang(self.env, v[2], monetary=True, dp=dp))
+            for v in prices["vals"]
+        ]
+        return prices
+
+    def encode_custom_values(self, custom_vals):
+        """Hook to alter the values of the custom values before creating
+        or writing
+        :param custom_vals: dict {product.attribute.id: custom_value}
+        :returns: list of custom values compatible with write and create
+        """
+        attr_obj = self.env["product.attribute"]
+        binary_attribute_ids = attr_obj.search([("custom_type", "=", "binary")]).ids
+        custom_lines = []
+
+        for key, val in custom_vals.items():
+            custom_vals = {"attribute_id": key}
+            # TODO: Is this extra check neccesairy as we already make
+            # the check in validate_configuration?
+            attr_obj.browse(key).validate_custom_val(val)
+            if key in binary_attribute_ids:
+                custom_vals.update({"attachment_ids": [(6, 0, val.ids)]})
+            else:
+                custom_vals.update({"value": val})
+            custom_lines.append((0, 0, custom_vals))
+        return custom_lines
+
+    @api.model
+    def get_child_specification(self, model, parent):
+        """return dictiory of onchange specification by
+        appending parent before each key"""
+        model_obj = self.env[model]
+        specs = model_obj._onchange_spec()
+        new_specs = {}
+        for key, val in specs.items():
+            new_specs["%s.%s" % (parent, key)] = val
+        return new_specs
+
+    @api.model
+    def get_onchange_specifications(self, model):
+        """return onchange specification
+        - same functionality by _onchange_spec
+        - needed this method because odoo don't add specification for fields
+        one2many or many2many there is view-reference(using : tree_view_ref)
+        intead of view in that field"""
+        model_obj = self.env[model]
+        specs = model_obj._onchange_spec()
+        for name, field in model_obj._fields.items():
+            if field.type not in ["one2many", "many2many"]:
+                continue
+            ch_specs = self.get_child_specification(
+                model=field.comodel_name, parent=name
+            )
+            specs.update(ch_specs)
+        return specs
+
+    @api.model
+    def get_vals_to_write(self, values, model):
+        """Return values in formate excepted by write/create methods
+        - same functionality by _convert_to_write
+        - needed this method because odoo don't call convert to write
+        for the many2many/one2many fields"""
+        model_obj = self.env[model]
+        values = model_obj._convert_to_write(values)
+        fields = model_obj._fields
+        for key, vals in values.items():
+            if not isinstance(vals, list):
+                continue
+            new_lst = []
+            for line in vals:
+                new_line = line
+                if line and isinstance(line[-1], dict):
+                    new_line = line[:-1] + (
+                        self.get_vals_to_write(
+                            values=line[-1], model=fields[key].comodel_name
+                        ),
+                    )
+                new_lst.append(new_line)
+            values[key] = new_lst
+        return values
+
+
+class ProductConfigSessionCustomValue(models.Model):
+    _name = "product.config.session.custom.value"
+    _rec_name = "attribute_id"
+    _description = "Product Config Session Custom Value"
+
+    @api.depends("attribute_id", "attribute_id.uom_id")
+    def _compute_val_name(self):
+        for attr_val_custom in self:
+            uom = attr_val_custom.attribute_id.uom_id.name
+            attr_val_custom.name = "%s%s" % (
+                attr_val_custom.value,
+                (" %s" % uom) or "",
+            )
+
+    name = fields.Char(readonly=True, compute="_compute_val_name", store=True)
+    attribute_id = fields.Many2one(
+        comodel_name="product.attribute", string="Attribute", required=True
+    )
+    cfg_session_id = fields.Many2one(
+        comodel_name="product.config.session",
+        required=True,
+        ondelete="cascade",
+        string="Session",
+    )
+    value = fields.Char(help="Custom value held as string")
+    attachment_ids = fields.Many2many(
+        comodel_name="ir.attachment",
+        relation="product_config_session_custom_value_attachment_rel",
+        column1="cfg_sesion_custom_val_id",
+        column2="attachment_id",
+        string="Attachments",
+    )
+
+    def eval(self):
+        """Return custom value evaluated using the related custom field type"""
+        field_type = self.attribute_id.custom_type
+        if field_type == "binary":
+            vals = self.attachment_ids.mapped("datas")
+            if len(vals) == 1:
+                return vals[0]
+            return vals
+        elif field_type == "integer":
+            return int(self.value)
+        elif field_type == "float":
+            return float(self.value)
+        return self.value
+
+    @api.constrains("cfg_session_id", "attribute_id")
+    def unique_attribute(self):
+        for custom_val in self:
+            if (
+                len(
+                    custom_val.cfg_session_id.custom_value_ids.filtered(
+                        lambda x: x.attribute_id == custom_val.attribute_id
+                    )
+                )
+                > 1
+            ):
+                raise ValidationError(
+                    _("Configuration cannot have the " "same value inserted twice")
+                )
+
+    # @api.constrains('cfg_session_id.value_ids')
+    # def custom_only(self):
+    #     """Verify that the attribute_id is not present in vals as well"""
+    #     import ipdb;ipdb.set_trace()
+    #     if self.cfg_session_id.value_ids.filtered(
+    #             lambda x: x.attribute_id == self.attribute_id):
+    #         raise ValidationError(
+    #             _("Configuration cannot have a selected option and a custom "
+    #               "value with the same attribute")
+    #         )
+
+    @api.constrains("attachment_ids", "value")
+    def check_custom_type(self):
+        for custom_val in self:
+            custom_type = custom_val.attribute_id.custom_type
+            if custom_val.value and custom_type == "binary":
+                raise ValidationError(
+                    _(
+                        "Attribute custom type is binary, attachments are the "
+                        "only accepted values with this custom field type"
+                    )
+                )
+            if custom_val.attachment_ids and custom_type != "binary":
+                raise ValidationError(
+                    _(
+                        "Attribute custom type must be 'binary' for saving "
+                        "attachments to custom value"
+                    )
+                )
diff --git a/product_configurator/readme/DESCRIPTION.rst b/product_configurator/readme/DESCRIPTION.rst
new file mode 100644
index 0000000000..a0b4c65152
--- /dev/null
+++ b/product_configurator/readme/DESCRIPTION.rst
@@ -0,0 +1,2 @@
+This module has all the mechanics to support product configuration. It serves as a base
+dependency for configuration interfaces.
diff --git a/product_configurator/security/configurator_security.xml b/product_configurator/security/configurator_security.xml
new file mode 100644
index 0000000000..8d7f835f04
--- /dev/null
+++ b/product_configurator/security/configurator_security.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <record id="product_config_category" model="ir.module.category">
+        <field name="name">Product Configurator</field>
+    </record>
+
+    <record id="group_product_configurator" model="res.groups">
+        <field name="name">User</field>
+        <field name="category_id" ref="product_config_category" />
+        <field name="implied_ids" eval="[(4, ref('product.group_product_variant'))]" />
+    </record>
+
+    <record id="group_product_configurator_manager" model="res.groups">
+        <field name="name">Manager</field>
+        <field name="category_id" ref="product_config_category" />
+        <field
+            name="implied_ids"
+            eval="[(4, ref('product_configurator.group_product_configurator'))]"
+        />
+        <field name="users" eval="[(4, ref('base.user_admin'))]" />
+    </record>
+
+    <!-- Set default to all employees -->
+    <record model="res.groups" id="base.group_user">
+        <field
+            name="implied_ids"
+            eval="[(4, ref('product_configurator.group_product_configurator'))]"
+        />
+    </record>
+    <record model="res.users" id="base.user_root">
+        <field
+            eval="[(4, ref('product_configurator.group_product_configurator_manager'))]"
+            name="groups_id"
+        />
+    </record>
+
+</odoo>
diff --git a/product_configurator/security/ir.model.access.csv b/product_configurator/security/ir.model.access.csv
new file mode 100644
index 0000000000..d9bfcf0869
--- /dev/null
+++ b/product_configurator/security/ir.model.access.csv
@@ -0,0 +1,40 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+product_configurator_config_line,Config Line,model_product_config_line,group_product_configurator,1,0,0,0
+product_configurator_config_image,Config Image,model_product_config_image,group_product_configurator,1,0,0,0
+product_configurator_config_step,Config Step,model_product_config_step,group_product_configurator,1,0,0,0
+product_configurator_config_step_line,Config Step Line,model_product_config_step_line,group_product_configurator,1,0,0,0
+product_configurator_config_domain,Config Domain,model_product_config_domain,group_product_configurator,1,0,0,0
+product_configurator_config_domain_line,Config Domain Line,model_product_config_domain_line,group_product_configurator,1,0,0,0
+product_configurator_custom_attribute_value,Attribute Value Line,model_product_attribute_value_line,group_product_configurator,1,0,0,0
+product_configurator_config_session,Config Session,model_product_config_session,group_product_configurator,1,1,1,1
+product_configurator_config_session_custom_value,Config Session Custom Value,model_product_config_session_custom_value,group_product_configurator,1,1,1,1
+user_config_line,User Config Line,model_product_config_line,base.group_user,1,0,0,0
+user_config_image,User Config Image,model_product_config_image,base.group_user,1,0,0,0
+user_config_step,User Config Step,model_product_config_step,base.group_user,1,0,0,0
+user_config_step_line,User Config Step Line,model_product_config_step_line,base.group_user,1,0,0,0
+user_config_domain_line,User Config Domain Line,model_product_config_domain_line,base.group_user,1,0,0,0
+user_config_domain,User Config Domain,model_product_config_domain,base.group_user,1,0,0,0
+user_custom_attribute_value,User Attribute Value Line,model_product_attribute_value_line,base.group_user,1,0,0,0
+user_config_session,User Config Session,model_product_config_session,base.group_user,1,0,0,0
+user_config_session_custom_value,User Config Session Custom Value,model_product_config_session_custom_value,base.group_user,1,0,0,0
+portal_config_image,Portal Config Image,model_product_config_image,base.group_portal,1,0,0,0
+portal_config_step,Portal Config Step,model_product_config_step,base.group_portal,1,0,0,0
+portal_config_session,Portal Config Session,model_product_config_session,base.group_portal,1,0,0,0
+portal_config_session_custom_value,Portal Config Session Custom Value,model_product_config_session_custom_value,base.group_portal,1,0,0,0
+portal_configurator_config_line,Portal Config Line,model_product_config_line,base.group_portal,1,0,0,0
+portal_configurator_config_step_line,Portal Config Step Line,model_product_config_step_line,base.group_portal,1,0,0,0
+portal_configurator_config_domain,Portal Config Domain,model_product_config_domain,base.group_portal,1,0,0,0
+portal_configurator_config_domain_line,Portal Config Domain Line,model_product_config_domain_line,base.group_portal,1,0,0,0
+product_configurator_config_line_manager,Config Line Manager,product_configurator.model_product_config_line,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_config_image_manager,Config Image Manager,product_configurator.model_product_config_image,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_config_step_manager,Config Step Manager,product_configurator.model_product_config_step,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_config_step_line_manager,Config Step Line Manager,product_configurator.model_product_config_step_line,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_config_domain_manager,Config Domain Manager,product_configurator.model_product_config_domain,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_config_domain_line_manager,Config Domain Line Manager,product_configurator.model_product_config_domain_line,product_configurator.group_product_configurator_manager,1,1,1,1
+product_configurator_custom_attribute_value_manager,Attribute Value Line Manager,product_configurator.model_product_attribute_value_line,product_configurator.group_product_configurator_manager,1,1,1,1
+access_product_template_product_config_user,product.template Product Config user,product.model_product_template,product_configurator.group_product_configurator,1,0,0,0
+access_product_template_product_config_manager,product.template Product Config Manager,product.model_product_template,product_configurator.group_product_configurator_manager,1,1,1,1
+access_product_product_product_config_user,product.product Product Config user,product.model_product_product,product_configurator.group_product_configurator,1,0,0,0
+access_product_product_product_config_manager,product.product Product Config Manager,product.model_product_product,product_configurator.group_product_configurator_manager,1,1,1,1
+access_product_attribute_line_product_config_manager,product.attribute line Product Config Manager,product.model_product_template_attribute_line,product_configurator.group_product_configurator_manager,1,1,1,1
+access_product_configurator_group,product_configurator,model_product_configurator,product_configurator.group_product_configurator,1,1,1,1
diff --git a/product_configurator/static/description/configurable-template.png b/product_configurator/static/description/configurable-template.png
new file mode 100644
index 0000000000..860c818ea0
Binary files /dev/null and b/product_configurator/static/description/configurable-template.png differ
diff --git a/product_configurator/static/description/cover.png b/product_configurator/static/description/cover.png
new file mode 100644
index 0000000000..be5e2578ce
Binary files /dev/null and b/product_configurator/static/description/cover.png differ
diff --git a/product_configurator/static/description/icon.png b/product_configurator/static/description/icon.png
new file mode 100644
index 0000000000..d5d307c648
Binary files /dev/null and b/product_configurator/static/description/icon.png differ
diff --git a/product_configurator/static/description/index.html b/product_configurator/static/description/index.html
new file mode 100644
index 0000000000..b30bc62175
--- /dev/null
+++ b/product_configurator/static/description/index.html
@@ -0,0 +1,75 @@
+<section class="oe_container">
+    <div class="oe_row oe_spaced">
+        <div class="oe_span12">
+            <h2 class="oe_slogan">Odoo Product Configurator</h2>
+            <h3 class="oe_slogan">Generate products on-demand, easy and error-free</h3>
+        </div>
+        <div class="oe_span12 mt32">
+            <div class="oe_centeralign oe_websiteonly">
+                <a href="http://www.pledra.com"><img src="pledra-logo.png" target="_blank" title="Pledra" alt="Pledra"/></a>
+            </div>
+        </div>
+    </div>
+</section>
+
+<section class="oe_container oe_dark mt64">
+    <div class="oe_row oe_spaced">
+        <h2 class="oe_slogan">Select your template</h2>
+        <div class="oe_span12">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture" src="wizard-template.png">
+            </div>
+        </div>
+    </div>
+</section>
+
+<section class="oe_container oe_mt64">
+    <div class="oe_row oe_spaced">
+        <h2 class="oe_slogan">Choose your options and get live image updates</h2>
+        <div class="oe_span12">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture oe_screenshot" src="wizard-color.png"/>
+            </div>
+        </div>
+    </div>
+</section>
+
+<section class="oe_container oe_dark oe_mt64">
+    <div class="oe_row oe_spaced">
+        <h2 class="oe_slogan">Custom values and multiple selections supported</h2>
+        <div class="oe_span12">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture oe_screenshot" src="wizard-last-step.png"/>
+            </div>
+        </div>
+    </div>
+</section>
+
+<section class="oe_container oe_dark oe_mt64">
+    <div class="oe_row oe_spaced">
+        <h2 class="oe_slogan">The wizard is generated automatically for you!</h2>
+        <h3 class="oe_slogan">Just define the attributes and rules on the template and you are done</h3>
+        <div class="oe_span12">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture oe_screenshot" src="configurable-template.png"/>
+            </div>
+        </div>
+    </div>
+</section>
+
+<section class="oe_container oe_mt64">
+    <div class="oe_row oe_spaced">
+        <h2 class="oe_slogan">Compatible with Odoo Enterprise and Community</h2>
+        <h3 class="oe_slogan">Odoo versions supported: 8 / 9 / 10</h3>
+        <div class="oe_span6">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture" src="odoo-enterprise-interface.png"/>
+            </div>
+        </div>
+        <div class="oe_span6">
+            <div class="oe_row_img oe_centered">
+                <img class="oe_picture" src="odoo-community-interface.png"/>
+            </div>
+        </div>
+    </div>
+</section>
diff --git a/product_configurator/static/description/odoo-community-interface.png b/product_configurator/static/description/odoo-community-interface.png
new file mode 100644
index 0000000000..e741d61531
Binary files /dev/null and b/product_configurator/static/description/odoo-community-interface.png differ
diff --git a/product_configurator/static/description/odoo-enterprise-interface.png b/product_configurator/static/description/odoo-enterprise-interface.png
new file mode 100644
index 0000000000..5b13b3dd19
Binary files /dev/null and b/product_configurator/static/description/odoo-enterprise-interface.png differ
diff --git a/product_configurator/static/description/pledra-logo.png b/product_configurator/static/description/pledra-logo.png
new file mode 100644
index 0000000000..b82d4ab00d
Binary files /dev/null and b/product_configurator/static/description/pledra-logo.png differ
diff --git a/product_configurator/static/description/quotation-updated.png b/product_configurator/static/description/quotation-updated.png
new file mode 100644
index 0000000000..8b0f5aafb3
Binary files /dev/null and b/product_configurator/static/description/quotation-updated.png differ
diff --git a/product_configurator/static/description/quotation.png b/product_configurator/static/description/quotation.png
new file mode 100644
index 0000000000..8ed8013836
Binary files /dev/null and b/product_configurator/static/description/quotation.png differ
diff --git a/product_configurator/static/description/wizard-color.png b/product_configurator/static/description/wizard-color.png
new file mode 100644
index 0000000000..396e046b74
Binary files /dev/null and b/product_configurator/static/description/wizard-color.png differ
diff --git a/product_configurator/static/description/wizard-last-step.png b/product_configurator/static/description/wizard-last-step.png
new file mode 100644
index 0000000000..3d89020dc4
Binary files /dev/null and b/product_configurator/static/description/wizard-last-step.png differ
diff --git a/product_configurator/static/description/wizard-template.png b/product_configurator/static/description/wizard-template.png
new file mode 100644
index 0000000000..948ce549be
Binary files /dev/null and b/product_configurator/static/description/wizard-template.png differ
diff --git a/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg
new file mode 100644
index 0000000000..a20f555251
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black-star-spoke-384.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg
new file mode 100644
index 0000000000..f7fa6043e8
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black-star-spoke-387.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-black.jpg b/product_configurator/static/img/2-series-coupe-black.jpg
new file mode 100644
index 0000000000..77584cb03e
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-black.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg
new file mode 100644
index 0000000000..47b2937e30
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-red-star-spoke-384.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg
new file mode 100644
index 0000000000..92d4f1ab75
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-red-star-spoke-387.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg b/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg
new file mode 100644
index 0000000000..5e747277eb
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver-star-spoke-384.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg b/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg
new file mode 100644
index 0000000000..b62f75fce7
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver-star-spoke-387.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe-silver.jpg b/product_configurator/static/img/2-series-coupe-silver.jpg
new file mode 100644
index 0000000000..2c5e003a50
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe-silver.jpg differ
diff --git a/product_configurator/static/img/2-series-coupe.jpg b/product_configurator/static/img/2-series-coupe.jpg
new file mode 100644
index 0000000000..0e4b3df727
Binary files /dev/null and b/product_configurator/static/img/2-series-coupe.jpg differ
diff --git a/product_configurator/static/img/product-advantage.jpg b/product_configurator/static/img/product-advantage.jpg
new file mode 100644
index 0000000000..60a6e1c1cc
Binary files /dev/null and b/product_configurator/static/img/product-advantage.jpg differ
diff --git a/product_configurator/static/img/product-armrest.jpg b/product_configurator/static/img/product-armrest.jpg
new file mode 100644
index 0000000000..97ab6c9551
Binary files /dev/null and b/product_configurator/static/img/product-armrest.jpg differ
diff --git a/product_configurator/static/img/product-engine.jpg b/product_configurator/static/img/product-engine.jpg
new file mode 100644
index 0000000000..2f1b6f6be5
Binary files /dev/null and b/product_configurator/static/img/product-engine.jpg differ
diff --git a/product_configurator/static/img/product-luxury-line.jpg b/product_configurator/static/img/product-luxury-line.jpg
new file mode 100644
index 0000000000..0fd9248e59
Binary files /dev/null and b/product_configurator/static/img/product-luxury-line.jpg differ
diff --git a/product_configurator/static/img/product-m-sport.jpg b/product_configurator/static/img/product-m-sport.jpg
new file mode 100644
index 0000000000..0cfc029d9e
Binary files /dev/null and b/product_configurator/static/img/product-m-sport.jpg differ
diff --git a/product_configurator/static/img/product-paint-silver.jpg b/product_configurator/static/img/product-paint-silver.jpg
new file mode 100644
index 0000000000..b49c8619d4
Binary files /dev/null and b/product_configurator/static/img/product-paint-silver.jpg differ
diff --git a/product_configurator/static/img/product-smoker-package.jpg b/product_configurator/static/img/product-smoker-package.jpg
new file mode 100644
index 0000000000..8cdaf37412
Binary files /dev/null and b/product_configurator/static/img/product-smoker-package.jpg differ
diff --git a/product_configurator/static/img/product-sport-line.jpg b/product_configurator/static/img/product-sport-line.jpg
new file mode 100644
index 0000000000..7f18487aa5
Binary files /dev/null and b/product_configurator/static/img/product-sport-line.jpg differ
diff --git a/product_configurator/static/img/product-sunroof.jpg b/product_configurator/static/img/product-sunroof.jpg
new file mode 100644
index 0000000000..11c5c87587
Binary files /dev/null and b/product_configurator/static/img/product-sunroof.jpg differ
diff --git a/product_configurator/static/img/product-towhook.jpg b/product_configurator/static/img/product-towhook.jpg
new file mode 100644
index 0000000000..2f4d10f8b7
Binary files /dev/null and b/product_configurator/static/img/product-towhook.jpg differ
diff --git a/product_configurator/static/img/product-transmission-steptronic-sport.jpg b/product_configurator/static/img/product-transmission-steptronic-sport.jpg
new file mode 100644
index 0000000000..84e4956301
Binary files /dev/null and b/product_configurator/static/img/product-transmission-steptronic-sport.jpg differ
diff --git a/product_configurator/static/img/product-transmission-steptronic.jpg b/product_configurator/static/img/product-transmission-steptronic.jpg
new file mode 100644
index 0000000000..a46a0f7ff4
Binary files /dev/null and b/product_configurator/static/img/product-transmission-steptronic.jpg differ
diff --git a/product_configurator/static/src/js/boolean_button_widget.esm.js b/product_configurator/static/src/js/boolean_button_widget.esm.js
new file mode 100644
index 0000000000..0973764baf
--- /dev/null
+++ b/product_configurator/static/src/js/boolean_button_widget.esm.js
@@ -0,0 +1,58 @@
+/** @odoo-module **/
+const {onMounted, onRendered, useRef, useState} = owl;
+import {BooleanField} from "@web/views/fields/boolean/boolean_field";
+import {registry} from "@web/core/registry";
+import {standardFieldProps} from "@web/views/fields/standard_field_props";
+
+export class BooleanButtonField extends BooleanField {
+    setup() {
+        super.setup();
+        this.state1 = useState({value: 0});
+        this.root = useRef("root");
+        onMounted(() => {
+            this.updateConfigurableButton();
+        });
+        onRendered(() => {
+            this.updateConfigurableButton();
+        });
+    }
+
+    onChange() {
+        this.state1.value++;
+    }
+
+    updateConfigurableButton() {
+        this.text = this.props.value
+            ? this.props.activeString
+            : this.props.inactiveString;
+        this.hover = this.props.value
+            ? this.props.inactiveString
+            : this.props.activeString;
+        var val_color = this.props.value ? "text-success" : "text-danger";
+        var hover_color = this.props.value ? "text-danger" : "text-success";
+        var $val = $("<span>")
+            .addClass("o_stat_text o_boolean_button o_not_hover " + val_color)
+            .text(this.text);
+        var $hover = $("<span>")
+            .addClass("o_stat_text o_boolean_button o_hover d-none " + hover_color)
+            .text(this.hover);
+        $(this.root.el).empty();
+        $(this.root.el).append($val).append($hover);
+    }
+}
+
+BooleanButtonField.props = {
+    ...standardFieldProps,
+    activeString: {type: String, optional: true},
+    inactiveString: {type: String, optional: true},
+};
+
+BooleanButtonField.extractProps = ({attrs}) => {
+    return {
+        activeString: attrs.options.active,
+        inactiveString: attrs.options.inactive,
+    };
+};
+
+BooleanButtonField.template = "product_configurator.BooleanButtonField";
+registry.category("fields").add("boolean_button", BooleanButtonField);
diff --git a/product_configurator/static/src/js/boolean_button_widget.xml b/product_configurator/static/src/js/boolean_button_widget.xml
new file mode 100644
index 0000000000..b1d6447d50
--- /dev/null
+++ b/product_configurator/static/src/js/boolean_button_widget.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<templates xml:space="preserve">
+
+    <t t-name="product_configurator.BooleanButtonField" owl="1">
+        <div t-ref="root" t-on-click="onChange">
+            <CheckBox
+                id="props.id"
+                value="props.value or false"
+                className="'d-inline-block me-2'"
+                disabled="isReadonly"
+                onChange.bind="onChange"
+            />
+        </div>
+    </t>
+
+</templates>
diff --git a/product_configurator/static/src/js/form_widgets.js b/product_configurator/static/src/js/form_widgets.js
new file mode 100644
index 0000000000..b45e17f26f
--- /dev/null
+++ b/product_configurator/static/src/js/form_widgets.js
@@ -0,0 +1,79 @@
+odoo.define("product_configurator.FieldBooleanButton", function (require) {
+    "use strict";
+
+    var FormController = require("web.FormController");
+    var ListController = require("web.ListController");
+    var KanbanController = require("web.KanbanController");
+
+    var pyUtils = require("web.py_utils");
+
+    FormController.include({
+        /* eslint-disable no-unused-vars*/
+        renderButtons: function ($node) {
+            var self = this;
+            this._super.apply(this, arguments);
+            if (
+                self.modelName === "product.product" &&
+                self.initialState.context.custom_create_variant
+            ) {
+                this.$buttons.find(".o_form_button_create").css("display", "none");
+            }
+        },
+        /* eslint-disable no-unused-vars*/
+
+        _onButtonClicked: function (event) {
+            var self = this;
+            var attrs = event.data.attrs;
+            if (event.data.attrs.context) {
+                var record_ctx = self.model.get(event.data.record.id).context;
+                var btn_ctx = pyUtils.eval(
+                    "context",
+                    record_ctx,
+                    event.data.attrs.context
+                );
+                self.model.localData[event.data.record.id].context = _.extend(
+                    {},
+                    btn_ctx,
+                    record_ctx
+                );
+            }
+            if (attrs.special === "no_save") {
+                this.canBeSaved = function () {
+                    return true;
+                };
+                var event_no_save = $.extend(true, {}, event);
+                event_no_save.data.attrs.special = false;
+                return this._super(event_no_save);
+            }
+            this._super(event);
+        },
+    });
+    ListController.include({
+        /* eslint-disable no-unused-vars*/
+        renderButtons: function ($node) {
+            var self = this;
+            this._super.apply(this, arguments);
+            if (
+                self.modelName === "product.product" &&
+                self.initialState.context.custom_create_variant
+            ) {
+                this.$buttons.find(".o_list_button_add").css("display", "none");
+            }
+        },
+        /* eslint-disable no-unused-vars*/
+    });
+    KanbanController.include({
+        /* eslint-disable no-unused-vars*/
+        renderButtons: function ($node) {
+            var self = this;
+            this._super.apply(this, arguments);
+            if (
+                self.modelName === "product.product" &&
+                self.initialState.context.custom_create_variant
+            ) {
+                this.$buttons.find(".o-kanban-button-new").css("display", "none");
+            }
+        },
+        /* eslint-disable no-unused-vars*/
+    });
+});
diff --git a/product_configurator/static/src/js/relational_fields.js b/product_configurator/static/src/js/relational_fields.js
new file mode 100644
index 0000000000..cabb0950e7
--- /dev/null
+++ b/product_configurator/static/src/js/relational_fields.js
@@ -0,0 +1,26 @@
+odoo.define("product_configurator.FieldStatus", function (require) {
+    "use strict";
+
+    var fields = require("web.relational_fields");
+    var FieldStatus = fields.FieldStatus;
+
+    FieldStatus.include({
+        /* Prase input as string in order to have a clickable statusbar*/
+        _onClickStage: function (e) {
+            this._setValue(String($(e.currentTarget).data("value")));
+        },
+    });
+
+    /* Bug from odoo: in case of widget many2many_tags $input and $el do not exist
+    in 'this', so it returns 'undefine', but setIDForLabel(method in AbstractField)
+    expecting getFocusableElement always return object*/
+    fields.FieldMany2One.include({
+        getFocusableElement: function () {
+            var element = this._super.apply(this, arguments);
+            if (element === undefined) {
+                return $();
+            }
+            return element;
+        },
+    });
+});
diff --git a/product_configurator/static/src/scss/form_widget.scss b/product_configurator/static/src/scss/form_widget.scss
new file mode 100644
index 0000000000..7a9323bc16
--- /dev/null
+++ b/product_configurator/static/src/scss/form_widget.scss
@@ -0,0 +1,20 @@
+.oe_stat_button {
+    &:hover {
+        .o_boolean_button.o_not_hover {
+            display: none;
+        }
+        .o_boolean_button.o_hover {
+            display: inline-block !important;
+        }
+    }
+}
+
+.oe_prod_config_image {
+    img {
+        min-height: 30px;
+    }
+}
+
+.pull-right {
+    float: right;
+}
diff --git a/product_configurator/views/product_attribute_view.xml b/product_configurator/views/product_attribute_view.xml
new file mode 100644
index 0000000000..2316db3acc
--- /dev/null
+++ b/product_configurator/views/product_attribute_view.xml
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Product Attributes -->
+
+    <record id="product_attribute_tree_view" model="ir.ui.view">
+        <field name="name">product.config.product.attribute.tree</field>
+        <field name="model">product.attribute</field>
+        <field name="inherit_id" ref="product.attribute_tree_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='name']" position="after">
+                <field name="search_ok" />
+            </xpath>
+        </field>
+    </record>
+
+    <record model="ir.ui.view" id="product_attribute_form_view">
+        <field name="name">product.attribute.form.view</field>
+        <field name="model">product.attribute</field>
+        <field name="priority">100</field>
+        <field name="inherit_id" ref="product.product_attribute_view_form" />
+        <field name="arch" type="xml">
+            <xpath expr="//form//group[@name='main_fields']" position="before">
+                <div
+                    class="oe_left"
+                    style="width: 500px;"
+                    invisible="not context.get('flag_config_ok')"
+                >
+                    <field name="image" widget="image" class="oe_avatar oe_left" />
+                </div>
+            </xpath>
+            <xpath expr="//group[@name='main_fields']" position="inside">
+                <field name="active" invisible="not context.get('flag_config_ok')" />
+            </xpath>
+            <xpath expr="//field[@name='create_variant']" position="attributes">
+                <attribute name="groups">base.group_no_one</attribute>
+            </xpath>
+            <xpath expr="//notebook//page" position="after">
+                <page
+                    string="Configurator"
+                    groups="product_configurator.group_product_configurator_manager"
+                    invisible="not context.get('flag_config_ok')"
+                >
+                    <group>
+                        <group>
+                            <field name="required" />
+                            <field
+                                name="multi"
+                                attrs="{'readonly': [('val_custom','=',True)]}"
+                                force_save="1"
+                            />
+                            <field
+                                name="val_custom"
+                                attrs="{'readonly': [('multi', '=', True)]}"
+                                force_save="1"
+                            />
+                        </group>
+                        <group>
+                            <field name="uom_id" />
+                        </group>
+                        <field name="description" colspan="4" />
+                    </group>
+                </page>
+                <page
+                    string="Custom Values"
+                    invisible="not context.get('flag_config_ok')"
+                    attrs="{'invisible': [('val_custom', '!=', True)]}"
+                    groups="product_configurator.group_product_configurator_manager"
+                >
+                    <group>
+                        <group>
+                            <field name="custom_type" />
+                            <field
+                                name="min_val"
+                                attrs="{'invisible': [('custom_type','not in',['integer','float'])]}"
+                            />
+                            <field
+                                name="max_val"
+                                attrs="{'invisible': [('custom_type','not in',['integer','float'])]}"
+                            />
+                        </group>
+                        <group>
+                            <field name="search_ok" />
+                        </group>
+                    </group>
+                </page>
+            </xpath>
+        </field>
+    </record>
+
+    <record model="ir.actions.act_window" id="action_attributes_view">
+        <field name="name">Attributes</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.attribute</field>
+        <field name="view_mode">tree,form</field>
+        <field name="context">{'flag_config_ok': True}</field>
+    </record>
+
+    <menuitem
+        id="menu_attribute_action_configuration"
+        action="action_attributes_view"
+        parent="menu_product_configurable_settings"
+        sequence="10"
+        groups="product.group_product_variant"
+    />
+
+    <!-- Product Attribute Value -->
+
+    <record
+        id="product_template_attribute_value_view_tree_weight_extra"
+        model="ir.ui.view"
+    >
+        <field
+            name="name"
+        >product.template.attribute.value.view.tree.weight.extra</field>
+        <field name="model">product.template.attribute.value</field>
+        <field
+            name="inherit_id"
+            ref="product.product_template_attribute_value_view_tree"
+        />
+        <field name="type">tree</field>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='price_extra']" position="after">
+                <field
+                    name="weight_extra"
+                    groups="product_configurator.group_product_configurator_manager"
+                />
+            </xpath>
+        </field>
+    </record>
+
+    <record
+        id="product_template_attribute_value_view_form_weight_extra"
+        model="ir.ui.view"
+    >
+        <field
+            name="name"
+        >product.template.attribute.value.view.form.weight.extra</field>
+        <field name="model">product.template.attribute.value</field>
+        <field
+            name="inherit_id"
+            ref="product.product_template_attribute_value_view_form"
+        />
+        <field name="type">form</field>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='price_extra']" position="before">
+                <field
+                    name="weight_extra"
+                    groups="product_configurator.group_product_configurator_manager"
+                />
+            </xpath>
+        </field>
+    </record>
+
+    <!-- Product Attribute Value -->
+
+    <record id="variants_tree_view" model="ir.ui.view">
+        <field name="name">product.attribute.value.tree</field>
+        <field name="model">product.attribute.value</field>
+        <field name="arch" type="xml">
+            <tree>
+                <field name="sequence" widget="handle" />
+                <field name="attribute_id" />
+                <field name="name" />
+            </tree>
+        </field>
+    </record>
+
+    <record model="ir.ui.view" id="product_attribute_value_form_view">
+        <field name="name">product.config.product.attribute.value.form.view</field>
+        <field name="model">product.attribute.value</field>
+        <field name="arch" type="xml">
+            <form string="Product Attribute Values">
+                <sheet>
+                    <field name="image" widget="image" class="oe_avatar" />
+                    <div class="oe_left" style="width: 500px;">
+                        <div class="oe_title" style="width: 390px;">
+                            <label class="oe_edit_only" for="name" string="Value" />
+                            <h1><field name="name" class="oe_inline" /></h1>
+                            <label for="active" /><field name="active" />
+                        </div>
+                    </div>
+                    <group>
+                        <group>
+                            <field name="attribute_id" />
+                        </group>
+                        <group>
+                            <field name="product_id" />
+                        </group>
+                    </group>
+                </sheet>
+            </form>
+        </field>
+    </record>
+
+    <record id="variants_action" model="ir.actions.act_window">
+        <field name="name">Attribute Values</field>
+        <field name="type">ir.actions.act_window</field>
+        <field name="res_model">product.attribute.value</field>
+        <field name="view_mode">tree,form</field>
+    </record>
+
+    <menuitem
+        id="menu_variants_action_configuration"
+        action="variants_action"
+        parent="menu_product_configurable_settings"
+        sequence="20"
+        groups="product.group_product_variant"
+    />
+
+    <!-- Product Attribute Line -->
+    <record id="product_template_attribute_line_form_config" model="ir.ui.view">
+        <field name="name">product.template.attribute.line.form</field>
+        <field name="model">product.template.attribute.line</field>
+        <field name="inherit_id" ref="product.product_template_attribute_line_form" />
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='attribute_id']" position="attributes">
+                <attribute
+                    name="context"
+                >{'flag_config_ok': context.get('default_config_ok', False)}</attribute>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>
diff --git a/product_configurator/views/product_config_view.xml b/product_configurator/views/product_config_view.xml
new file mode 100644
index 0000000000..9a132f28fc
--- /dev/null
+++ b/product_configurator/views/product_config_view.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <!-- Configuration Steps -->
+
+    <record id="config_step_form_view" model="ir.ui.view">
+        <field name="name">product.configurator.config.step.form</field>
+        <field name="model">product.config.step</field>
+        <field name="arch" type="xml">
+            <form string="Configuration Step">
+                <sheet>
+                    <group>
+                        <group>
+                            <field name="name" />
+                        </group>
+                    </group>
+                </sheet>
+            </form>
+        </field>
+    </record>
+
+    <record id="config_step_tree_view" model="ir.ui.view">
+        <field name="name">product.configurator.config.step.tree</field>
+        <field name="model">product.config.step</field>
+        <field name="arch" type="xml">
+            <form string="Configuration Step">
+                <tree name="name">
+                    <field name="name" />
+                </tree>
+            </form>
+        </field>
+    </record>
+
+    <!-- Configuration Restrictions -->
+
+    <record id="product_config_domain_form_view" model="ir.ui.view">
+        <field name="name">product.configurator.domain.form</field>
+        <field name="model">product.config.domain</field>
+        <field name="arch" type="xml">
+            <form string="Configuration Restrictions">
+                <group>
+                    <group>
+                        <field name="name" />
+                    </group>
+                </group>
+                <notebook>
+                    <page string="Rules">
+                        <field name="domain_line_ids">
+                            <tree editable="bottom">
+                                <field name="sequence" widget="handle" />
+                                <field
+                                    name="attribute_id"
+                                    options="{'no_create': True, 'no_create_edit': True}"
+                                />
+                                <field name="condition" />
+                                <field
+                                    name="value_ids"
+                                    widget="many2many_tags"
+                                    options="{'no_create': True, 'no_create_edit': True}"
+                                    context="{'show_attribute': False}"
+                                    domain="[('attribute_id', '=', attribute_id)]"
+                                />
+                                <field name="operator" />
+                            </tree>
+                        </field>
+                    </page>
+                    <page string="Inherited">
+                        <field name="implied_ids" />
+                    </page>
+                </notebook>
+            </form>
+        </field>
+    </record>
+
+    <!-- Created seprate view for the template -->
+    <record id="product_config_domain_form_view_template" model="ir.ui.view">
+        <field name="name">product.configurator.domain.form.template</field>
+        <field name="model">product.config.domain</field>
+        <field name="arch" type="xml">
+            <form string="Configuration Restrictions">
+                <group>
+                    <group>
+                        <field name="name" />
+                    </group>
+                </group>
+                <notebook>
+                    <page string="Rules">
+                        <field name="domain_line_ids">
+                            <tree editable="bottom">
+                                <field name="sequence" widget="handle" />
+
+                                <field
+                                    name="attribute_id"
+                                    domain="[('id', 'in', context.get('product_attribute_ids', []))]"
+                                    options="{'no_create': True, 'no_create_edit': True}"
+                                />
+                                <field name="condition" />
+                                <field
+                                    name="template_attribute_value_ids"
+                                    invisible="1"
+                                />
+                                <field
+                                    name="value_ids"
+                                    widget="many2many_tags"
+                                    options="{'no_create': True, 'no_create_edit': True}"
+                                    context="{'show_attribute': False}"
+                                    domain="[('id', 'in', template_attribute_value_ids)]"
+                                />
+                                <field name="operator" />
+                            </tree>
+                        </field>
+                    </page>
+                    <page string="Inherited">
+                        <field name="implied_ids" />
+                    </page>
+                </notebook>
+            </form>
+        </field>
+    </record>
+
+    <record id="product_config_session_tree_view" model="ir.ui.view">
+        <field name="name">product.config.session.tree</field>
+        <field name="model">product.config.session</field>
+        <field name="arch" type="xml">
+            <tree create="false">
+                <field name="name" />
+                <field name="product_tmpl_id" />
+                <field name="value_ids" widget="many2many_tags" />
+                <field name="user_id" />
+                <field name="custom_value_ids" />
+                <field name="currency_id" invisible="1" />
+                <field
+                    name="price"
+                    widget='monetary'
+                    options="{'currency_field': 'currency_id', 'field_digits': True}"
+                />
+                <field name="config_step_name" />
+                <field name="product_id" />
+                <field name="state" />
+            </tree>
+        </field>
+    </record>
+
+    <record id="product_config_session_form_view" model="ir.ui.view">
+        <field name="name">product.config.session.form</field>
+        <field name="model">product.config.session</field>
+        <field name="arch" type="xml">
+            <form string="Configuration Sessions" editable="bottom" create="false">
+                <header>
+                    <field name="state" widget="statusbar" />
+                </header>
+                <sheet>
+                    <group>
+                        <h1><field name="name" nolabel="1" /></h1>
+                    </group>
+                    <group>
+                        <field name="product_tmpl_id" />
+                        <field name="user_id" />
+                        <field name="value_ids" widget="many2many_tags" />
+                        <field name="currency_id" invisible="1" />
+                        <field
+                            name="price"
+                            widget='monetary'
+                            options="{'currency_field': 'currency_id', 'field_digits': True}"
+                        />
+                        <field name="config_step_name" />
+                        <field name="product_id" />
+                    </group>
+                    <notebook>
+                        <page name="custom_vals" string="Custom Values">
+                            <field name="custom_value_ids">
+                                <tree editable="bottom">
+                                    <field name="attribute_id" />
+                                    <field name="value" />
+                                    <field
+                                        name="attachment_ids"
+                                        widget="many2many_tags"
+                                    />
+                                </tree>
+                            </field>
+                        </page>
+                    </notebook>
+                </sheet>
+            </form>
+        </field>
+    </record>
+
+</odoo>
diff --git a/product_configurator/views/product_view.xml b/product_configurator/views/product_view.xml
new file mode 100644
index 0000000000..3d8d26c068
--- /dev/null
+++ b/product_configurator/views/product_view.xml
@@ -0,0 +1,389 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <record id="product_template_form_view_config_inherited" model="ir.ui.view">
+        <field name="name">product.template.common.form</field>
+        <field name="model">product.template</field>
+        <field name="priority">16</field>
+        <field name="inherit_id" ref="product.product_template_form_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//header" position="inside">
+                <field name="config_ok" invisible="1" />
+            </xpath>
+        </field>
+    </record>
+
+    <record id="product_template_only_form_view_inherited" model="ir.ui.view">
+        <field name="name">product.configurator.product.template.form</field>
+        <field name="model">product.template</field>
+        <field name="priority">16</field>
+        <field name="inherit_id" ref="product.product_template_only_form_view" />
+        <field name="arch" type="xml">
+            <xpath expr='//header' position="inside">
+                <button
+                    name="configure_product"
+                    class="oe_highlight"
+                    type="object"
+                    string="Configure Product"
+                    groups="product_configurator.group_product_configurator"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                />
+            </xpath>
+            <div name="button_box" position="inside">
+                <button
+                    class="oe_stat_button"
+                    name="toggle_config"
+                    string="Configurable"
+                    type="object"
+                    icon="fa-wrench"
+                    groups="product_configurator.group_product_configurator_manager"
+                >
+                    <field
+                        name="config_ok"
+                        string="Configurable"
+                        options="{&quot;active&quot;: &quot;Configurable&quot;, &quot;inactive&quot;: &quot;Standard&quot;}"
+                        widget="boolean_button"
+                    />
+                </button>
+            </div>
+            <xpath
+                expr="//label[@for='purchase_ok' or @for='sale_ok']"
+                position="after"
+            >
+                <div class="oe_left" name="options" groups="base.group_user">
+                    <field name="id" invisible="True" />
+                </div>
+            </xpath>
+
+            <!-- Product attributes -->
+            <xpath expr="//field[@name='attribute_line_ids']" position="attributes">
+                <attribute name="context">{
+                    'show_attribute': False,
+                    'attribute_line_ids': attribute_line_ids,
+                }</attribute>
+            </xpath>
+
+            <!-- TODO: Implement a method to hide this field for non-configurable product templates -->
+            <xpath
+                expr="//field[@name='attribute_line_ids']/tree/field[@name='value_ids']"
+                position="after"
+            >
+                <field
+                    name="default_val"
+                    domain="[('id', 'in', value_ids)]"
+                    context="{'show_attribute': False}"
+                    options="{'no_create': True, 'no_create_edit': True}"
+                    invisible="not context.get('default_config_ok', False)"
+                />
+                <field
+                    name="required"
+                    invisible="not context.get('default_config_ok', False)"
+                />
+                <field
+                    name="multi"
+                    invisible="not context.get('default_config_ok', False)"
+                    attrs="{'readonly': [('custom','=',True)]}"
+                    force_save="1"
+                />
+                <field
+                    name="custom"
+                    invisible="not context.get('default_config_ok', False)"
+                    attrs="{'readonly': [('multi','=',True)]}"
+                    force_save="1"
+                />
+            </xpath>
+
+            <xpath
+                expr="//field[@name='attribute_line_ids']/tree/field[@name='value_ids']"
+                position="attributes"
+            >
+                <attribute name="attrs">{'required': [('custom','!=',True)]}</attribute>
+            </xpath>
+
+            <xpath
+                expr="//field[@name='attribute_line_ids']/tree/field[@name='attribute_id']"
+                position="attributes"
+            >
+                <attribute
+                    name="context"
+                >{'flag_config_ok': context.get('default_config_ok', False)}</attribute>
+            </xpath>
+
+            <xpath
+                expr="//field[@name='attribute_line_ids']/tree/field[@name='attribute_id']"
+                position="before"
+            >
+                <field name="sequence" widget="handle" />
+            </xpath>
+
+            <!-- TODO: Apply domains so only values from template are available -->
+            <xpath expr="//notebook/page[@name='variants']" position="after">
+                <page
+                    string="Configurator"
+                    name="configurator"
+                    attrs="{'invisible': [('config_ok','=',False)]}"
+                    groups="product_configurator.group_product_configurator"
+                >
+                <separator
+                        colspan="4"
+                        string="Configuration Restrictions"
+                        name="configurator_restrictions"
+                    />
+                <field
+                        name="config_line_ids"
+                        attrs="{'readonly': [('attribute_line_ids','=',[])]}"
+                        context="{'show_attribute': False}"
+                    >
+                    <tree editable="bottom">
+                        <field name="sequence" widget="handle" />
+                        <field
+                                name="attribute_line_id"
+                                domain="[('product_tmpl_id','=',parent.id)]"
+                                options="{'no_create': True, 'no_create_edit': True}"
+                            />
+                        <!-- # TODO: Find a more elegant way to restrict the value_ids -->
+                        <field
+                                name="attr_line_val_ids"
+                                widget="many2many_tags"
+                                invisible="True"
+                            />
+                        <field
+                                name="value_ids"
+                                widget="many2many_tags"
+                                attrs="{'readonly': [('attribute_line_id','=',False)]}"
+                                domain="[('id','in',attr_line_val_ids)]"
+                                options="{'no_create': True, 'no_create_edit': True}"
+                                context="{'show_attribute': False}"
+                            />
+                        <field
+                                name="domain_id"
+                                context="{'product_tmpl_id': product_tmpl_id or active_id or parent.id, 'product_attribute_ids': template_attribute_ids, 'form_view_ref': 'product_configurator.product_config_domain_form_view_template'}"
+                            />
+                        <field name="product_tmpl_id" invisible="1" />
+                        <field name="template_attribute_ids" invisible="1" />
+
+                    </tree>
+                </field>
+                <separator
+                        colspan="4"
+                        string="Configuration Steps"
+                        name="configurator_steps"
+                    />
+                <field
+                        name="config_step_line_ids"
+                        attrs="{'readonly': [('attribute_line_ids','=',[])]}"
+                    >
+                    <tree editable="bottom">
+                        <field name="sequence" widget="handle" />
+                        <field name="config_step_id" />
+                        <field
+                                name="attribute_line_ids"
+                                required="1"
+                                domain="[('product_tmpl_id', '=', parent.id)]"
+                                options="{'no_create': True, 'no_create_edit': True}"
+                                widget="many2many_tags"
+                            />
+                    </tree>
+                </field>
+                <separator
+                        colspan="4"
+                        string="Configuration Images"
+                        name="configurator_images"
+                    />
+                <field name="config_image_ids">
+                    <tree editable="bottom">
+                        <field name="sequence" widget="handle" />
+                        <field name="name" />
+                        <field
+                                name="value_ids"
+                                widget="many2many_tags"
+                                context="{'_cfg_product_tmpl_id': parent.id}"
+                            />
+                        <field
+                                name="image_1920"
+                                widget="image"
+                                class="oe_prod_config_image"
+                            />
+                    </tree>
+                </field>
+                <field name="attribute_line_val_ids" invisible="1" />
+                <field name="attribute_value_line_ids" invisible="1">
+                    <tree
+                            editable="bottom"
+                            context="{'default_product_tmpl_id': self.id}"
+                        >
+                        <field name="product_tmpl_id" invisible="1" />
+                        <field name="attribute_id" invisible="1" />
+                        <field name="sequence" widget="handle" />
+                        <field
+                                name="value_id"
+                                domain="[('id', 'in', parent.attribute_line_val_ids)]"
+                            />
+                        <field
+                                name="value_ids"
+                                domain="[
+                                ('id', 'in', parent.attribute_line_val_ids),
+                                ('attribute_id', '!=', attribute_id)
+                               ]"
+                                widget="many2many_tags"
+                            />
+                    </tree>
+                </field>
+                <separator
+                        string="Variant Name"
+                        colspan="4"
+                        groups="product_configurator.group_product_configurator_manager"
+                    />
+                    <field
+                        name="mako_tmpl_name"
+                        groups="product_configurator.group_product_configurator_manager"
+                    />
+                </page>
+            </xpath>
+
+            <xpath expr="//field[@name='default_code']" position="attributes">
+                <attribute
+                    name="attrs"
+                >{'invisible': [('config_ok','=',True)]}</attribute>
+            </xpath>
+
+        </field>
+    </record>
+
+    <record id="product_template_search_view" model="ir.ui.view">
+        <field name="name">product.configurator.product.template.search.view</field>
+        <field name="model">product.template</field>
+        <field name="inherit_id" ref="product.product_template_search_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//filter[@name='filter_to_sell']" position="after">
+                <separator />
+                <filter
+                    string="Standard Products"
+                    name="filter_standard_products"
+                    domain="[('config_ok','=',False)]"
+                />
+                <filter
+                    string="Configurable Products"
+                    name="filter_config_ok"
+                    domain="[('config_ok','=',True)]"
+                />
+            </xpath>
+        </field>
+    </record>
+
+    <record id="template_view_tree_configurable" model="ir.ui.view">
+        <field name="name">product.template.product.tree</field>
+        <field name="model">product.template</field>
+        <field name="inherit_id" ref="product.product_template_tree_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='default_code']" position="attributes">
+                <attribute
+                    name="invisible"
+                >context.get('default_config_ok', 0)</attribute>
+            </xpath>
+        </field>
+    </record>
+
+    <record id="product_template_kanban_view_inherited" model="ir.ui.view">
+        <field name="name">Product.template.product.kanban</field>
+        <field name="model">product.template</field>
+        <field name="inherit_id" ref="product.product_template_kanban_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//kanban" position="inside">
+                <field name="config_ok" />
+            </xpath>
+            <xpath expr="//field[@name='priority']" position="before">
+                <div
+                    class="pull-right"
+                    groups="product_configurator.group_product_configurator"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                >
+                    <a name="configure_product" type="object">
+                        <i class="fa fa-wrench fa-lg" title="Configure Product" />
+                    </a>
+                </div>
+            </xpath>
+        </field>
+    </record>
+
+    <record id="product_form_view_custom_vals_inherit" model="ir.ui.view">
+        <field name="name">product.configurator.form.view.custom.vals</field>
+        <field name="model">product.product</field>
+        <field name="inherit_id" ref="product.product_normal_form_view" />
+        <field name="arch" type="xml">
+            <xpath expr='//header' position="inside">
+                <button
+                    name="reconfigure_product"
+                    groups="product_configurator.group_product_configurator"
+                    class="oe_highlight"
+                    type="object"
+                    string="Reconfigure Product"
+                    attrs="{'invisible': [('config_ok','=',False)]}"
+                />
+            </xpath>
+            <!-- <xpath expr="//button[@name='toggle_config']" position="attributes">
+                <attribute name="invisible">1</attribute>
+            </xpath> -->
+            <xpath expr="//div[@name='options']" position="inside">
+                <field
+                    name="config_preset_ok"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                />
+                <label
+                    for="config_preset_ok"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                />
+            </xpath>
+        </field>
+    </record>
+
+    <record id="product_variant_easy_edit_view_inherit" model="ir.ui.view">
+        <field name="name">product.product.view.form.easy</field>
+        <field name="model">product.product</field>
+        <field name="inherit_id" ref="product.product_variant_easy_edit_view" />
+        <field name="mode">primary</field>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='active']" position="after">
+                <field name="config_ok" invisible="1" />
+                <field
+                    name="config_preset_ok"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                />
+            </xpath>
+            <xpath expr="//field[@name='weight']" position="attributes">
+                <attribute
+                    name="attrs"
+                >{'readonly': [('config_ok', '=', True)]}</attribute>
+            </xpath>
+        </field>
+    </record>
+
+    <record id="product_kanban_view_inherited" model="ir.ui.view">
+        <field name="name">Product Kanban</field>
+        <field name="model">product.product</field>
+        <field name="inherit_id" ref="product.product_kanban_view" />
+        <field name="arch" type="xml">
+            <xpath expr="//kanban" position="inside">
+                <field name="config_ok" />
+            </xpath>
+            <xpath
+                expr="//div/div[2]/strong[hasclass('o_kanban_record_title')]"
+                position="after"
+            >
+                <div
+                    class="pull-right"
+                    groups="product_configurator.group_product_configurator"
+                    attrs="{'invisible': [('config_ok', '=', False)]}"
+                >
+                    <a
+                        name="reconfigure_product"
+                        type="object"
+                        class="fa fa-repeat fa-lg"
+                    >
+                    </a>
+                </div>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>
diff --git a/product_configurator/views/res_config_settings_view.xml b/product_configurator/views/res_config_settings_view.xml
new file mode 100644
index 0000000000..1e85e6d434
--- /dev/null
+++ b/product_configurator/views/res_config_settings_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+
+    <record id="configurator_settings_view_form" model="ir.ui.view">
+        <field name="name">res.config.settings.view.form.inherit.sale</field>
+        <field name="model">res.config.settings</field>
+        <field name="priority" eval="10" />
+        <field name="inherit_id" ref="base.res_config_settings_view_form" />
+        <field name="arch" type="xml">
+            <xpath expr="//div[hasclass('settings')]" position="inside">
+                <div
+                    class="app_settings_block"
+                    data-string="Product Configurator"
+                    string="Product Configurator"
+                    data-key="product_configurator"
+                >
+                </div>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>
diff --git a/product_configurator/wizard/__init__.py b/product_configurator/wizard/__init__.py
new file mode 100644
index 0000000000..3c76586e5c
--- /dev/null
+++ b/product_configurator/wizard/__init__.py
@@ -0,0 +1,1 @@
+from . import product_configurator
diff --git a/product_configurator/wizard/product_configurator.py b/product_configurator/wizard/product_configurator.py
new file mode 100644
index 0000000000..882dad7eb7
--- /dev/null
+++ b/product_configurator/wizard/product_configurator.py
@@ -0,0 +1,1061 @@
+from lxml import etree
+
+from odoo import _, api, fields, models, tools
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools import frozendict
+
+from odoo.addons.base.models.ir_model import FIELD_TYPES
+from odoo.addons.base.models.ir_ui_view import (
+    transfer_field_to_modifiers,
+    transfer_modifiers_to_node,
+    transfer_node_to_modifiers,
+)
+
+
+class FreeSelection(fields.Selection):
+    def convert_to_cache(self, value, record, validate=True):
+        return super(FreeSelection, self).convert_to_cache(
+            value=value, record=record, validate=False
+        )
+
+
+class ProductConfigurator(models.TransientModel):
+    _name = "product.configurator"
+    _inherits = {"product.config.session": "config_session_id"}
+    _description = "Product configuration Wizard"
+
+    @property
+    def _prefixes(self):
+        """Return a dictionary with all dynamic field prefixes used to generate
+        fields in the wizard. Any module extending this functionality should
+        override this method to add all extra prefixes"""
+        return {
+            "field_prefix": "__attribute_",
+            "custom_field_prefix": "__custom_",
+        }
+
+    # TODO: Remove _prefix suffix as this is implied by the class property name
+
+    @api.model
+    def _remove_dynamic_fields(self, fields):
+        """Remove elements from the fields dictionary/list that begin with any
+        prefix from the _prefixes property
+            :param fields: list or dict of the form [fn1, fn2] / {fn1: val}
+        """
+
+        prefixes = self._prefixes.values()
+
+        field_type = type(fields)
+
+        if field_type == list:
+            static_fields = []
+        elif field_type == dict:
+            static_fields = {}
+
+        for field_name in fields:
+            if any(prefix in field_name for prefix in prefixes):
+                continue
+            if field_type == list:
+                static_fields.append(field_name)
+            elif field_type == dict:
+                static_fields[field_name] = fields[field_name]
+        return static_fields
+
+    @api.depends("product_tmpl_id", "value_ids", "custom_value_ids")
+    def _compute_cfg_image(self):
+        # TODO: Update when allowing custom values to influence image
+        for configurator in self:
+            cfg_sessions = configurator.config_session_id.with_context(bin_size=False)
+            image = cfg_sessions.get_config_image()
+            configurator.product_img = image
+
+    @api.depends("product_tmpl_id", "product_tmpl_id.attribute_line_ids")
+    def _compute_attr_lines(self):
+        """Use compute method instead of related due to increased flexibility
+        and strange behavior when attempting to have a related field point
+        to computed values"""
+        for configurator in self:
+            attribute_lines = configurator.product_tmpl_id.attribute_line_ids
+            configurator.attribute_line_ids = attribute_lines
+
+    # TODO: We could use a m2o instead of a monkeypatched select field but
+    # adding new steps should be trivial via custom development
+    def get_state_selection(self):
+        """Get the states of the wizard using standard values and optional
+        configuration steps set on the product.template via
+        config_step_line_ids"""
+
+        steps = [("select", "Select Template")]
+
+        # Get the wizard id from context set via action_next_step method
+        wizard_id = self.env.context.get("wizard_id")
+        wiz = self.browse(wizard_id).exists()
+
+        if not wiz:
+            return steps
+
+        open_lines = wiz.config_session_id.get_open_step_lines()
+
+        if open_lines:
+            open_steps = open_lines.mapped(lambda x: (str(x.id), x.config_step_id.name))
+            steps = open_steps if wiz.product_id else steps + open_steps
+        else:
+            steps.append(("configure", "Configure"))
+        return steps
+
+    @api.onchange("product_tmpl_id")
+    def onchange_product_tmpl(self):
+        """set the preset_id if exist in session"""
+        template = self.product_tmpl_id
+
+        self.config_step_ids = template.config_step_line_ids.mapped("config_step_id")
+
+        # Set product preset if exist in session
+        if template:
+            session = self.env["product.config.session"].search_session(
+                product_tmpl_id=template.id
+            )
+            self.product_preset_id = session.product_preset_id
+
+        if self.value_ids:
+            # TODO: Add confirmation button an delete cfg session
+            raise UserError(
+                _(
+                    "Changing the product template while having an active "
+                    "configuration will erase reset/clear all values"
+                )
+            )
+
+    def get_onchange_domains(
+        self,
+        values,
+        cfg_val_ids,
+        product_tmpl_id=False,
+        config_session_id=False,
+    ):
+        """Generate domains to be returned by onchange method in order
+        to restrict the availble values of dynamically inserted fields
+
+        :param values: values argument passed to onchance wrapper
+        :cfg_val_ids: current configuration passed as a list of value_ids
+        (usually in the form of db value_ids + interface value_ids)
+
+        :returns: a dictionary of domains returned by onchance method
+        """
+
+        field_prefix = self._prefixes.get("field_prefix")
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+        if not config_session_id:
+            config_session_id = self.config_session_id
+
+        domains = {}
+        check_avail_ids = cfg_val_ids[:]
+        for line in product_tmpl_id.attribute_line_ids.sorted():
+            field_name = field_prefix + str(line.attribute_id.id)
+
+            if field_name not in values:
+                continue
+
+            vals = values[field_name]
+
+            # get available values
+
+            avail_ids = config_session_id.values_available(
+                check_val_ids=line.value_ids.ids, value_ids=check_avail_ids
+            )
+            domains[field_name] = [("id", "in", avail_ids)]
+            check_avail_ids = list(
+                set(check_avail_ids) - (set(line.value_ids.ids) - set(avail_ids))
+            )
+            # Include custom value in the domain if attr line permits it
+            if line.custom:
+                custom_val = config_session_id.get_custom_value_id()
+                domains[field_name][0][2].append(custom_val.id)
+                if line.multi and vals and custom_val.id in vals[0][2]:
+                    continue
+        return domains
+
+    def get_onchange_vals(self, cfg_val_ids, config_session_id=None):
+        """Onchange hook to add / modify returned values by onchange method"""
+        if not config_session_id:
+            config_session_id = self.config_session_id
+
+        # Remove None from cfg_val_ids if exist
+        cfg_val_ids = [val for val in cfg_val_ids if val]
+
+        product_img = config_session_id.get_config_image(cfg_val_ids)
+        price = config_session_id.get_cfg_price(cfg_val_ids)
+        weight = config_session_id.get_cfg_weight(value_ids=cfg_val_ids)
+
+        return {
+            "product_img": product_img,
+            "value_ids": [(6, 0, cfg_val_ids)],
+            "weight": weight,
+            "price": price,
+        }
+
+    def get_form_vals(
+        self,
+        dynamic_fields,
+        domains,
+        cfg_val_ids=None,
+        product_tmpl_id=None,
+        config_session_id=None,
+    ):
+        """Generate a dictionary to return new values via onchange method.
+        Domains hold the values available, this method enforces these values
+        if a selection exists in the view that is not available anymore.
+
+        :param dynamic_fields: Dictionary with the current {dynamic_field: val}
+        :param domains: Odoo domains restricting attribute values
+
+        :returns vals: Dictionary passed to {'value': vals} by onchange method
+        """
+        vals = {}
+        dynamic_fields = {k: v for k, v in dynamic_fields.items() if v}
+        for k, v in dynamic_fields.items():
+            if not v:
+                continue
+            available_val_ids = domains[k][0][2]
+            if isinstance(v, list):
+                if any(type(el) != int for el in v):
+                    v = v[0][2]
+                value_ids = list(set(v) & set(available_val_ids))
+                dynamic_fields.update({k: value_ids})
+                vals[k] = [[6, 0, value_ids]]
+            elif v not in available_val_ids:
+                dynamic_fields.update({k: None})
+                vals[k] = None
+            else:
+                vals[k] = v
+
+        final_cfg_val_ids = list(dynamic_fields.values())
+
+        vals.update(self.get_onchange_vals(final_cfg_val_ids, config_session_id))
+        # To solve the Multi selection problem removing extra []
+        if "value_ids" in vals:
+            val_ids = vals["value_ids"][0]
+            vals["value_ids"] = [[val_ids[0], val_ids[1], tools.flatten(val_ids[2])]]
+
+        return vals
+
+    def apply_onchange_values(self, values, field_name, field_onchange):
+        """Called from web-controller
+        - original onchage return M2o values in formate
+        (attr-value.id, attr-value.name) but on website
+        we need only attr-value.id"""
+        product_tmpl_id = self.env["product.template"].browse(
+            values.get("product_tmpl_id", [])
+        )
+        if not product_tmpl_id:
+            product_tmpl_id = self.product_tmpl_id
+
+        config_session_id = self.env["product.config.session"].browse(
+            values.get("config_session_id", [])
+        )
+        if not config_session_id:
+            config_session_id = self.config_session_id
+
+        state = values.get("state", False)
+        if not state:
+            state = self.state
+
+        cfg_vals = self.env["product.attribute.value"]
+        if values.get("value_ids", []):
+            cfg_vals = self.env["product.attribute.value"].browse(
+                values.get("value_ids", [])[0][2]
+            )
+        if not cfg_vals:
+            cfg_vals = self.value_ids
+
+        field_type = type(field_name)
+
+        field_prefix = self._prefixes.get("field_prefix")
+        custom_field_prefix = self._prefixes.get("custom_field_prefix")
+        if field_type == list or (
+            not field_name.startswith(field_prefix)
+            and not field_name.startswith(custom_field_prefix)
+        ):
+            values = self._remove_dynamic_fields(values)
+            res = super(ProductConfigurator, self).onchange(
+                values, field_name, field_onchange
+            )
+            return res
+
+        view_val_ids = set()
+        view_attribute_ids = set()
+
+        try:
+            cfg_step_id = int(state)
+            cfg_step = product_tmpl_id.config_step_line_ids.filtered(
+                lambda x: x.id == cfg_step_id
+            )
+        except Exception:
+            cfg_step = self.env["product.config.step.line"]
+
+        dynamic_fields = {k: v for k, v in values.items() if k.startswith(field_prefix)}
+
+        # Get the unstored values from the client view
+        for k, v in dynamic_fields.items():
+            attr_id = int(k.split(field_prefix)[1])
+            # if isinstance(v, list):
+            #    dynamic_fields[k] = v[0][2]
+            line_attributes = cfg_step.attribute_line_ids.mapped("attribute_id")
+            if not cfg_step or attr_id in line_attributes.ids:
+                view_attribute_ids.add(attr_id)
+            else:
+                continue
+            if not v:
+                continue
+            if isinstance(v, list):
+                view_val_ids |= set(v[0][2])
+            elif isinstance(v, int):
+                view_val_ids.add(v)
+
+        # Clear all DB values belonging to attributes changed in the wizard
+        cfg_vals = cfg_vals.filtered(
+            lambda v: v.attribute_id.id not in view_attribute_ids
+        )
+
+        # Combine database values with wizard values_available
+        cfg_val_ids = cfg_vals.ids + list(view_val_ids)
+
+        domains = self.get_onchange_domains(
+            values, cfg_val_ids, product_tmpl_id, config_session_id
+        )
+        vals = self.get_form_vals(
+            dynamic_fields=dynamic_fields,
+            domains=domains,
+            product_tmpl_id=product_tmpl_id,
+            config_session_id=config_session_id,
+        )
+
+        return {"value": vals, "domain": domains}
+
+    def onchange(self, values, field_name, field_onchange):
+        """Override the onchange wrapper to return domains to dynamic
+        fields as onchange isn't triggered for non-db fields
+        """
+        onchange_values = self.apply_onchange_values(
+            values=values, field_name=field_name, field_onchange=field_onchange
+        )
+        field_prefix = self._prefixes.get("field_prefix")
+        vals = onchange_values.get("value", {})
+        for key, val in vals.items():
+            if isinstance(val, int) and key.startswith(field_prefix):
+                att_val = self.env["product.attribute.value"].browse(val)
+                vals[key] = (att_val.id, att_val.name)
+        return onchange_values
+
+    config_session_id = fields.Many2one(
+        required=True,
+        ondelete="cascade",
+        comodel_name="product.config.session",
+        string="Configuration Session",
+    )
+    attribute_line_ids = fields.One2many(
+        comodel_name="product.template.attribute.line",
+        compute="_compute_attr_lines",
+        string="Attributes",
+        readonly=True,
+        store=False,
+    )
+    config_step_ids = fields.Many2many(
+        comodel_name="product.config.step",
+        relation="product_config_config_steps_rel",
+        column1="config_wiz_id",
+        column2="config_step_id",
+        string="Configuration Steps",
+        readonly=True,
+        store=False,
+    )
+    product_id = fields.Many2one(
+        comodel_name="product.product",
+        readonly=True,
+        string="Product Variant",
+        help="Set only when re-configuring a existing variant",
+    )
+    product_img = fields.Binary(compute="_compute_cfg_image", readonly=True)
+    state = FreeSelection(
+        selection="get_state_selection", default="select", string="State"
+    )
+
+    @api.onchange("state")
+    def _onchange_state(self):
+        """Save values when change state of wizard by clicking on statusbar"""
+        if self.env.context.get("allow_preset_selection"):
+            self = self.with_context(allow_preset_selection=False)
+        if self.config_session_id:
+            self.config_session_id._origin.write(
+                {
+                    "value_ids": [[6, 0, self.value_ids.ids]],
+                    "config_step": self.state,
+                }
+            )
+
+    @api.onchange("product_preset_id")
+    def _onchange_product_preset(self):
+        """Set value ids as from product preset"""
+        pta_value_ids = self.product_preset_id.product_template_attribute_value_ids
+        attr_value_ids = pta_value_ids.mapped("product_attribute_value_id")
+        self.value_ids = attr_value_ids
+
+    @api.model
+    def get_field_default_attrs(self):
+
+        return {
+            "company_dependent": False,
+            "depends": (),
+            "groups": False,
+            "readonly": False,
+            "manual": False,
+            "required": False,
+            "searchable": False,
+            "store": False,
+            "translate": False,
+        }
+
+    @api.model
+    def fields_get(self, allfields=None, write_access=True, attributes=None):
+        """Artificially inject fields which are dynamically created using the
+        attribute_ids on the product.template as reference"""
+
+        field_prefix = self._prefixes.get("field_prefix")
+        custom_field_prefix = self._prefixes.get("custom_field_prefix")
+
+        res = super(ProductConfigurator, self).fields_get(
+            allfields=allfields, attributes=attributes
+        )
+
+        wizard_id = self.env.context.get("wizard_id")
+
+        # If wizard_id is not defined in the context then the wizard was just
+        # launched and is not stored in the database yet
+        if not wizard_id:
+            return res
+
+        # Get the wizard object from the database
+        wiz = self.browse(wizard_id)
+        active_step_id = wiz.state
+
+        # If the product template is not set it is still at the 1st step
+        if not wiz.product_tmpl_id:
+            return res
+
+        cfg_step_lines = wiz.product_tmpl_id.config_step_line_ids
+
+        try:
+            # Get only the attribute lines for the next step if defined
+            active_step_line = cfg_step_lines.filtered(
+                lambda l: l.id == int(active_step_id)
+            )
+            if active_step_line:
+                attribute_lines = active_step_line.attribute_line_ids
+            else:
+                attribute_lines = wiz.product_tmpl_id.attribute_line_ids
+        except Exception:
+            # If no configuration steps exist then get all attribute lines
+            attribute_lines = wiz.product_tmpl_id.attribute_line_ids
+
+        attribute_lines = wiz.product_tmpl_id.attribute_line_ids
+
+        # Generate relational fields with domains restricting values to
+        # the corresponding attributes
+
+        # Default field attributes
+        default_attrs = self.get_field_default_attrs()
+
+        for line in attribute_lines:
+            attribute = line.attribute_id
+            value_ids = line.value_ids.ids
+
+            value_ids = wiz.config_session_id.values_available(check_val_ids=value_ids)
+
+            # If attribute lines allows custom values add the
+            # generic "Custom" attribute.value to the list of options
+            if line.custom:
+                config_session_obj = self.env["product.config.session"]
+                custom_val = config_session_obj.get_custom_value_id()
+                value_ids.append(custom_val.id)
+
+                # Set default field type
+                field_type = "char"
+
+                if attribute.custom_type:
+                    field_types = FIELD_TYPES
+                    custom_type = line.attribute_id.custom_type
+                    # TODO: Rename int to integer in values
+                    if custom_type == "integer":
+                        field_type = "integer"
+                    elif custom_type in [f[0] for f in field_types]:
+                        field_type = custom_type
+
+                # TODO: Implement custom string on custom attribute
+                res[custom_field_prefix + str(attribute.id)] = dict(
+                    default_attrs,
+                    string="Custom",
+                    type=field_type,
+                    sequence=line.sequence,
+                )
+
+            # Add the dynamic field to the result set using the convention
+            # "__attribute_DBID" to later identify and extract it
+            res[field_prefix + str(attribute.id)] = dict(
+                default_attrs,
+                type="many2many" if line.multi else "many2one",
+                domain=[("id", "in", value_ids)],
+                string=line.attribute_id.name,
+                relation="product.attribute.value",
+                # sequence=line.sequence,
+            )
+        return res
+
+    @api.model
+    def get_view(self, view_id=None, view_type="form", **options):
+        """Generate view dynamically using attributes stored on the
+        product.template"""
+
+        if view_type == "form" and not view_id:
+            view_ext_id = "product_configurator.product_configurator_form"
+            view_id = self.env.ref(view_ext_id).id
+        res = super(ProductConfigurator, self).get_view(
+            view_id=view_id, view_type=view_type, **options
+        )
+
+        wizard_id = self.env.context.get("wizard_id")
+
+        wizard_model = res["model"]
+        if not wizard_id or not res["models"].get(wizard_model):
+            return res
+
+        wiz = self.browse(wizard_id)
+
+        # Get updated fields including the dynamic ones
+        fields = self.fields_get()
+
+        # Include all dynamic fields in the view
+        dynamic_field_prefixes = tuple(self._prefixes.values())
+
+        dynamic_fields = {
+            k: v for k, v in fields.items() if k.startswith(dynamic_field_prefixes)
+        }
+        models = dict(res["models"])
+        models[wizard_model] = models[wizard_model] + tuple(dynamic_fields.keys())
+        res["models"] = frozendict(models)
+
+        mod_view = self.add_dynamic_fields(res, dynamic_fields, wiz)
+
+        # Update result dict from super with modified view
+        res.update({"arch": etree.tostring(mod_view)})
+        return res
+
+    @api.model
+    def setup_modifiers(self, node, field=None):
+        """Processes node attributes and field descriptors to generate
+        the ``modifiers`` node attribute and set it on the provided node.
+
+        Alters its first argument in-place.
+
+        :param node: ``field`` node from an OpenERP view
+        :type node: lxml.etree._Element
+        :param dict field: field descriptor corresponding to the provided node
+        :param dict context: execution context used to evaluate node attributes
+        :param bool current_node_path: triggers the ``column_invisible`` code
+                                  path (separate from ``invisible``): in
+                                  tree view there are two levels of
+                                  invisibility, cell content (a column is
+                                  present but the cell itself is not
+                                  displayed) with ``invisible`` and column
+                                  invisibility (the whole column is
+                                  hidden) with ``column_invisible``.
+        :returns: nothing
+        """
+        modifiers = {}
+        if field is not None:
+            transfer_field_to_modifiers(field=field, modifiers=modifiers)
+        transfer_node_to_modifiers(node=node, modifiers=modifiers)
+        transfer_modifiers_to_node(modifiers=modifiers, node=node)
+
+    def prepare_attrs_initial(
+        self, attr_lines, field_prefix, custom_field_prefix, dynamic_fields, wiz
+    ):
+        cfg_step_ids = []
+        for attr_line in attr_lines:
+
+            attribute_id = attr_line.attribute_id.id
+            field_name = field_prefix + str(attribute_id)
+            custom_field = custom_field_prefix + str(attribute_id)
+
+            # Check if the attribute line has been added to the db fields
+            if field_name not in dynamic_fields:
+                continue
+
+            config_steps = wiz.product_tmpl_id.config_step_line_ids.filtered(
+                lambda x: attr_line in x.attribute_line_ids
+            )
+
+            # attrs property for dynamic fields
+            attrs = {"readonly": [], "required": [], "invisible": []}
+
+            if config_steps:
+                cfg_step_ids = [str(id) for id in config_steps.ids]
+                attrs["invisible"].append(("state", "not in", cfg_step_ids))
+                attrs["readonly"].append(("state", "not in", cfg_step_ids))
+
+                # If attribute is required make it so only in the proper step
+                if attr_line.required:
+                    attrs["required"].append(("state", "in", cfg_step_ids))
+            else:
+                attrs["invisible"].append(("state", "not in", ["configure"]))
+                attrs["readonly"].append(("state", "not in", ["configure"]))
+
+                # If attribute is required make it so only in the proper step
+                if attr_line.required:
+                    attrs["required"].append(("state", "in", ["configure"]))
+
+            if attr_line.custom:
+                pass
+                # TODO: Implement restrictions for ranges
+
+            config_lines = wiz.product_tmpl_id.config_line_ids
+            dependencies = config_lines.filtered(
+                lambda cl: cl.attribute_line_id == attr_line
+            )
+
+            # If an attribute field depends on another field from the same
+            # configuration step then we must use attrs to enable/disable the
+            # required and readonly depending on the value entered in the
+            # dependee
+
+            if attr_line.value_ids <= dependencies.mapped("value_ids"):
+                attr_depends = {}
+                domain_lines = dependencies.mapped("domain_id.domain_line_ids")
+                for domain_line in domain_lines:
+                    attr_id = domain_line.attribute_id.id
+                    attr_field = field_prefix + str(attr_id)
+                    attr_lines = wiz.product_tmpl_id.attribute_line_ids
+                    # If the fields it depends on are not in the config step
+                    # allow to update attrs for all attribute.\ otherwise
+                    # required will not work with stepchange using statusbar.
+                    # if config_steps and wiz.state not in cfg_step_ids:
+                    #     continue
+                    if attr_field not in attr_depends:
+                        attr_depends[attr_field] = set()
+                    if domain_line.condition == "in":
+                        attr_depends[attr_field] |= set(domain_line.value_ids.ids)
+                    elif domain_line.condition == "not in":
+                        val_ids = attr_lines.filtered(
+                            lambda l: l.attribute_id.id == attr_id
+                        ).value_ids
+                        val_ids = val_ids - domain_line.value_ids
+                        attr_depends[attr_field] |= set(val_ids.ids)
+
+                for dependee_field, val_ids in attr_depends.items():
+                    if not val_ids:
+                        continue
+                    if not attr_line.custom:
+                        attrs["readonly"].append(
+                            (dependee_field, "not in", list(val_ids))
+                        )
+
+                    if attr_line.required and not attr_line.custom:
+                        attrs["required"].append((dependee_field, "in", list(val_ids)))
+        return attrs, field_name, custom_field, config_steps, cfg_step_ids
+
+    @api.model
+    def add_dynamic_fields(self, res, dynamic_fields, wiz):
+        """Create the configuration view using the dynamically generated
+        fields in fields_get()
+        """
+
+        field_prefix = self._prefixes.get("field_prefix")
+        custom_field_prefix = self._prefixes.get("custom_field_prefix")
+
+        try:
+            # Search for view container hook and add dynamic view and fields
+            xml_view = etree.fromstring(res["arch"])
+            xml_static_form = xml_view.xpath("//group[@name='static_form']")[0]
+            xml_dynamic_form = etree.Element("group", colspan="2", name="dynamic_form")
+            xml_parent = xml_static_form.getparent()
+            xml_parent.insert(xml_parent.index(xml_static_form) + 1, xml_dynamic_form)
+            xml_dynamic_form = xml_view.xpath("//group[@name='dynamic_form']")[0]
+        except Exception as exc:
+            raise UserError(
+                _("There was a problem rendering the view " "(dynamic_form not found)")
+            ) from exc
+
+        # Get all dynamic fields inserted via fields_get method
+        attr_lines = wiz.product_tmpl_id.attribute_line_ids.sorted()
+
+        # Loop over the dynamic fields and add them to the view one by one
+        for attr_line in attr_lines:
+            (
+                attrs,
+                field_name,
+                custom_field,
+                config_steps,
+                cfg_step_ids,
+            ) = self.prepare_attrs_initial(
+                attr_line, field_prefix, custom_field_prefix, dynamic_fields, wiz
+            )
+
+            if len(attrs["readonly"]) > 1 and attrs["readonly"][0] != "|":
+                attrs["readonly"].insert(0, "|")
+            if len(attrs["invisible"]) > 1 and attrs["invisible"][0] != "|":
+                attrs["invisible"].insert(0, "|")
+
+            # Create the new field in the view
+            node = etree.Element(
+                "field",
+                name=field_name,
+                on_change="1",
+                default_focus="1" if attr_line == attr_lines[0] else "0",
+                attrs=str(attrs),
+                context=str(
+                    {
+                        "show_attribute": False,
+                        "show_price_extra": True,
+                        "active_id": wiz.product_tmpl_id.id,
+                    }
+                ),
+                options=str(
+                    {
+                        "no_create": True,
+                        "no_create_edit": True,
+                        "no_open": True,
+                    }
+                ),
+            )
+
+            field_type = dynamic_fields[field_name].get("type")
+            if field_type == "many2many":
+                node.attrib["widget"] = "many2many_tags"
+            # Apply the modifiers (attrs) on the newly inserted field in the
+            # arch and add it to the view
+            self.setup_modifiers(node)
+            xml_dynamic_form.append(node)
+
+            if attr_line.custom and custom_field in dynamic_fields:
+                widget = ""
+                config_session_obj = self.env["product.config.session"]
+                custom_option_id = config_session_obj.get_custom_value_id().id
+
+                if field_type == "many2many":
+                    field_val = [(6, False, [custom_option_id])]
+                else:
+                    field_val = custom_option_id
+
+                attrs["readonly"] += [(field_name, "!=", field_val)]
+                attrs["invisible"] += [(field_name, "!=", field_val)]
+                attrs["required"] += [(field_name, "=", field_val)]
+
+                if config_steps:
+                    attrs["required"] += [("state", "in", cfg_step_ids)]
+
+                # TODO: Add a field2widget mapper
+                if attr_line.attribute_id.custom_type == "color":
+                    widget = "color"
+
+                if len(attrs["invisible"]) > 1 and attrs["invisible"][0] != "|":
+                    attrs["invisible"].insert(0, "|")
+                if len(attrs["readonly"]) > 1 and attrs["readonly"][0] != "|":
+                    attrs["readonly"].insert(0, "|")
+
+                node = etree.Element(
+                    "field", name=custom_field, attrs=str(attrs), widget=widget
+                )
+                self.setup_modifiers(node)
+                xml_dynamic_form.append(node)
+        return xml_view
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        """Sets the configuration values of the product_id if given (if any).
+        This is used in reconfiguration of a existing variant"""
+        for vals in vals_list:
+            if "product_id" in vals:
+                product = self.env["product.product"].browse(vals["product_id"])
+                pta_value_ids = product.product_template_attribute_value_ids
+                attr_value_ids = pta_value_ids.mapped("product_attribute_value_id")
+                vals.update(
+                    {
+                        "product_tmpl_id": product.product_tmpl_id.id,
+                        "value_ids": [(6, 0, attr_value_ids.ids)],
+                    }
+                )
+
+            # Get existing session for this product_template or create a new one
+            session = self.env["product.config.session"].create_get_session(
+                product_tmpl_id=int(vals.get("product_tmpl_id"))
+            )
+            vals.update({"user_id": self.env.uid, "config_session_id": session.id})
+            wz_value_ids = vals.get("value_ids", [])
+            if session.value_ids and (
+                (wz_value_ids and not wz_value_ids[0][2]) or not wz_value_ids
+            ):
+                vals.update({"value_ids": [(6, 0, session.value_ids.ids)]})
+        return super(ProductConfigurator, self).create(vals_list)
+
+    def read(self, fields=None, load="_classic_read"):
+        """Remove dynamic fields from the fields list and update the
+        returned values with the dynamic data stored in value_ids"""
+
+        field_prefix = self._prefixes.get("field_prefix")
+        custom_field_prefix = self._prefixes.get("custom_field_prefix")
+
+        attr_vals = [f for f in fields if f.startswith(field_prefix)]
+        custom_attr_vals = [f for f in fields if f.startswith(custom_field_prefix)]
+
+        dynamic_fields = attr_vals + custom_attr_vals
+        fields = self._remove_dynamic_fields(fields)
+
+        custom_val = self.env["product.config.session"].get_custom_value_id()
+        dynamic_vals = {}
+
+        res = super(ProductConfigurator, self).read(fields=fields, load=load)
+
+        if not dynamic_fields:
+            return res
+
+        for attr_line in self.product_tmpl_id.attribute_line_ids:
+            attr_id = attr_line.attribute_id.id
+            field_name = field_prefix + str(attr_id)
+
+            if field_name not in dynamic_fields:
+                continue
+
+            custom_field_name = custom_field_prefix + str(attr_id)
+
+            # Handle default values for dynamic fields on Odoo frontend
+            res[0].update({field_name: False, custom_field_name: False})
+
+            custom_vals = self.custom_value_ids.filtered(
+                lambda x: x.attribute_id.id == attr_id
+            ).with_context(show_attribute=False)
+            vals = attr_line.value_ids.filtered(
+                lambda v: v in self.value_ids
+            ).with_context(
+                show_attribute=False,
+                show_price_extra=True,
+                active_id=self.product_tmpl_id.id,
+            )
+
+            if not attr_line.custom and not vals:
+                continue
+
+            if attr_line.custom and custom_vals:
+                custom_field_val = custom_val.id
+                if load == "_classic_read":
+                    custom_field_val = custom_val.name_get()[0]
+                dynamic_vals.update(
+                    {
+                        field_name: custom_field_val,
+                        custom_field_name: custom_vals.eval(),
+                    }
+                )
+            elif attr_line.multi:
+                dynamic_vals = {field_name: vals.ids}
+            else:
+                try:
+                    vals.ensure_one()
+                    field_value = vals.id
+                    if load == "_classic_read":
+                        field_value = vals.name_get()[0]
+                    dynamic_vals = {field_name: field_value}
+                except Exception:
+                    continue
+            res[0].update(dynamic_vals)
+        return res
+
+    def write(self, vals):
+        """Prevent database storage of dynamic fields and instead write values
+        to database persistent value_ids field"""
+
+        # Remove all dynamic fields from write values
+        self.config_session_id.update_session_configuration_value(
+            vals=vals, product_tmpl_id=self.product_tmpl_id
+        )
+        vals = self._remove_dynamic_fields(vals)
+
+        return super(ProductConfigurator, self).write(vals)
+
+    def action_next_step(self):
+        """Proceeds to the next step of the configuration process. This usually
+        implies the next configuration step (if any) defined via the
+        config_step_line_ids on the product.template.
+
+        More importantly it sets metadata on the context
+        variable so the fields_get and fields_view_get methods can generate the
+        appropriate dynamic content"""
+        wizard_action = self.with_context(
+            allow_preset_selection=False
+        ).get_wizard_action(wizard=self)
+
+        if not self.product_tmpl_id:
+            return wizard_action
+
+        if not self.product_tmpl_id.attribute_line_ids:
+            raise ValidationError(
+                _("Product Template does not have any attribute lines defined")
+            )
+        next_step = self.config_session_id.get_next_step(
+            state=self.state,
+            product_tmpl_id=self.product_tmpl_id,
+            value_ids=self.config_session_id.value_ids,
+            custom_value_ids=self.config_session_id.custom_value_ids,
+        )
+        if not next_step:
+            return self.action_config_done()
+        return self.open_step(step=next_step)
+
+    def action_previous_step(self):
+        """Proceeds to the next step of the configuration process. This usually
+        implies the next configuration step (if any) defined via the
+        config_step_line_ids on the product.template."""
+        wizard_action = self.with_context(
+            wizard_id=self.id, view_cache=False, allow_preset_selection=False
+        ).get_wizard_action(wizard=self)
+
+        cfg_step_lines = self.product_tmpl_id.config_step_line_ids
+        if not cfg_step_lines:
+            self.state = "select"
+            return wizard_action
+
+        try:
+            cfg_step_line_id = int(self.state)
+            active_cfg_line_id = cfg_step_lines.filtered(
+                lambda x: x.id == cfg_step_line_id
+            ).id
+        except Exception:
+            active_cfg_line_id = None
+
+        adjacent_steps = self.config_session_id.get_adjacent_steps(
+            active_step_line_id=active_cfg_line_id
+        )
+        previous_step = adjacent_steps.get("previous_step")
+        if previous_step:
+            self.state = str(previous_step.id)
+        else:
+            self.state = "select"
+        self.config_session_id.config_step = self.state
+        return wizard_action
+
+    def action_reset(self):
+        """Delete wizard and configuration session then create
+        a new wizard+session and return an action for the new wizard object"""
+        try:
+            session = self.config_session_id
+            # Field parent_id(of session) defind in
+            # product_configurator_subconfig, so this code should
+            # be moved in product_configurator_subconfig
+            # while session.parent_id:
+            #     session = session.parent_id
+            session_product_tmpl_id = session.product_tmpl_id
+            session.unlink()
+        except Exception:
+            session = self.env["product.config.step"]
+
+        action = self.with_context(
+            wizard_id=None,
+            allow_preset_selection=False,
+            default_product_tmpl_id=session_product_tmpl_id.id,
+        ).get_wizard_action()
+        return action
+
+    def get_wizard_action(self, view_cache=False, wizard=None):
+        """Return action of wizard
+        :param view_cache: Boolean (True/False)
+        :param wizard: recordset of product.configurator
+        :returns : dictionary
+        """
+        ctx = self.env.context.copy()
+        ctx.update(
+            {
+                "view_cache": view_cache,
+                "differentiator": ctx.get("differentiator", 1) + 1,
+            }
+        )
+        if wizard:
+            ctx.update({"wizard_id": wizard.id})
+        wizard_action = {
+            "type": "ir.actions.act_window",
+            "res_model": self._name,
+            "name": "Configure Product",
+            "views": [
+                [
+                    self.env.ref("product_configurator.product_configurator_form").id,
+                    "form",
+                ]
+            ],
+            "view_mode": "form",
+            "context": ctx,
+            "target": "new",
+        }
+        if wizard:
+            wizard_action.update({"res_id": wizard.id})
+        return wizard_action
+
+    def open_step(self, step):
+        """Open wizard step 'step'
+        :param step: recordset of product.config.step.line
+        """
+        wizard_action = self.with_context(
+            allow_preset_selection=False
+        ).get_wizard_action(wizard=self)
+        if not step:
+            return wizard_action
+        if isinstance(step, type(self.env["product.config.step.line"])):
+            step = "%s" % (step.id)
+        self.state = step
+        self.config_session_id.config_step = step
+        return wizard_action
+
+    def action_config_done(self):
+        """This method is for the final step which will be taken care by a
+        separate module"""
+        # This try except is too generic.
+        # The create_variant routine could effectively fail for
+        # a large number of reasons, including bad programming.
+        # It should be refactored.
+        # In the meantime, at least make sure that a validation
+        # error legitimately raised in a nested routine
+        # is passed through.
+        step_to_open = self.config_session_id.check_and_open_incomplete_step()
+        if step_to_open:
+            return self.open_step(step_to_open)
+        self.config_session_id.action_confirm()
+        variant = self.config_session_id.product_id
+        action = {
+            "type": "ir.actions.act_window",
+            "res_model": "product.product",
+            "name": "Product Variant",
+            "view_mode": "form",
+            "context": dict(self.env.context, custom_create_variant=True),
+            "res_id": variant.id,
+        }
+        return action
+
+
+# class ProductConfiguratorCustomValue(models.TransientModel):
+#     _name = "product.configurator.custom.value"
+#     _description = "Product Configurator Custom Value"
+
+#     attachment_ids = fields.Many2many(
+#         comodel_name="ir.attachment",
+#         column1="config_attachment",
+#         column2="attachment_id",
+#         string="Attachments",
+#     )
+#     attribute_id = fields.Many2one(
+#         string="Attribute", comodel_name="product.attribute", required=True
+#     )
+#     user_id = fields.Many2one(
+#         string="User",
+#         comodel_name="res.users",
+#         related="wizard_id.create_uid",
+#         required=True,
+#     )
+#     value = fields.Char(string="Value")
+#     wizard_id = fields.Many2one(comodel_name="product.configurator", string="Wizard")
+# TODO: Current value ids to save frontend/backend session?
diff --git a/product_configurator/wizard/product_configurator_view.xml b/product_configurator/wizard/product_configurator_view.xml
new file mode 100644
index 0000000000..6114d0e83c
--- /dev/null
+++ b/product_configurator/wizard/product_configurator_view.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+
+    <record id="product_configurator_form" model="ir.ui.view">
+        <field name="name">product.configurator</field>
+        <field name="model">product.configurator</field>
+        <field name="arch" type="xml">
+            <form>
+                <header>
+                    <field name="state" widget="statusbar" clickable="True" />
+                    <button
+                        string="Reset"
+                        type="object"
+                        name="action_reset"
+                        confirm="Are you sure? This will remove your current configuration for this template!"
+                        special="no_save"
+                        class="oe_highlight"
+                        attrs="{'invisible': ['|', ('product_tmpl_id', '=', False), ('state', '=', 'select')]}"
+                    />
+                </header>
+                <sheet>
+                    <field
+                        attrs="{'invisible': [('product_img', '=', False)]}"
+                        name="product_img"
+                        readonly="1"
+                        nolabel="1"
+                        widget="image"
+                    />
+                    <group col="3">
+                        <group name='static_form' states='select' colspan="2">
+                            <field
+                                name="config_session_id"
+                                required="context.get('wizard_id')"
+                                invisible="1"
+                            />
+                            <field name="product_id" invisible="1" />
+                            <field name="currency_id" invisible="1" />
+                            <field
+                                name="product_tmpl_id"
+                                readonly="context.get('product_tmpl_id_readonly', False)"
+                                required="True"
+                                options="{'no_create': True}"
+                                force_save="1"
+                            />
+                            <field
+                                name="product_preset_id"
+                                invisible="not context.get('allow_preset_selection')"
+                                options="{'no_create': True}"
+                            />
+                            <separator
+                                string="To reset/change the Preset Please close and start the configuration again"
+                                invisible="context.get('allow_preset_selection')"
+                                colspan="2"
+                            />
+                            <field
+                                attrs="{'invisible': [('attribute_line_ids', '=', [])]}"
+                                name="attribute_line_ids"
+                            >
+                                <tree>
+                                    <field name="attribute_id" />
+                                    <field name="custom" />
+                                    <field name="multi" />
+                                </tree>
+                            </field>
+                        </group>
+                        <group colspan="1">
+                            <field name="weight" />
+                            <field
+                                name="price"
+                                widget='monetary'
+                                options="{'currency_field': 'currency_id', 'field_digits': True}"
+                            />
+                        </group>
+                    </group>
+                </sheet>
+                <footer>
+                    <field name="value_ids" readonly="1" force_save="1" invisible="1" />
+                    <button
+                        type="object"
+                        name="action_previous_step"
+                        attrs="{'invisible': [('state','=','select')]}"
+                        string="Back"
+                    />
+                    <button
+                        type="object"
+                        name="action_next_step"
+                        class="oe_highlight"
+                        string="Next"
+                    />
+                </footer>
+            </form>
+        </field>
+    </record>
+
+</odoo>
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000..294bcaef52
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+# generated from manifests external_dependencies
+mako
diff --git a/setup/product_configurator/odoo/addons/product_configurator b/setup/product_configurator/odoo/addons/product_configurator
new file mode 120000
index 0000000000..b4a3cfff94
--- /dev/null
+++ b/setup/product_configurator/odoo/addons/product_configurator
@@ -0,0 +1,1 @@
+../../../../product_configurator
\ No newline at end of file
diff --git a/setup/product_configurator/setup.py b/setup/product_configurator/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/product_configurator/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+    setup_requires=['setuptools-odoo'],
+    odoo_addon=True,
+)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f1b2b9fd6c..e1af7438fa 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     name: Detect unreleased dependencies
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - run: |
           for reqfile in requirements.txt test-requirements.txt ; do
               if [ -f ${reqfile} ] ; then
@@ -36,10 +36,10 @@ jobs:
       matrix:
         include:
           - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest
-            makepot: "true"
             name: test with Odoo
           - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest
             name: test with OCB
+            makepot: "true"
     services:
       postgres:
         image: postgres:12.0
@@ -50,7 +50,7 @@ jobs:
         ports:
           - 5432:5432
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           persist-credentials: false
       - name: Install addons and dependencies
diff --git a/product_configurator/tests/__init__.py b/product_configurator/tests/__init__.py
new file mode 100644
index 0000000000..435b31b0a0
--- /dev/null
+++ b/product_configurator/tests/__init__.py
@@ -0,0 +1,9 @@
+from . import test_product_configurator_test_cases
+
+# from . import test_create
+# from . import test_configuration_rules
+from . import test_product
+
+# from . import test_product_attribute
+# from . import test_product_config
+# from . import test_wizard
diff --git a/product_configurator/tests/test_configuration_rules.py b/product_configurator/tests/test_configuration_rules.py
new file mode 100644
index 0000000000..8d51b3bf90
--- /dev/null
+++ b/product_configurator/tests/test_configuration_rules.py
@@ -0,0 +1,129 @@
+from odoo import SUPERUSER_ID
+from odoo.exceptions import ValidationError
+from odoo.tests.common import TransactionCase
+
+
+class ConfigurationRules(TransactionCase):
+    def setUp(self):
+        super(ConfigurationRules, self).setUp()
+
+        self.cfg_tmpl = self.env.ref("product_configurator.bmw_2_series")
+        self.cfg_session = self.env["product.config.session"].create(
+            {"product_tmpl_id": self.cfg_tmpl.id, "user_id": SUPERUSER_ID}
+        )
+
+        attribute_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids")
+        self.attr_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids")
+
+        self.attr_val_ext_ids = {
+            v: k for k, v in attribute_vals.get_external_id().items()
+        }
+
+    def get_attr_val_ids(self, ext_ids):
+        """Return a list of database ids using the external_ids
+        passed via ext_ids argument"""
+
+        value_ids = []
+
+        attr_val_prefix = "product_configurator.product_attribute_value_%s"
+
+        for ext_id in ext_ids:
+            if ext_id in self.attr_val_ext_ids:
+                value_ids.append(self.attr_val_ext_ids[ext_id])
+            elif attr_val_prefix % ext_id in self.attr_val_ext_ids:
+                value_ids.append(self.attr_val_ext_ids[attr_val_prefix % ext_id])
+
+        return value_ids
+
+    def test_valid_configuration(self):
+        """Test validation of a valid configuration"""
+
+        conf = [
+            "gasoline",
+            "228i",
+            "model_luxury_line",
+            "silver",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+
+        attr_val_ids = self.get_attr_val_ids(conf)
+        validation = self.cfg_session.validate_configuration(attr_val_ids)
+        self.assertTrue(validation, "Valid configuration failed validation")
+
+    def test_invalid_configuration(self):
+
+        conf = [
+            "diesel",
+            "228i",
+            "model_luxury_line",
+            "silver",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+
+        attr_val_ids = self.get_attr_val_ids(conf)
+        with self.assertRaises(ValidationError):
+            self.cfg_session.validate_configuration(attr_val_ids)
+
+    def test_missing_val_configuration(self):
+        conf = [
+            "diesel",
+            "228i",
+            "model_luxury_line",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+
+        attr_val_ids = self.get_attr_val_ids(conf)
+        with self.assertRaises(ValidationError):
+            self.cfg_session.validate_configuration(attr_val_ids)
+
+    def test_invalid_multi_configuration(self):
+        conf = [
+            "gasoline",
+            "228i",
+            "model_luxury_line",
+            "silver",
+            "red",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+
+        attr_val_ids = self.get_attr_val_ids(conf)
+        with self.assertRaises(ValidationError):
+            self.cfg_session.validate_configuration(attr_val_ids)
+
+    def test_invalid_custom_value_configuration(self):
+        conf = [
+            "gasoline",
+            "228i",
+            "model_luxury_line",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+
+        attr_color_id = self.env.ref("product_configurator.product_attribute_color")
+
+        custom_vals = {attr_color_id: {"value": "#fefefe"}}
+
+        attr_val_ids = self.get_attr_val_ids(conf)
+        with self.assertRaises(ValidationError):
+            self.cfg_session.validate_configuration(attr_val_ids, custom_vals)
+
+    # TODO: Test configuration with disallowed custom type value
diff --git a/product_configurator/tests/test_create.py b/product_configurator/tests/test_create.py
new file mode 100644
index 0000000000..dc97ee1c46
--- /dev/null
+++ b/product_configurator/tests/test_create.py
@@ -0,0 +1,189 @@
+from odoo.tests.common import TransactionCase
+
+
+class ConfigurationCreate(TransactionCase):
+    def setUp(self):
+        super(ConfigurationCreate, self).setUp()
+
+        self.ProductConfWizard = self.env["product.configurator"]
+        self.config_product = self.env.ref("product_configurator.bmw_2_series")
+        self.product_category = self.env.ref("product.product_category_5")
+
+        # attributes
+        self.attr_fuel = self.env.ref("product_configurator.product_attribute_fuel")
+        self.attr_engine = self.env.ref("product_configurator.product_attribute_engine")
+        self.attr_color = self.env.ref("product_configurator.product_attribute_color")
+        self.attr_rims = self.env.ref("product_configurator.product_attribute_rims")
+        self.attr_model_line = self.env.ref(
+            "product_configurator.product_attribute_model_line"
+        )
+        self.attr_tapistry = self.env.ref(
+            "product_configurator.product_attribute_tapistry"
+        )
+        self.attr_transmission = self.env.ref(
+            "product_configurator.product_attribute_transmission"
+        )
+        self.attr_options = self.env.ref(
+            "product_configurator.product_attribute_options"
+        )
+
+        # values
+        self.value_gasoline = self.env.ref(
+            "product_configurator.product_attribute_value_gasoline"
+        )
+        self.value_218i = self.env.ref(
+            "product_configurator.product_attribute_value_218i"
+        )
+        self.value_220i = self.env.ref(
+            "product_configurator.product_attribute_value_220i"
+        )
+        self.value_red = self.env.ref(
+            "product_configurator.product_attribute_value_red"
+        )
+        self.value_rims_378 = self.env.ref(
+            "product_configurator.product_attribute_value_rims_378"
+        )
+        self.value_sport_line = self.env.ref(
+            "product_configurator.product_attribute_value_sport_line"
+        )
+        self.value_model_sport_line = self.env.ref(
+            "product_configurator.product_attribute_value_model_sport_line"
+        )
+        self.value_tapistry = self.env.ref(
+            "product_configurator.product_attribute_value_tapistry" + "_oyster_black"
+        )
+        self.value_transmission = self.env.ref(
+            "product_configurator.product_attribute_value_steptronic"
+        )
+        self.value_options_1 = self.env.ref(
+            "product_configurator.product_attribute_value_smoker_package"
+        )
+        self.value_options_2 = self.env.ref(
+            "product_configurator.product_attribute_value_sunroof"
+        )
+
+    def test_01_create(self):
+        """Test configuration item does not make variations"""
+
+        attr_test = self.env["product.attribute"].create(
+            {
+                "name": "Test",
+                "value_ids": [
+                    (0, 0, {"name": "1"}),
+                    (0, 0, {"name": "2"}),
+                ],
+            }
+        )
+
+        test_template = self.env["product.template"].create(
+            {
+                "name": "Test Configuration",
+                "config_ok": True,
+                "type": "consu",
+                "categ_id": self.product_category.id,
+                "attribute_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "attribute_id": attr_test.id,
+                            "value_ids": [
+                                (6, 0, attr_test.value_ids.ids),
+                            ],
+                            "required": True,
+                        },
+                    ),
+                ],
+            }
+        )
+
+        self.assertEqual(
+            len(test_template.product_variant_ids),
+            0,
+            "Create should not have any variants",
+        )
+
+    def test_02_previous_step_incompatible_changes(self):
+        """Test changes in previous steps which would makes
+        values in next configuration steps invalid"""
+
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+                "__attribute_{}".format(self.attr_rims.id): self.value_rims_378.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_sport_line.id,
+            }
+        )
+        product_config_wizard.action_previous_step()
+        product_config_wizard.action_previous_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_engine.id): self.value_220i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_model_sport_line.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_tapistry.id): self.value_tapistry.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_transmission.id
+                ): self.value_transmission.id,
+                "__attribute_{}".format(self.attr_options.id): [
+                    [6, 0, [self.value_options_1.id, self.value_options_2.id]]
+                ],
+            }
+        )
+        product_config_wizard.action_next_step()
+        value_ids = (
+            self.value_gasoline
+            + self.value_220i
+            + self.value_red
+            + self.value_rims_378
+            + self.value_model_sport_line
+            + self.value_tapistry
+            + self.value_transmission
+            + self.value_options_1
+            + self.value_options_2
+        )
+        new_variant = self.config_product.product_variant_ids.filtered(
+            lambda variant: variant.attribute_value_ids == value_ids
+        )
+        self.assertNotEqual(
+            new_variant.id,
+            False,
+            "Variant not generated at the end of the configuration process",
+        )
diff --git a/product_configurator/tests/test_product.py b/product_configurator/tests/test_product.py
new file mode 100644
index 0000000000..608cf6c2e3
--- /dev/null
+++ b/product_configurator/tests/test_product.py
@@ -0,0 +1,705 @@
+from odoo.exceptions import ValidationError
+
+from ..tests.test_product_configurator_test_cases import ProductConfiguratorTestCases
+
+
+class TestProduct(ProductConfiguratorTestCases):
+    def setUp(self):
+        super(TestProduct, self).setUp()
+        self.productTemplate = self.env["product.template"]
+        self.productAttributeLine = self.env["product.template.attribute.line"]
+        self.productConfigStepLine = self.env["product.config.step.line"]
+        self.product_category = self.env.ref("product.product_category_5")
+        self.attributelinefuel = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_fuel"
+        )
+        self.attributelineengine = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_engine"
+        )
+        self.value_diesel = self.env.ref(
+            "product_configurator.product_attribute_value_diesel"
+        )
+        self.value_218d = self.env.ref(
+            "product_configurator.product_attribute_value_218d"
+        )
+        self.value_220d = self.env.ref(
+            "product_configurator.product_attribute_value_220d"
+        )
+        self.value_silver = self.env.ref(
+            "product_configurator.product_attribute_value_silver"
+        )
+        self.config_step_engine = self.env.ref(
+            "product_configurator.config_step_engine"
+        )
+        self.config_step_body = self.env.ref("product_configurator.config_step_body")
+        self.product_tmpl_id = self.env["product.template"].create(
+            {
+                "name": "Test Configuration",
+                "config_ok": True,
+                "type": "consu",
+                "categ_id": self.product_category.id,
+            }
+        )
+        # create attribute line 1
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_fuel.id,
+                "value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])],
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_engine.id,
+                "value_ids": [
+                    (
+                        6,
+                        0,
+                        [
+                            self.value_218i.id,
+                            self.value_220i.id,
+                            self.value_218d.id,
+                            self.value_220d.id,
+                        ],
+                    )
+                ],
+                "required": True,
+            }
+        )
+        # create attribute line 3
+        self.attributeLine3 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_color.id,
+                "value_ids": [(6, 0, [self.value_red.id, self.value_silver.id])],
+                "required": True,
+            }
+        )
+
+    def _get_product_id(self):
+        self._configure_product_nxt_step()
+        return self.config_product.product_variant_ids
+
+    def test_00__compute_template_attr_vals(self):
+        value_ids = self.product_tmpl_id.attribute_line_ids.mapped("value_ids")
+        self.product_tmpl_id._compute_template_attr_vals()
+        self.assertEqual(
+            value_ids,
+            self.product_tmpl_id.attribute_line_val_ids,
+            "Error: if value are different\
+            Method: _compute_template_attr_vals() ",
+        )
+
+    def test_01_set_weight(self):
+        self.product_tmpl_id.weight = 120
+        self.product_tmpl_id._set_weight()
+        self.assertEqual(
+            self.product_tmpl_id.weight,
+            self.product_tmpl_id.weight_dummy,
+            "Error: If set diffrent value for dummy_weight\
+            Method: _set_weight()",
+        )
+        self.product_tmpl_id.config_ok = False
+        set_weight = self.product_tmpl_id._set_weight()
+        self.assertIsNone(
+            set_weight,
+            "Error: If Value none\
+            Method: _set_weight()",
+        )
+
+    def test_02_compute_weight(self):
+        self.product_tmpl_id.weight_dummy = 50.0
+        self.product_tmpl_id._compute_weight()
+        self.assertEqual(
+            self.product_tmpl_id.weight_dummy,
+            self.product_tmpl_id.weight,
+            "Error: If set diffrent value for weight\
+            Method: _compute_weight()",
+        )
+
+    def test_03_toggle_config(self):
+        configFalse = self.product_tmpl_id.toggle_config()
+        self.assertFalse(
+            configFalse,
+            "Error: If Boolean False\
+            Method: toggle_config()",
+        )
+        self.product_tmpl_id.toggle_config()
+        varient_value = self.product_tmpl_id._create_variant_ids()
+        self.assertIsNone(
+            varient_value,
+            "Error: If its return none\
+            Method: create_variant_ids()",
+        )
+
+    def test_04_unlink(self):
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        config_session_id = self.env["product.config.session"].search(
+            [("product_tmpl_id", "=", self.product_tmpl_id.id)]
+        )
+        config_session_id.unlink()
+        varientId = self.product_tmpl_id.product_variant_ids
+        boolValue = varientId.unlink()
+        self.assertTrue(
+            boolValue,
+            "Error: if record are not unlink\
+            Method: unlink()",
+        )
+
+    def test_05_check_default_values(self):
+        self.attributelinefuel.default_val = (self.value_gasoline.id,)
+        self.attributelineengine.default_val = self.value_218d.id
+        with self.assertRaises(ValidationError):
+            self.config_product._check_default_values()
+
+    def test_06_configure_product(self):
+        # configure product
+        self.product_tmpl_id.configure_product()
+        self.ProductConfWizard.action_next_step()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        wizard_action = product_config_wizard.action_next_step()
+        varient_id = wizard_action.get("res_id")
+        self.assertEqual(
+            varient_id,
+            self.product_tmpl_id.product_variant_ids.id,
+            "Error: If get diffrent varient Id\
+            Method: action_next_step()",
+        )
+        product_config_wizard.action_previous_step()
+        self.assertEqual(
+            product_config_wizard.state,
+            "select",
+            "Error: If get diffrent State\
+            Method: action_previous_step()",
+        )
+        # create config_step_line 1
+        self.configStepLine1 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "config_step_id": self.config_step_engine.id,
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        # create config_step_line 2
+        self.configStepLine2 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "config_step_id": self.config_step_body.id,
+                "attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "config_step_line_ids": [
+                    (6, 0, [self.configStepLine1.id, self.configStepLine2.id])
+                ],
+            }
+        )
+
+        # configure product
+        self.product_tmpl_id.configure_product()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        product_config_wizard.action_previous_step()
+        self.assertEqual(
+            product_config_wizard.state,
+            str(self.configStepLine1.id),
+            "Error: If diffrent previous state and config state\
+            Method: action_previous_step()",
+        )
+        product_config_wizard.action_next_step()
+        self.assertEqual(
+            product_config_wizard.config_session_id.config_step,
+            product_config_wizard.state,
+            "Error: If diffrent state and config_step\
+            Method: action_previous_step()",
+        )
+        product_config_wizard.action_next_step()
+
+    def test_07_get_mako_tmpl_name(self):
+        # check for product_product
+        product_product = self._get_product_id()
+        mako_tmpl_vals = product_product._get_mako_tmpl_name()
+        self.assertEqual(
+            mako_tmpl_vals,
+            product_product.display_name,
+            "Error: If get display_name are different\
+            Method: _get_mako_tmpl_name()",
+        )
+        self.config_product.write({"mako_tmpl_name": "Test Configuration Product"})
+        mako_tmpl_vals = product_product._get_mako_tmpl_name()
+        self.assertEqual(
+            self.config_product.mako_tmpl_name,
+            mako_tmpl_vals,
+            "Error: If Mako Template are not exists or different\
+            Method: _get_mako_tmpl_name()",
+        )
+
+    def test_08_compute_product_weight(self):
+        product_product = self._get_product_id()
+        self.config_product.weight = 10
+        product_product.weight_extra = 20
+        product_product._compute_product_weight()
+        self.assertEqual(
+            product_product.weight,
+            30,
+            "Error: If value are not get 30\
+            Method: _compute_product_weight()",
+        )
+        product_product.config_ok = False
+        product_product.weight_dummy = 50
+        product_product._compute_product_weight()
+        self.assertEqual(
+            product_product.weight,
+            50,
+            "Error: If value are not get 50\
+            Method: _compute_product_weight()",
+        )
+
+    def test_09_compute_config_name(self):
+        product_product = self._get_product_id()
+        product_product.config_ok = False
+        product_product._compute_config_name()
+        self.assertEqual(
+            product_product.config_name,
+            "2 Series",
+            "Error: If different product config_name\
+            Method: _compute_config_name()",
+        )
+        product_product.config_ok = True
+        product_product._compute_config_name()
+        self.assertEqual(
+            product_product.config_name,
+            "2 Series",
+            "Error: If different product config_name\
+            Method: _compute_config_name()",
+        )
+
+    def test_10_reconfigure_product(self):
+        self.product_tmpl_id.configure_product()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        # reconfigure product
+        product_product = self.product_tmpl_id.product_variant_ids
+        product_product.reconfigure_product()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218d.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_silver.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        value_ids = self.value_gasoline + self.value_218d + self.value_silver
+        # val_ids = self.value_gasoline + self.value_218i + self.value_red
+        # pta_val_ids = self.env["product.template.attribute.value"].search(
+        #     [
+        #         ("product_tmpl_id", "=", self.product_tmpl_id.id),
+        #         ("product_attribute_value_id", "in", value_ids.ids),
+        #     ]
+        # )
+        new_variant = self.product_tmpl_id.product_variant_ids.filtered(
+            lambda variant: variant.product_template_attribute_value_ids == value_ids
+        )
+        self.assertFalse(
+            new_variant.id,
+            "Error: if variant id not exists\
+            Method: reconfigure_product()",
+        )
+
+    def test_11_compute_product_weight_extra(self):
+        product_id = self.env.ref("product.product_delivery_01")
+        product_template_attribute_value_ids = self.env.ref(
+            "product.product_4_attribute_1_value_2"
+        )
+        product_template_attribute_value_ids.write(
+            {
+                "weight_extra": 50.0,
+            }
+        )
+        product_id._compute_product_weight_extra()
+        product_id.write(
+            {
+                "product_template_attribute_value_ids": product_template_attribute_value_ids
+            }
+        )
+        self.assertEqual(
+            product_template_attribute_value_ids.weight_extra,
+            50.0,
+            product_id.weight_extra,
+        )
+
+        # _compute_product_weight_extra
+        product_product = self._get_product_id()
+        productAttPrice = self.env["product.template.attribute.value"].search(
+            [
+                ("product_tmpl_id", "=", self.config_product.id),
+                ("product_attribute_value_id", "=", self.value_gasoline.id),
+            ]
+        )
+        productAttPrice.weight_extra = 45
+        product_product._compute_product_weight_extra()
+        self.assertEqual(
+            productAttPrice.weight_extra,
+            product_product.weight_extra,
+            "Error: If weight_extra not equal\
+            Method: _compute_product_weight_extra()",
+        )
+
+    def test_12_unlink(self):
+        product_product = self._get_product_id()
+        unlinkVals = product_product.unlink()
+        self.assertTrue(
+            unlinkVals,
+            "Error: If unlink record true\
+            Method: unlink()",
+        )
+
+    def test_13_copy(self):
+        vals = self.config_product.copy()
+        self.assertEqual(
+            vals.name,
+            "2 Series (copy)",
+            "Error: If not equal\
+            Method: copy()",
+        )
+        self.assertTrue(
+            vals.attribute_line_ids,
+            "Error: If attribute_line_ids not exists\
+            Method: copy()",
+        )
+
+    def test_14_validate_unique_config(self):
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_gasoline.id,
+                            "value_ids": [(6, 0, [self.value_218i.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        with self.assertRaises(ValidationError):
+            self.product_tmpl_id.write(
+                {
+                    "attribute_value_line_ids": [
+                        (
+                            0,
+                            0,
+                            {
+                                "product_tmpl_id": self.product_tmpl_id.id,
+                                "value_id": self.value_gasoline.id,
+                                "value_ids": [(6, 0, [self.value_218i.id])],
+                            },
+                        )
+                    ]
+                }
+            )
+
+    def test_15_check_attr_value_ids(self):
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_gasoline.id,
+                            "value_ids": [(6, 0, [self.value_gasoline.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_diesel.id,
+                            "value_ids": [(6, 0, [self.value_diesel.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_218i.id,
+                            "value_ids": [(6, 0, [self.value_218i.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_220i.id,
+                            "value_ids": [(6, 0, [self.value_220i.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_218d.id,
+                            "value_ids": [(6, 0, [self.value_218d.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_220d.id,
+                            "value_ids": [(6, 0, [self.value_220d.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_red.id,
+                            "value_ids": [(6, 0, [self.value_red.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_value_line_ids": [
+                    (
+                        0,
+                        0,
+                        {
+                            "product_tmpl_id": self.product_tmpl_id.id,
+                            "value_id": self.value_silver.id,
+                            "value_ids": [(6, 0, [self.value_silver.id])],
+                        },
+                    )
+                ]
+            }
+        )
+        with self.assertRaises(ValidationError):
+            self.product_tmpl_id.write(
+                {
+                    "attribute_value_line_ids": [
+                        (
+                            0,
+                            0,
+                            {
+                                "product_tmpl_id": self.product_tmpl_id.id,
+                                "value_id": self.value_rims_378.id,
+                                "value_ids": [(6, 0, [self.value_rims_378.id])],
+                            },
+                        )
+                    ]
+                }
+            )
+
+    def test_16_check_duplicate_product(self):
+        self.product_tmpl_id.configure_product()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        val_ids = self.value_gasoline + self.value_218i + self.value_red
+        pta_val_ids = self.env["product.template.attribute.value"].search(
+            [
+                ("product_tmpl_id", "=", self.product_tmpl_id.id),
+                ("product_attribute_value_id", "in", val_ids.ids),
+            ]
+        )
+        with self.assertRaises(ValidationError):
+            self.env["product.product"].create(
+                {
+                    "name": "Test Configuration",
+                    "product_tmpl_id": self.product_tmpl_id.id,
+                    "product_template_attribute_value_ids": [(6, 0, pta_val_ids.ids)],
+                }
+            )
+
+    def test_17_fields_view_get(self):
+        product_product = self._get_product_id()
+        product_product.with_context(default_config_ok=True).get_view()
+
+    def test_19_compute_product_variant_count(self):
+        self.product_tmpl_id = self.env["product.template"].create(
+            {
+                "name": "Test Configuration",
+                "config_ok": True,
+                "type": "consu",
+                "categ_id": self.product_category.id,
+            }
+        )
+        product_variant_count = self.product_tmpl_id.product_variant_count
+        self.assertEqual(
+            product_variant_count,
+            1,
+            "Error: If not equal\
+            Method: _compute_product_variant_count()",
+        )
+
+    def test_20_get_config_name(self):
+        product_product = self._get_product_id()
+        product_product._get_config_name()
+        self.assertTrue(
+            product_product.name,
+            "Error: If value False\
+            Method: _get_config_name()",
+        )
+
+    def test_21_search_product_weight(self):
+        product_product = self._get_product_id()
+        operator = "and"
+        value = 10
+        search_product_weight = product_product._search_product_weight(operator, value)
+        self.assertTrue(
+            search_product_weight,
+            "Error: If value False\
+            Method: _search_product_weight()",
+        )
+
+    def test_22_search_weight(self):
+        operator = "and"
+        value = 10
+        search_weight = self.product_tmpl_id._search_weight(operator, value)
+        self.assertTrue(
+            search_weight,
+            "Error: If value False\
+            Method: _search_weight()",
+        )
+
+    def test_23_check_config_line_domain(self):
+        product_config_line = self.env.ref(
+            "product_configurator.product_config_line_218_lines"
+        )
+        with self.assertRaises(ValidationError):
+            self.env["product.template"].create(
+                {
+                    "name": "template_test",
+                    "config_line_ids": product_config_line,
+                }
+            )
diff --git a/product_configurator/tests/test_product_attribute.py b/product_configurator/tests/test_product_attribute.py
new file mode 100644
index 0000000000..6e65796133
--- /dev/null
+++ b/product_configurator/tests/test_product_attribute.py
@@ -0,0 +1,182 @@
+from odoo.exceptions import ValidationError
+from odoo.tests.common import TransactionCase
+
+
+class ProductAttributes(TransactionCase):
+    def setUp(self):
+        super(ProductAttributes, self).setUp()
+        self.productAttributeLine = self.env["product.template.attribute.line"]
+        self.ProductAttributeFuel = self.env.ref(
+            "product_configurator.product_attribute_fuel"
+        )
+        self.ProductAttributeLineFuel = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_fuel"
+        )
+        self.ProductTemplate = self.env.ref("product_configurator.bmw_2_series")
+        self.product_category = self.env.ref("product.product_category_5")
+        self.ProductAttributePrice = self.env["product.template.attribute.value"]
+        self.attr_fuel = self.env.ref("product_configurator.product_attribute_fuel")
+        self.attr_engine = self.env.ref("product_configurator.product_attribute_engine")
+        self.value_diesel = self.env.ref(
+            "product_configurator.product_attribute_value_diesel"
+        )
+        self.value_218i = self.env.ref(
+            "product_configurator.product_attribute_value_218i"
+        )
+        self.value_gasoline = self.env.ref(
+            "product_configurator.product_attribute_value_gasoline"
+        )
+        self.ProductAttributeValueFuel = self.value_gasoline.attribute_id.id
+
+    def test_01_onchange_custome_type(self):
+        self.ProductAttributeFuel.min_val = 20
+        self.ProductAttributeFuel.max_val = 30
+        self.ProductAttributeFuel.custom_type = "char"
+        self.ProductAttributeFuel.onchange_custom_type()
+        self.assertEqual(self.ProductAttributeFuel.min_val, 0, "Min value is not False")
+        self.assertEqual(self.ProductAttributeFuel.max_val, 0, "Max value is not False")
+
+        self.ProductAttributeFuel.min_val = 20
+        self.ProductAttributeFuel.max_val = 30
+        self.ProductAttributeFuel.custom_type = "integer"
+        self.ProductAttributeFuel.onchange_custom_type()
+        self.assertEqual(
+            self.ProductAttributeFuel.min_val,
+            20,
+            "Min value is not equal to existing min value",
+        )
+        self.assertEqual(
+            self.ProductAttributeFuel.max_val,
+            30,
+            "Max value is not equal to existing max value",
+        )
+
+        self.ProductAttributeFuel.custom_type = "float"
+        self.ProductAttributeFuel.onchange_custom_type()
+        self.assertEqual(
+            self.ProductAttributeFuel.min_val,
+            20,
+            "Min value is equal to existing min value \
+            when type is changed to integer to float",
+        )
+        self.assertEqual(
+            self.ProductAttributeFuel.max_val,
+            30,
+            "Max value is equal to existing max value \
+            when type is changed to integer to float",
+        )
+        self.ProductAttributeFuel.custom_type = "binary"
+        self.ProductAttributeFuel.onchange_custom_type()
+        self.assertFalse(
+            self.ProductAttributeFuel.search_ok,
+            "Error: if search true\
+            Method: onchange_custom_type()",
+        )
+
+    def test_02_onchange_val_custom(self):
+        self.ProductAttributeFuel.val_custom = False
+        self.ProductAttributeFuel.custom_type = "integer"
+        self.ProductAttributeFuel.onchange_val_custom_field()
+        self.assertFalse(
+            self.ProductAttributeFuel.custom_type, "custom_type is not False"
+        )
+
+    def test_03_check_searchable_field(self):
+        self.ProductAttributeFuel.custom_type = "binary"
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeFuel.search_ok = True
+
+    def test_04_validate_custom_val(self):
+        self.ProductAttributeFuel.write({"max_val": 20, "min_val": 10})
+        self.ProductAttributeFuel.custom_type = "integer"
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeFuel.validate_custom_val(5)
+
+        self.ProductAttributeFuel.write({"max_val": 0, "min_val": 10})
+        self.ProductAttributeFuel.custom_type = "integer"
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeFuel.validate_custom_val(5)
+
+        self.ProductAttributeFuel.write({"min_val": 0, "max_val": 20})
+        self.ProductAttributeFuel.custom_type = "integer"
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeFuel.validate_custom_val(25)
+
+    def test_05_check_constraint_min_max_value(self):
+        self.ProductAttributeFuel.custom_type = "integer"
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeFuel.write({"max_val": 10, "min_val": 20})
+
+    def test_06_onchange_attribute(self):
+        with self.env.do_in_onchange():
+            self.ProductAttributeLineFuel.onchange_attribute()
+            self.assertFalse(
+                self.ProductAttributeLineFuel.value_ids, "value_ids is not False"
+            )
+            self.assertTrue(
+                self.ProductAttributeLineFuel.required, "required not exsits value"
+            )
+            self.ProductAttributeLineFuel.multi = True
+            self.assertTrue(
+                self.ProductAttributeLineFuel.multi, "multi not exsits value"
+            )
+            self.ProductAttributeLineFuel.custom = True
+            self.assertTrue(
+                self.ProductAttributeLineFuel.custom, "custom not exsits value"
+            )
+
+    def test_07_check_default_values(self):
+        with self.assertRaises(ValidationError):
+            self.ProductAttributeLineFuel.default_val = self.value_218i.id
+
+    def test_08_copy_attribute(self):
+        copyAttribute = self.ProductAttributeFuel.copy()
+        self.assertEqual(
+            copyAttribute.name,
+            "Fuel (copy)",
+            "Error: If not copy attribute\
+            Method: copy()",
+        )
+
+    def test_09_compute_get_value_id(self):
+        attrvalline = self.env["product.attribute.value.line"].create(
+            {
+                "product_tmpl_id": self.ProductTemplate.id,
+                "value_id": self.value_gasoline.id,
+            }
+        )
+        self.assertTrue(
+            attrvalline.product_value_ids,
+            "Error: If product_value_ids not exists\
+            Method: _compute_get_value_id()",
+        )
+
+    def test_10_validate_configuration(self):
+        with self.assertRaises(ValidationError):
+            self.env["product.attribute.value.line"].create(
+                {
+                    "product_tmpl_id": self.ProductTemplate.id,
+                    "value_id": self.value_diesel.id,
+                    "value_ids": [(6, 0, [self.value_218i.id])],
+                }
+            )
+
+    def test_11_copy(self):
+        default = {}
+        productattribute = self.value_gasoline.copy(default)
+        self.assertEqual(
+            productattribute.name,
+            self.value_gasoline.name + " (copy)",
+            "Error: If not equal productattribute name\
+            Method: copy()",
+        )
+
+    def test_12_onchange_values(self):
+        productattributeline = self.env["product.template.attribute.line"]
+        productattributeline.onchange_values()
+        self.assertEqual(
+            productattributeline.default_val,
+            productattributeline.value_ids,
+            "Error: If default_val not exists\
+            Method: onchange_values()",
+        )
diff --git a/product_configurator/tests/test_product_config.py b/product_configurator/tests/test_product_config.py
new file mode 100644
index 0000000000..9dbff9acad
--- /dev/null
+++ b/product_configurator/tests/test_product_config.py
@@ -0,0 +1,711 @@
+from odoo.exceptions import UserError, ValidationError
+
+from ..tests.test_product_configurator_test_cases import ProductConfiguratorTestCases
+
+
+class ProductConfig(ProductConfiguratorTestCases):
+    def setUp(self):
+        super(ProductConfig, self).setUp()
+        self.productConfWizard = self.env["product.configurator"]
+        self.productTemplate = self.env["product.template"]
+        self.productAttribute = self.env["product.attribute"]
+        self.productAttributeVals = self.env["product.attribute.value"]
+        self.productAttributeLine = self.env["product.template.attribute.line"]
+        self.productConfigSession = self.env["product.config.session"]
+        self.productConfigDomain = self.env["product.config.domain"]
+        self.config_product = self.env.ref("product_configurator.bmw_2_series")
+        self.attr_engine = self.env.ref("product_configurator.product_attribute_engine")
+        self.config_step_engine = self.env.ref(
+            "product_configurator.config_step_engine"
+        )
+        self.config_product_1 = self.env.ref(
+            "product_configurator.product_config_line_gasoline_engines"
+        )
+        self.config_product_2 = self.env.ref(
+            "product_configurator.2_series_config_step_body"
+        )
+        # domain
+        self.domain_gasolin = self.env.ref(
+            "product_configurator.product_config_domain_gasoline"
+        )
+        self.domain_engine = self.env.ref(
+            "product_configurator.product_config_domain_diesel"
+        )
+        self.config_image_red = self.env.ref("product_configurator.config_image_1")
+        # value
+        self.value_gasoline = self.env.ref(
+            "product_configurator.product_attribute_value_gasoline"
+        )
+        self.value_diesel = self.env.ref(
+            "product_configurator.product_attribute_value_diesel"
+        )
+        self.value_red = self.env.ref(
+            "product_configurator.product_attribute_value_red"
+        )
+        # config_step
+        self.config_step_engine = self.env.ref(
+            "product_configurator.config_step_engine"
+        )
+        self.attribute_line = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_engine"
+        )
+        self.value_silver = self.env.ref(
+            "product_configurator.product_attribute_value_silver"
+        )
+        self.value_rims_387 = self.env.ref(
+            "product_configurator.product_attribute_value_rims_387"
+        )
+        # attribute line
+        self.attribute_line_2_series_rims = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_rims"
+        )
+        self.attribute_line_2_series_tapistry = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_tapistry"
+        )
+        self.attribute_value_tapistry_oyster_black = self.env.ref(
+            "product_configurator." + "product_attribute_value_tapistry_oyster_black"
+        )
+        self.attribute_line_2_series_transmission = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_transmission"
+        )
+
+        # attribute value
+        self.attribute_rims = self.env.ref(
+            "product_configurator.product_attribute_rims"
+        )
+        self.attribute_tapistry = self.env.ref(
+            "product_configurator.product_attribute_tapistry"
+        )
+        self.attribute_transmission = self.env.ref(
+            "product_configurator.product_attribute_transmission"
+        )
+
+        # session id
+        self.session_id = self.productConfigSession.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+                "value_ids": [
+                    (
+                        6,
+                        0,
+                        [
+                            self.value_gasoline.id,
+                            self.value_transmission.id,
+                            self.value_red.id,
+                        ],
+                    )
+                ],
+                "user_id": self.env.user.id,
+            }
+        )
+        # ir attachment
+        self.irAttachement = self.env["ir.attachment"].create(
+            {
+                "name": "Test attachement",
+                "datas": "bWlncmF0aW9uIHRlc3Q=",
+            }
+        )
+
+        # configure product
+        self._configure_product_nxt_step()
+        self.config_session = self.productConfigSession.search(
+            [("product_tmpl_id", "=", self.config_product.id)]
+        )
+
+        # create product template
+        self.product_tmpl_id = self.productTemplate.create({"name": "Coca-Cola"})
+        # create attribute 1
+        self.attribute_1 = self.productAttribute.create(
+            {
+                "name": "Color",
+            }
+        )
+        # create attribute 2
+        self.attribute_2 = self.productAttribute.create(
+            {
+                "name": "Flavour",
+            }
+        )
+
+        # create attribute value 1
+        self.attribute_vals_1 = self.productAttributeVals.create(
+            {
+                "name": "Orange",
+                "attribute_id": self.attribute_1.id,
+            }
+        )
+        # create attribute value 2
+        self.attribute_vals_2 = self.productAttributeVals.create(
+            {
+                "name": "Balck",
+                "attribute_id": self.attribute_1.id,
+            }
+        )
+        # create attribute value 3
+        self.attribute_vals_3 = self.productAttributeVals.create(
+            {
+                "name": "Coke",
+                "attribute_id": self.attribute_2.id,
+            }
+        )
+        # create attribute value 4
+        self.attribute_vals_4 = self.productAttributeVals.create(
+            {
+                "name": "Mango",
+                "attribute_id": self.attribute_2.id,
+            }
+        )
+
+    # TODO :: Left to take review of code
+    def test_00_check_value_attributes(self):
+        with self.assertRaises(ValidationError):
+            self.config_product_1.write(
+                {"value_ids": [(6, 0, [self.value_gasoline.id])]}
+            )
+
+    def test_01_check_config_step(self):
+        with self.assertRaises(ValidationError):
+            self.config_product_2.config_step_id = 4
+
+    def test_02_get_trans_implied(self):
+        self.domain_gasolin.write({"implied_ids": [(6, 0, [self.domain_engine.id])]})
+        trans_implied_ids = self.domain_gasolin.trans_implied_ids.ids
+        self.assertEqual(
+            trans_implied_ids[-1],
+            self.domain_engine.id,
+            "Error: If value not exists\
+            Method: _get_trans_implied()",
+        )
+
+    def test_03_check_config_step(self):
+        with self.assertRaises(ValidationError):
+            self.env["product.config.step.line"].create(
+                {
+                    "product_tmpl_id": self.config_product.id,
+                    "config_step_id": self.config_step_engine.id,
+                    "attribute_line_ids": [(6, 0, [self.attribute_line.id])],
+                }
+            )
+
+    def test_04_compute_cfg_price(self):
+        # check for _compute_cfg_price
+        price = self.config_product.list_price
+        price += self.value_220i.product_id.lst_price
+        price += self.value_model_sport_line.product_id.lst_price
+        price += self.value_transmission.product_id.lst_price
+        price += self.value_options_2.product_id.lst_price
+        self.assertEqual(
+            self.session_id.price,
+            price,
+            "Error: If different session price and list_price\
+            Method: _compute_cfg_price",
+        )
+
+    def test_05_get_custom_vals_dict(self):
+        # check for _get_custom_vals_dict
+        productConfigSessionCustVals = self.env[
+            "product.config.session.custom.value"
+        ].create(
+            {"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id}
+        )
+        # check for custom type Int
+        self.attr_fuel.custom_type = "integer"
+        productConfigSessionCustVals.update({"value": 154})
+        checkIntval = self.session_id._get_custom_vals_dict()
+        attr_id = productConfigSessionCustVals.attribute_id.id
+        self.assertEqual(
+            checkIntval.get(attr_id),
+            154,
+            "Error: If Not Integer value or False\
+            Method: _get_custom_vals_dict()",
+        )
+        # check for custom type Float
+        self.attr_fuel.custom_type = "float"
+        productConfigSessionCustVals.update({"value": 94.5})
+        checkFloatval = self.session_id._get_custom_vals_dict()
+        attr_id = productConfigSessionCustVals.attribute_id.id
+        self.assertEqual(
+            checkFloatval.get(attr_id),
+            94.5,
+            "Error: If Not Float value or False\
+            Method: _get_custom_vals_dict()",
+        )
+        # check for custom type Binary
+        self.attr_color.custom_type = "binary"
+        productConfigSessionCustVals1 = self.env[
+            "product.config.session.custom.value"
+        ].create(
+            {
+                "cfg_session_id": self.session_id.id,
+                "attribute_id": self.attr_color.id,
+                "attachment_ids": [(6, 0, [self.irAttachement.id])],
+            }
+        )
+        checkBinaryval = self.session_id._get_custom_vals_dict()
+        attr_id = productConfigSessionCustVals1.attribute_id.id
+        self.assertEqual(
+            checkBinaryval.get(attr_id),
+            productConfigSessionCustVals1.attachment_ids,
+            "Error: If Not attachement\
+            Method: _get_custom_vals_dict()",
+        )
+
+    def test_06_compute_config_step_name(self):
+        self.config_session._compute_config_step_name()
+        self.assertTrue(
+            self.config_session.config_step_name,
+            "Error: If not config step name\
+            Method: _compute_config_step_name()",
+        )
+        self.config_session._compute_config_step_name()
+        self.assertEqual(
+            self.config_session.config_step_name,
+            "Extras",
+            "Error: If not equal config_step_name and config_step\
+            Method: _compute_config_step_name()",
+        )
+        session = self.productConfigSession.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+                "value_ids": [
+                    (6, 0, [self.value_gasoline.id, self.value_transmission.id])
+                ],
+                "user_id": self.env.user.id,
+            }
+        )
+        session._compute_config_step_name()
+        self.assertFalse(
+            session.config_step_name,
+            "Error: If config_step_name not False\
+            Method: _compute_config_step_name()",
+        )
+
+    def test_07_search_variant(self):
+        with self.assertRaises(ValidationError):
+            self.env["product.config.session"].search_variant()
+
+        # check for search duplicate variant
+        variant_id = self.config_product.product_variant_ids
+        checkSearchvarient = self.config_session.search_variant()
+        self.assertEqual(
+            checkSearchvarient,
+            variant_id,
+            "Error: If Not Equal Variant or False\
+            Method: search_variant()",
+        )
+
+    def test_08_check_custom_type(self):
+        # check for check_custom_type
+        with self.assertRaises(ValidationError):
+            self.env["product.config.session.custom.value"].create(
+                {
+                    "attribute_id": self.value_silver.attribute_id.id,
+                    "cfg_session_id": self.config_session.id,
+                    "value": "Test",
+                    "attachment_ids": [(6, 0, [self.irAttachement.id])],
+                }
+            )
+
+        self.attr_color.custom_type = "binary"
+        with self.assertRaises(ValidationError):
+            self.env["product.config.session.custom.value"].create(
+                {
+                    "attribute_id": self.value_silver.attribute_id.id,
+                    "cfg_session_id": self.config_session.id,
+                    "value": "Test",
+                    "attachment_ids": [(6, 0, [self.irAttachement.id])],
+                }
+            )
+
+    def test_09_create_get_variant(self):
+        # configure new product to check for search not dublicate variant
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_1.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
+                ],
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_2.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
+                ],
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        self.product_tmpl_id.configure_product()
+        self.productConfWizard.action_next_step()
+        product_config_wizard = self.productConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attribute_1.id): self.attribute_vals_1.id,
+                "__attribute_{}".format(self.attribute_2.id): self.attribute_vals_3.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        config_session_1 = self.productConfigSession.search(
+            [("product_tmpl_id", "=", self.product_tmpl_id.id)]
+        )
+        createVarientId = config_session_1.create_get_variant()
+        self.assertEqual(
+            createVarientId.name,
+            self.product_tmpl_id.name,
+            "Error: If Not Equal variant name\
+            Method: search_variant()",
+        )
+        self.attributeLine1.custom = True
+        self.env["product.config.session.custom.value"].create(
+            {
+                "cfg_session_id": config_session_1.id,
+                "attribute_id": self.attribute_1.id,
+                "value": "Coke",
+            }
+        )
+        config_session_1.create_get_variant()
+
+    def test_10_check_value_ids(self):
+        with self.assertRaises(ValidationError):
+            self.config_image_red.write(
+                {"value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])]}
+            )
+
+    def test_11_unique_attribute(self):
+        with self.assertRaises(ValidationError):
+            self.env["product.config.session.custom.value"].create(
+                {
+                    "cfg_session_id": self.config_session.id,
+                    "attribute_id": self.attr_engine.id,
+                    "value": "1234",
+                }
+            )
+            self.env["product.config.session.custom.value"].create(
+                {
+                    "cfg_session_id": self.config_session.id,
+                    "attribute_id": self.attr_engine.id,
+                    "value": "1234",
+                }
+            )
+
+    def test_12_get_cfg_weight(self):
+        self.env["product.template.attribute.value"].create(
+            {
+                "product_tmpl_id": self.config_product.id,
+                "product_attribute_value_id": self.value_red.id,
+                "weight_extra": 20.0,
+            }
+        )
+        self.config_product.weight = 20
+        weightVal = self.config_session.get_cfg_weight()
+        self.assertEqual(
+            weightVal,
+            40.0,
+            "Error: If Value are not equal\
+            Method: get_cfg_weight()",
+        )
+        # check for config weight
+        self.assertEqual(
+            self.config_session.weight,
+            40.0,
+            "Error: If config weight are not equal\
+            Method: _compute_cfg_weight()",
+        )
+
+    def test_13_update_session_configuration_value(self):
+        # configure new product to check for search not dublicate variant
+        self.custom_vals = self.productConfigSession.get_custom_value_id()
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_1.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
+                ],
+                "custom": True,
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_2.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
+                ],
+                "custom": True,
+                "required": True,
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        self.attribute_1.custom_type = "binary"
+        self.product_tmpl_id.configure_product()
+        self.productConfWizard.action_next_step()
+        product_config_wizard = self.productConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attribute_1.id): self.custom_vals.id,
+                "__custom_{}".format(self.attribute_1.id): self.irAttachement.id,
+                "__attribute_{}".format(self.attribute_1.id): self.custom_vals.id,
+                "__custom_{}".format(self.attribute_1.id): "Test",
+            }
+        )
+        product_config_wizard.action_next_step()
+
+    def test_14_get_cfg_price(self):
+        self.env["product.template.attribute.value"].create(
+            {
+                "product_tmpl_id": self.config_product.id,
+                "product_attribute_value_id": self.value_red.id,
+                "weight_extra": 20.0,
+                "price_extra": 20.0,
+            }
+        )
+        price = self.config_product.list_price
+        price += self.value_220i.product_id.lst_price
+        price += self.value_model_sport_line.product_id.lst_price
+        price += self.value_transmission.product_id.lst_price
+        price += self.value_options_2.product_id.lst_price
+        price_extra_val = self.session_id.get_cfg_price()
+        self.assertEqual(
+            price_extra_val,
+            price + 20,
+            "Error: If not equal price extra\
+            Method: get_cfg_price()",
+        )
+
+    def test_15_get_next_step(self):
+        self.session_id.get_next_step(state=None)
+        self.session_id.get_next_step(state="draft")
+        with self.assertRaises(UserError):
+            self.productConfigSession.get_next_step(
+                state="draft", value_ids=False, custom_value_ids=False
+            )
+
+    def test_16_get_all_step_lines(self):
+        step_line_value_1 = self.productConfigSession.get_all_step_lines()
+        self.assertFalse(
+            step_line_value_1,
+            "Error: If return True\
+            Method: get_all_step_lines()",
+        )
+        step_line_value_2 = self.session_id.get_all_step_lines()
+        self.assertTrue(
+            step_line_value_2,
+            "Error: If return True\
+            Method: get_all_step_lines()",
+        )
+
+    def test_17_custom_value_validate_configuration(self):
+        self.custom_vals = self.productConfigSession.get_custom_value_id()
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_1.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
+                ],
+                "custom": True,
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_2.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
+                ],
+                "custom": True,
+                "required": True,
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        self.attribute_1.custom_type = "binary"
+        self.product_tmpl_id.configure_product()
+        self.productConfWizard.action_next_step()
+        product_config_wizard = self.productConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attribute_1.id): self.custom_vals.id,
+                "__custom_{}".format(self.attribute_1.id): self.irAttachement.id,
+                "__attribute_{}".format(self.attribute_1.id): self.custom_vals.id,
+                "__custom_{}".format(self.attribute_1.id): "Test",
+            }
+        )
+        self.attributeLine1.custom = False
+        self.attributeLine2.custom = False
+        with self.assertRaises(ValidationError):
+            self.product_tmpl_id.configure_product()
+
+    def test_18_onchange_attribute(self):
+        # create domain
+        self.productConfigDomainId = self.env["product.config.domain"].create(
+            {"name": "restriction 1"}
+        )
+        self.productConfigDomainId.compute_domain()
+        # create attribute value line 1
+        self.env["product.config.domain.line"].create(
+            {
+                "domain_id": self.productConfigDomainId.id,
+                "attribute_id": self.attr_fuel.id,
+                "condition": "in",
+                "value_ids": [(6, 0, [self.value_gasoline.id])],
+                "operator": "and",
+            }
+        )
+        self.env["product.config.domain.line"].create(
+            {
+                "domain_id": self.productConfigDomainId.id,
+                "attribute_id": self.attr_color.id,
+                "condition": "in",
+                "value_ids": [(6, 0, [self.value_red.id])],
+                "operator": "and",
+            }
+        )
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_1.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
+                ],
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attribute_2.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
+                ],
+                "required": True,
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        self.productConfigDomainId.compute_domain()
+        # create attribute value line 1
+        config_line = self.env["product.config.line"].create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_line_id": self.attributeLine1.id,
+                "value_ids": [
+                    (6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
+                ],
+                "domain_id": self.productConfigDomainId.id,
+            }
+        )
+        with self.assertRaises(ValidationError):
+            config_line.onchange_attribute()
+
+        self.assertFalse(
+            config_line.value_ids,
+            "Error: If value_ids True\
+            Method: onchange_attribute()",
+        )
+
+    def test_19_eval(self):
+        self.attr_color.custom_type = "binary"
+        productConfigSessionCustVals1 = self.env[
+            "product.config.session.custom.value"
+        ].create(
+            {
+                "cfg_session_id": self.session_id.id,
+                "attribute_id": self.attr_color.id,
+                "attachment_ids": [(6, 0, [self.irAttachement.id])],
+            }
+        )
+        checkBinary = productConfigSessionCustVals1.eval()
+        self.assertTrue(
+            checkBinary,
+            "Error: If value False\
+            Method: eval()",
+        )
+
+        productConfigSessionCustVals = self.env[
+            "product.config.session.custom.value"
+        ].create(
+            {"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id}
+        )
+        self.attr_fuel.custom_type = "integer"
+        productConfigSessionCustVals.update({"value": 154})
+        checkIntval = productConfigSessionCustVals.eval()
+        self.assertEqual(
+            154,
+            checkIntval,
+            "Error: If Value not equal\
+            Method: eval()",
+        )
+
+        self.attr_fuel.custom_type = "float"
+        productConfigSessionCustVals.update({"value": 15.4})
+        checkfloat = productConfigSessionCustVals.eval()
+        self.assertEqual(
+            15.4,
+            checkfloat,
+            "Error: If Value not equal\
+            Method: eval()",
+        )
+
+    def test_20_values_available(self):
+        check_available_val_ids = (
+            self.value_gasoline + self.value_218i + self.value_sport_line
+        ).ids
+        product_tmpl_id = self.config_product.id
+        values_ids = [self.value_diesel.id]
+        available_value_ids = self.productConfigSession.values_available(
+            check_available_val_ids, values_ids, {}, product_tmpl_id
+        )
+        self.assertNotIn(
+            self.value_sport_line.id,
+            available_value_ids,
+            "Error: If value exists\
+            Method: values_available()",
+        )
diff --git a/product_configurator/tests/test_product_configurator_test_cases.py b/product_configurator/tests/test_product_configurator_test_cases.py
new file mode 100644
index 0000000000..15a721cfeb
--- /dev/null
+++ b/product_configurator/tests/test_product_configurator_test_cases.py
@@ -0,0 +1,127 @@
+from odoo.tests.common import TransactionCase
+
+
+class ProductConfiguratorTestCases(TransactionCase):
+    def setUp(self):
+        super(ProductConfiguratorTestCases, self).setUp()
+
+        self.ProductConfWizard = self.env["product.configurator"]
+        self.config_product = self.env.ref("product_configurator.bmw_2_series")
+        self.product_category = self.env.ref("product.product_category_5")
+
+        # attributes
+        self.attr_fuel = self.env.ref("product_configurator.product_attribute_fuel")
+        self.attr_engine = self.env.ref("product_configurator.product_attribute_engine")
+        self.attr_color = self.env.ref("product_configurator.product_attribute_color")
+        self.attr_rims = self.env.ref("product_configurator.product_attribute_rims")
+        self.attr_model_line = self.env.ref(
+            "product_configurator.product_attribute_model_line"
+        )
+        self.attr_tapistry = self.env.ref(
+            "product_configurator.product_attribute_tapistry"
+        )
+        self.attr_transmission = self.env.ref(
+            "product_configurator.product_attribute_transmission"
+        )
+        self.attr_options = self.env.ref(
+            "product_configurator.product_attribute_options"
+        )
+
+        # values
+        self.value_gasoline = self.env.ref(
+            "product_configurator.product_attribute_value_gasoline"
+        )
+        self.value_218i = self.env.ref(
+            "product_configurator.product_attribute_value_218i"
+        )
+        self.value_220i = self.env.ref(
+            "product_configurator.product_attribute_value_220i"
+        )
+        self.value_red = self.env.ref(
+            "product_configurator.product_attribute_value_red"
+        )
+        self.value_rims_378 = self.env.ref(
+            "product_configurator.product_attribute_value_rims_378"
+        )
+        self.value_sport_line = self.env.ref(
+            "product_configurator.product_attribute_value_sport_line"
+        )
+        self.value_model_sport_line = self.env.ref(
+            "product_configurator.product_attribute_value_model_sport_line"
+        )
+        self.value_tapistry = self.env.ref(
+            "product_configurator.product_attribute_value_tapistry" + "_oyster_black"
+        )
+        self.value_transmission = self.env.ref(
+            "product_configurator.product_attribute_value_steptronic"
+        )
+        self.value_options_1 = self.env.ref(
+            "product_configurator.product_attribute_value_smoker_package"
+        )
+        self.value_options_2 = self.env.ref(
+            "product_configurator.product_attribute_value_sunroof"
+        )
+
+    def _configure_product_nxt_step(self):
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+                "__attribute_{}".format(self.attr_rims.id): self.value_rims_378.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_sport_line.id,
+            }
+        )
+        product_config_wizard.action_previous_step()
+        product_config_wizard.action_previous_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_engine.id): self.value_220i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_model_sport_line.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_tapistry.id): self.value_tapistry.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_transmission.id
+                ): self.value_transmission.id,
+                "__attribute_{}".format(self.attr_options.id): [
+                    [6, 0, [self.value_options_2.id]]
+                ],
+            }
+        )
+
+        return product_config_wizard.action_next_step()
diff --git a/product_configurator/tests/test_wizard.py b/product_configurator/tests/test_wizard.py
new file mode 100644
index 0000000000..0c35b93194
--- /dev/null
+++ b/product_configurator/tests/test_wizard.py
@@ -0,0 +1,604 @@
+from odoo.exceptions import UserError
+
+from ..tests.test_product_configurator_test_cases import ProductConfiguratorTestCases
+
+
+class ConfigurationWizard(ProductConfiguratorTestCases):
+    def setUp(self):
+        super(ConfigurationWizard, self).setUp()
+        self.productTemplate = self.env["product.template"]
+        self.productAttributeLine = self.env["product.template.attribute.line"]
+        self.productConfigStepLine = self.env["product.config.step.line"]
+        self.productConfigSession = self.env["product.config.session"]
+        self.product_category = self.env.ref("product.product_category_5")
+        self.attr_line_fuel = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_fuel"
+        )
+        self.attr_line_engine = self.env.ref(
+            "product_configurator.product_attribute_line_2_series_engine"
+        )
+        self.value_diesel = self.env.ref(
+            "product_configurator.product_attribute_value_diesel"
+        )
+        self.value_218d = self.env.ref(
+            "product_configurator.product_attribute_value_218d"
+        )
+        self.value_220d = self.env.ref(
+            "product_configurator.product_attribute_value_220d"
+        )
+        self.value_silver = self.env.ref(
+            "product_configurator.product_attribute_value_silver"
+        )
+        self.config_step_engine = self.env.ref(
+            "product_configurator.config_step_engine"
+        )
+        self.config_step_body = self.env.ref("product_configurator.config_step_body")
+        self.product_tmpl_id = self.env["product.template"].create(
+            {
+                "name": "Test Configuration",
+                "config_ok": True,
+                "type": "consu",
+                "categ_id": self.product_category.id,
+            }
+        )
+        self.custom_vals = self.productConfigSession.get_custom_value_id()
+        self.cfg_tmpl = self.env.ref("product_configurator.bmw_2_series")
+
+        attribute_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids")
+        self.attr_vals = attribute_vals
+
+        self.attr_val_ext_ids = {
+            v: k for k, v in attribute_vals.get_external_id().items()
+        }
+
+    def _check_wizard_nxt_step(self):
+        self.ProductConfWizard.action_next_step()
+        product_config_wizard = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+            }
+        )
+        # create attribute line 1
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_fuel.id,
+                "value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])],
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_engine.id,
+                "value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])],
+                "required": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine3 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "attribute_id": self.attr_engine.id,
+                "value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])],
+                "required": True,
+            }
+        )
+        # configure product creating config step
+        self.configStepLine1 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "config_step_id": self.config_step_engine.id,
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        # create config_step_line 2
+        self.configStepLine2 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": self.product_tmpl_id.id,
+                "config_step_id": self.config_step_body.id,
+                "attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
+            }
+        )
+        self.product_tmpl_id.write(
+            {
+                "config_step_line_ids": [
+                    (6, 0, [self.configStepLine1.id, self.configStepLine2.id])
+                ],
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+            }
+        )
+        product_config_wizard.action_next_step()
+        product_config_wizard.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        return product_config_wizard
+
+    def test_01_action_previous_step(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard.action_previous_step()
+        self.assertEqual(
+            product_config_wizard.state,
+            str(self.configStepLine1.id),
+            "Error: If state are not equal\
+            Method: action_next_step()",
+        )
+        product_config_wizard.action_next_step()
+        self.assertEqual(
+            product_config_wizard.state,
+            str(self.configStepLine2.id),
+            "Error: If state are not equal\
+            Method: action_next_step()",
+        )
+        wizard_action = product_config_wizard.action_next_step()
+        variant_id2 = wizard_action.get("res_id")
+        self.assertTrue(
+            variant_id2,
+            "Error: If varient not exists\
+            Method: action_next_step()",
+        )
+
+    def test_02_action_reset(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        action_wizard = product_config_wizard.action_reset()
+        product_tmpl_id = action_wizard.get("context")
+        self.assertTrue(
+            product_tmpl_id.get("default_product_tmpl_id"),
+            "Error: If product_tmpl_id not exists\
+            Method: action_reset()",
+        )
+
+    def test_03_compute_attr_lines(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard._compute_attr_lines()
+        self.assertTrue(
+            product_config_wizard.attribute_line_ids,
+            "Error: If atttribute_line_ids not exists\
+            Method: _compute_attr_lines()",
+        )
+
+    def test_04_get_state_selection(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        config_wiz = product_config_wizard.with_context(
+            wizard_id=product_config_wizard.id
+        ).get_state_selection()
+        self.assertTrue(
+            config_wiz[1:],
+            "Error: If not config step selection\
+            Method: get_state_selection()",
+        )
+
+    def test_05_compute_cfg_image(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard._compute_cfg_image()
+        self.assertFalse(
+            product_config_wizard.product_img,
+            "Error: If product_img exists\
+            Method: _compute_cfg_image()",
+        )
+
+    def test_06_onchange_product_tmpl(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard.write(
+            {
+                "product_tmpl_id": self.config_product.id,
+            }
+        )
+        with self.assertRaises(UserError):
+            product_config_wizard.onchange_product_tmpl()
+
+    def test_07_get_onchange_domains(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        conf = [
+            "gasoline",
+            "228i",
+            "model_luxury_line",
+            "silver",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+        values = [
+            "gasoline",
+            "228i",
+            "model_luxury_line",
+            "silver",
+            "rims_384",
+            "tapistry_black",
+            "steptronic",
+            "smoker_package",
+            "tow_hook",
+        ]
+        product_config_wizard.get_onchange_domains(values, conf)
+
+    def test_08_onchange_state(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard._onchange_state()
+
+    def test_09_onchange_product_preset(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard._onchange_product_preset()
+
+    def test_10_open_step(self):
+        wizard = self.env["product.configurator"]
+        step_to_open = wizard.config_session_id.check_and_open_incomplete_step()
+        wizard.open_step(step_to_open)
+
+    def test_11_onchange(self):
+        field_name = ""
+        values = {"__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id}
+        product_config_wizard = self._check_wizard_nxt_step()
+        field_prefix = product_config_wizard._prefixes.get("field_prefix")
+        field_name = "%s%s" % (field_prefix, field_name)
+        specs = product_config_wizard._onchange_spec()
+        product_config_wizard.onchange(values, field_name, specs)
+
+        product_config_wizard.attribute_line_ids.update(
+            {
+                "attribute_id": self.attr_fuel.id,
+                "custom": True,
+            }
+        )
+        values2 = {
+            "__attribute_{}".format(self.attr_fuel.id): self.custom_vals.id,
+            "__custom_{}".format(self.attr_fuel.id): "Test1",
+        }
+        product_config_wizard.onchange(values2, field_name, specs)
+
+    def test_12_fields_get(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard.fields_get()
+        product_config_wizard.with_context(
+            wizard_id=product_config_wizard.id
+        ).fields_get()
+
+        # custom value
+        self.attr_line_fuel.custom = True
+        self.attr_line_engine.custom = True
+        product_config_wizard_1 = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__custom_{}".format(self.attr_fuel.id): "Test1",
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__custom_{}".format(self.attr_engine.id): "Test2",
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+                "__attribute_{}".format(self.attr_rims.id): self.value_rims_378.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_sport_line.id,
+            }
+        )
+        product_config_wizard_1.action_previous_step()
+        product_config_wizard_1.action_previous_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_engine.id): self.value_220i.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_model_sport_line.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_tapistry.id): self.value_tapistry.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_transmission.id
+                ): self.value_transmission.id,
+                "__attribute_{}".format(self.attr_options.id): [
+                    [6, 0, [self.value_options_2.id]]
+                ],
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.with_context(
+            wizard_id=product_config_wizard_1.id
+        ).fields_get()
+
+    def test_13_fields_view_get(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        product_config_wizard.fields_view_get()
+        product_config_wizard.with_context(
+            wizard_id=product_config_wizard.id
+        ).fields_view_get()
+        # custom value
+        # custom value
+        self.attr_line_fuel.custom = True
+        self.attr_line_engine.custom = True
+        product_config_wizard_1 = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+                "__custom_{}".format(self.attr_fuel.id): "Test1",
+                "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+                "__custom_{}".format(self.attr_engine.id): "Test2",
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+                "__attribute_{}".format(self.attr_rims.id): self.value_rims_378.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_sport_line.id,
+            }
+        )
+        product_config_wizard_1.action_previous_step()
+        product_config_wizard_1.action_previous_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_engine.id): self.value_220i.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_model_line.id
+                ): self.value_model_sport_line.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_tapistry.id): self.value_tapistry.id,
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(
+                    self.attr_transmission.id
+                ): self.value_transmission.id,
+                "__attribute_{}".format(self.attr_options.id): [
+                    [6, 0, [self.value_options_2.id]]
+                ],
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.with_context(
+            wizard_id=product_config_wizard_1.id
+        ).fields_view_get()
+
+    def test_14_unlink(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        unlinkWizard = product_config_wizard.unlink()
+        self.assertTrue(
+            unlinkWizard,
+            "Error: If not unlink record\
+            Method: unlink()",
+        )
+
+    def test_15_read(self):
+        product_config_wizard = self._check_wizard_nxt_step()
+        values = {
+            "__attribute_{}".format(self.attr_fuel.id): self.value_gasoline.id,
+            "__attribute_{}".format(self.attr_engine.id): self.value_218i.id,
+            "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+        }
+        product_config_wizard.read(values)
+        product_tmpl = self.env["product.template"].create(
+            {
+                "name": "Test Custom",
+                "config_ok": True,
+                "type": "consu",
+                "categ_id": self.product_category.id,
+            }
+        )
+        self.ProductConfWizard.action_next_step()
+        product_config_wizard_1 = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+            }
+        )
+        # create attribute line 1
+        self.attributeLine1 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+                "attribute_id": self.attr_fuel.id,
+                "value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])],
+                "required": True,
+                "custom": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine2 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+                "attribute_id": self.attr_engine.id,
+                "value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])],
+                "required": True,
+                "custom": True,
+            }
+        )
+        # create attribute line 2
+        self.attributeLine3 = self.productAttributeLine.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+                "attribute_id": self.attr_engine.id,
+                "value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])],
+                "required": True,
+            }
+        )
+        # configure product creating config step
+        self.configStepLine1 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+                "config_step_id": self.config_step_engine.id,
+                "attribute_line_ids": [
+                    (6, 0, [self.attributeLine1.id, self.attributeLine2.id])
+                ],
+            }
+        )
+        # create config_step_line 2
+        self.configStepLine2 = self.productConfigStepLine.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+                "config_step_id": self.config_step_body.id,
+                "attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
+            }
+        )
+        product_tmpl.write(
+            {
+                "config_step_line_ids": [
+                    (6, 0, [self.configStepLine1.id, self.configStepLine2.id])
+                ],
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): self.custom_vals.id,
+                "__custom_{}".format(self.attr_fuel.id): "#DEFSRE",
+                "__attribute_{}".format(self.attr_engine.id): self.custom_vals.id,
+                "__custom_{}".format(self.attr_engine.id): "#FERDFGR",
+            }
+        )
+        product_config_wizard_1.action_next_step()
+        product_config_wizard_1.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        # check for custom value
+        custom_vals = {
+            "__attribute_{}".format(self.attr_fuel.id): self.custom_vals.id,
+            "__custom_{}".format(self.attr_fuel.id): "#DEFSRE",
+            "__attribute_{}".format(self.attr_engine.id): self.custom_vals.id,
+            "__custom_{}".format(self.attr_engine.id): "#FERDFGR",
+            "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+        }
+        product_config_wizard_1.read(custom_vals)
+        session = self.productConfigSession.search(
+            [("product_tmpl_id", "=", product_tmpl.id)]
+        )
+        session.unlink()
+        self.attributeLine1.custom = False
+        self.attributeLine1.multi = True
+        self.ProductConfWizard.action_next_step()
+        product_config_wizard_2 = self.ProductConfWizard.create(
+            {
+                "product_tmpl_id": product_tmpl.id,
+            }
+        )
+        product_config_wizard_2.action_next_step()
+        product_config_wizard_2.write(
+            {
+                "__attribute_{}".format(self.attr_fuel.id): [
+                    (6, 0, [self.value_diesel.id, self.value_gasoline.id])
+                ],
+                "__attribute_{}".format(self.attr_engine.id): self.custom_vals.id,
+                "__custom_{}".format(self.attr_engine.id): "#FERDFGR",
+            }
+        )
+        product_config_wizard_2.action_next_step()
+        product_config_wizard_2.write(
+            {
+                "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+            }
+        )
+        # check for multi value
+        multi_vals = {
+            "__attribute_{}".format(self.attr_fuel.id): [
+                (6, 0, [self.value_diesel.id, self.value_gasoline.id])
+            ],
+            "__attribute_{}".format(self.attr_engine.id): self.custom_vals.id,
+            "__custom_{}".format(self.attr_engine.id): "#FERDFGR",
+            "__attribute_{}".format(self.attr_color.id): self.value_red.id,
+        }
+        product_config_wizard_2.read(multi_vals)
+
+    def test_16_get_onchange_domains(self):
+        self.wizard = self.env["product.configurator"]
+        # session id
+        session_id = self.productConfigSession.create(
+            {
+                "product_tmpl_id": self.config_product.id,
+                "value_ids": [
+                    (
+                        6,
+                        0,
+                        [
+                            self.value_gasoline.id,
+                            self.value_transmission.id,
+                            self.value_red.id,
+                        ],
+                    )
+                ],
+                "user_id": self.env.user.id,
+            }
+        )
+        field_prefix = self.wizard._prefixes.get("field_prefix")
+        check_available_val_id = {
+            field_prefix
+            + "%s" % (self.value_gasoline.attribute_id.id): self.value_gasoline.id,
+            field_prefix + "%s" % (self.value_218i.attribute_id.id): self.value_218i.id,
+            field_prefix
+            + "%s" % (self.value_sport_line.attribute_id.id): self.value_sport_line.id,
+        }
+        values_ids = self.value_diesel.ids
+        product_tmpl_id = self.config_product
+        domains_available = self.wizard.get_onchange_domains(
+            check_available_val_id, values_ids, product_tmpl_id, session_id
+        )
+        rec = domains_available[
+            field_prefix + str(self.value_sport_line.attribute_id.id)
+        ][-1][-1]
+        self.assertNotIn(
+            self.value_sport_line.id,
+            rec,
+            "Error: If value exists\
+            Method: get_onchange_domains()",
+        )