Move 7 IoT modules to oca-ocb-hw and 9 product modules to oca-product

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ernad Husremovic 2025-08-30 17:53:36 +02:00
parent 58e019d2db
commit bf10deb440
413 changed files with 37587 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# Product Configurator
Odoo addon: product_configurator
## Installation
```bash
pip install odoo-bringout-oca-product-configurator-product_configurator
```
## Dependencies
This addon depends on:
- account
## Manifest Information
- **Name**: Product Configurator
- **Version**: 16.0.1.1.2
- **Category**: Generic Modules/Base
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/product-configurator](https://github.com/OCA/product-configurator) branch 16.0, addon `product_configurator`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Product_configurator Module - product_configurator
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for product_configurator. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [account](https://github.com/bringout/oca-ocb-accounting/tree/b11fb50e2ed11eec1e305a0df730b49554c01199/odoo-bringout-oca-ocb-account)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon product_configurator or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-product-configurator-product_configurator"
# or
uv pip install odoo-bringout-oca-product-configurator-product_configurator"
```

View file

@ -0,0 +1,27 @@
# Models
Detected core models and extensions in product_configurator.
```mermaid
classDiagram
class product_attribute_value_line
class product_config_domain
class product_config_domain_line
class product_config_image
class product_config_line
class product_config_session
class product_config_session_custom_value
class product_config_step
class product_config_step_line
class ir_ui_view
class product_attribute
class product_attribute_value
class product_product
class product_template
class product_template_attribute_line
class product_template_attribute_value
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: product_configurator. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon product_configurator
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,42 @@
# Security
Access control and security definitions in product_configurator.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../product_configurator/security/ir.model.access.csv)**
- 39 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[configurator_security.xml](../product_configurator/security/configurator_security.xml)**
- 3 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[configurator_security.xml](../product_configurator/security/configurator_security.xml)**
- Security groups, categories, and XML-based rules
- **[ir.model.access.csv](../product_configurator/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon product_configurator
```

View file

@ -0,0 +1,9 @@
# Wizards
Transient models exposed as UI wizards in product_configurator.
```mermaid
classDiagram
class class
class ProductConfigurator
```

View file

@ -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.

View file

@ -0,0 +1,87 @@
====================
Product Configurator
====================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8356a8a41405ccb39726303feeac66d1ee3e8e998a285871fe0d6c01769a1273
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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
Contributors
~~~~~~~~~~~~
* `Aion Tech <https://aiontech.company/>`_:
* Simone Rubino <simone.rubino@aion-tech.it>
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.

View file

@ -0,0 +1,4 @@
from . import models
from . import wizard
from .init_hook import post_init_hook

View file

@ -0,0 +1,54 @@
{
"name": "Product Configurator",
"version": "16.0.1.1.2",
"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_controller.esm.js",
"/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,
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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")

View file

@ -0,0 +1,4 @@
from . import product_config
from . import product_attribute
from . import product
from . import ir_ui_view

View file

@ -0,0 +1,11 @@
from odoo import models
class View(models.Model):
_inherit = "ir.ui.view"
def _validate_tag_button(self, node, name_manager, node_info):
special = node.get("special")
if special and special == "no_save":
return
return super()._validate_tag_button(node, name_manager, node_info)

View file

@ -0,0 +1,578 @@
import logging
from io import StringIO
from mako.runtime import Context
from mako.template import Template
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class ProductTemplate(models.Model):
_inherit = "product.template"
@api.depends("product_variant_ids.product_tmpl_id")
def _compute_product_variant_count(self):
"""For configurable products return the number of variants configured or
1 as many views and methods trigger only when a template has at least
one variant attached. Since we create them from the template we should
have access to them always"""
result = super()._compute_product_variant_count()
for product_tmpl in self:
config_ok = product_tmpl.config_ok
variant_count = product_tmpl.product_variant_count
if config_ok and not variant_count:
product_tmpl.product_variant_count = 1
return result
@api.depends("attribute_line_ids.value_ids")
def _compute_template_attr_vals(self):
"""Compute all attribute values added in attribute line on
product template"""
for product_tmpl in self:
if product_tmpl.config_ok:
value_ids = product_tmpl.attribute_line_ids.mapped("value_ids")
product_tmpl.attribute_line_val_ids = value_ids
else:
product_tmpl.attribute_line_val_ids = False
@api.constrains("attribute_line_ids", "attribute_value_line_ids")
def check_attr_value_ids(self):
"""Check attribute lines don't have some attribute value that
is not present in attribute lines of that product template"""
for product_tmpl in self:
if not product_tmpl.env.context.get("check_constraint", True):
continue
attr_val_lines = product_tmpl.attribute_value_line_ids
attr_val_ids = attr_val_lines.mapped("value_ids")
if not attr_val_ids <= product_tmpl.attribute_line_val_ids:
raise ValidationError(
_(
"All attribute values used in attribute value lines "
"must be defined in the attribute lines of the "
"template"
)
)
@api.constrains("attribute_value_line_ids")
def _validate_unique_config(self):
"""Check for duplicate configurations for the same
attribute value in image lines"""
for template in self:
attr_val_line_vals = template.attribute_value_line_ids.read(
["value_id", "value_ids"], load=False
)
attr_val_line_vals = [
(line["value_id"], tuple(line["value_ids"]))
for line in attr_val_line_vals
]
if len(set(attr_val_line_vals)) != len(attr_val_line_vals):
raise ValidationError(
_("You cannot have a duplicate configuration for the same value")
)
config_ok = fields.Boolean(string="Can be Configured")
config_line_ids = fields.One2many(
comodel_name="product.config.line",
inverse_name="product_tmpl_id",
string="Attribute Dependencies",
copy=False,
)
config_image_ids = fields.One2many(
comodel_name="product.config.image",
inverse_name="product_tmpl_id",
string="Configuration Images",
copy=True,
)
attribute_value_line_ids = fields.One2many(
comodel_name="product.attribute.value.line",
inverse_name="product_tmpl_id",
string="Attribute Value Lines",
copy=True,
)
attribute_line_val_ids = fields.Many2many(
comodel_name="product.attribute.value",
compute="_compute_template_attr_vals",
store=False,
)
config_step_line_ids = fields.One2many(
comodel_name="product.config.step.line",
inverse_name="product_tmpl_id",
string="Configuration Lines",
copy=False,
)
mako_tmpl_name = fields.Text(
string="Variant name",
help="Generate Name based on Mako Template",
copy=True,
)
# We are calculating weight of variants based on weight of
# product-template so that no need of compute and inverse on this
weight = fields.Float(
compute="_compute_weight",
inverse="_set_weight", # pylint: disable=C8110
search="_search_weight",
store=False,
)
weight_dummy = fields.Float(
string="Manual Weight",
digits="Stock Weight",
help="Manual setting of product template weight",
)
def _compute_weight(self):
config_products = self.filtered(lambda template: template.config_ok)
for product in config_products:
product.weight = product.weight_dummy
standard_products = self - config_products
return super(ProductTemplate, standard_products)._compute_weight()
def _set_weight(self):
for product_tmpl in self:
product_tmpl.weight_dummy = product_tmpl.weight
if not product_tmpl.config_ok:
super(ProductTemplate, product_tmpl)._set_weight()
return
def _search_weight(self, operator, value):
return [("weight_dummy", operator, value)]
def _check_default_values(self):
default_val_ids = (
self.attribute_line_ids.filtered(lambda line: line.default_val)
.mapped("default_val")
.ids
)
cfg_session_obj = self.env["product.config.session"]
try:
cfg_session_obj.validate_configuration(
value_ids=default_val_ids, product_tmpl_id=self.id, final=False
)
except ValidationError as exc:
raise ValidationError(exc.args[0]) from exc
except Exception as exc:
raise ValidationError(
_("Default values provided generate an invalid configuration")
) from exc
@api.constrains("config_line_ids", "attribute_line_ids")
def _check_default_value_domains(self):
for template in self:
try:
template._check_default_values()
except ValidationError as exc:
raise ValidationError(
_(
"Restrictions added make the current default values "
"generate an invalid configuration.\
\n%s"
)
% (exc.name)
) from exc
def toggle_config(self):
for record in self:
record.config_ok = not record.config_ok
def _create_variant_ids(self):
"""Prevent configurable products from creating variants as these serve
only as a template for the product configurator"""
templates = self.filtered(lambda t: not t.config_ok)
if not templates:
return None
return super(ProductTemplate, templates)._create_variant_ids()
def unlink(self):
"""- Prevent the removal of configurable product templates
from variants
- Patch for check access rights of user(configurable products)"""
configurable_templates = self.filtered(lambda template: template.config_ok)
if configurable_templates:
configurable_templates[:1].check_config_user_access()
for config_template in configurable_templates:
variant_unlink = config_template.env.context.get(
"unlink_from_variant", False
)
if variant_unlink:
self -= config_template
res = super().unlink()
return res
def copy(self, default=None):
"""Copy restrictions, config Steps and attribute lines
ith product template"""
if not default:
default = {}
self = self.with_context(check_constraint=False)
res = super().copy(default=default)
# Attribute lines
attribute_line_dict = {}
for line in res.attribute_line_ids:
attribute_line_dict.update({line.attribute_id.id: line.id})
# Restrictions
for line in self.config_line_ids:
old_restriction = line.domain_id
new_restriction = old_restriction.copy()
config_line_default = {
"product_tmpl_id": res.id,
"domain_id": new_restriction.id,
}
new_attribute_line_id = attribute_line_dict.get(
line.attribute_line_id.attribute_id.id, False
)
if not new_attribute_line_id:
continue
config_line_default.update({"attribute_line_id": new_attribute_line_id})
line.copy(config_line_default)
# Config steps
config_step_line_default = {"product_tmpl_id": res.id}
for line in self.config_step_line_ids:
new_attribute_line_ids = [
attribute_line_dict.get(old_attr_line.attribute_id.id)
for old_attr_line in line.attribute_line_ids
if old_attr_line.attribute_id.id in attribute_line_dict
]
if new_attribute_line_ids:
config_step_line_default.update(
{"attribute_line_ids": [(6, 0, new_attribute_line_ids)]}
)
line.copy(config_step_line_default)
return res
def configure_product(self):
"""launches a product configurator wizard with a linked
template in order to configure new product."""
return self.with_context(product_tmpl_id_readonly=True).create_config_wizard(
click_next=False
)
def create_config_wizard(
self,
model_name="product.configurator",
extra_vals=None,
click_next=True,
):
"""create product configuration wizard
- return action to launch wizard
- click on next step based on value of click_next"""
wizard_obj = self.env[model_name]
wizard_vals = {"product_tmpl_id": self.id}
if extra_vals:
wizard_vals.update(extra_vals)
wizard = wizard_obj.create(wizard_vals)
if click_next:
action = wizard.action_next_step()
else:
wizard_obj = wizard_obj.with_context(
wizard_model=model_name,
allow_preset_selection=True,
)
action = wizard_obj.get_wizard_action(wizard=wizard)
return action
@api.model
def _check_config_group_rights(self):
"""Return True/False from system parameter
- Signals access rights needs to check or not
:Params: return : boolean"""
ICPSudo = self.env["ir.config_parameter"].sudo()
manager_product_configuration_settings = ICPSudo.get_param(
"product_configurator.manager_product_configuration_settings"
)
return manager_product_configuration_settings
@api.model
def check_config_user_access(self):
"""Check user have access to perform action(create/write/delete)
on configurable products"""
if not self._check_config_group_rights():
return True
config_manager = self.env.user.has_group(
"product_configurator.group_product_configurator_manager"
)
user_root = self.env.ref("base.user_root")
user_admin = self.env.ref("base.user_admin")
if (
config_manager
or self.env.user.id in [user_root.id, user_admin.id]
or self.env.su
):
return True
raise ValidationError(
_(
"Sorry, you are not allowed to create/change this kind of "
"document. For more information please contact your manager."
)
)
@api.model_create_multi
def create(self, vals_list):
"""Patch for check access rights of user(configurable products)"""
for vals in vals_list:
config_ok = vals.get("config_ok", False)
if config_ok:
self.check_config_user_access()
return super().create(vals_list)
def write(self, vals):
"""Patch for check access rights of user(configurable products)"""
change_config_ok = "config_ok" in vals
configurable_templates = self.filtered(lambda template: template.config_ok)
if change_config_ok or configurable_templates:
self[:1].check_config_user_access()
return super().write(vals)
@api.constrains("config_line_ids")
def _check_config_line_domain(self):
attribute_line_ids = self.attribute_line_ids
tmpl_value_ids = attribute_line_ids._configurator_value_ids()
tmpl_attribute_ids = attribute_line_ids.mapped("attribute_id")
error_message = False
for domain_id in self.config_line_ids.mapped("domain_id"):
domain_attr_ids = domain_id.domain_line_ids.mapped("attribute_id")
domain_value_ids = domain_id.domain_line_ids.mapped("value_ids")
invalid_value_ids = domain_value_ids - tmpl_value_ids
invalid_attribute_ids = domain_attr_ids - tmpl_attribute_ids
if not invalid_attribute_ids and not invalid_value_ids:
continue
if not error_message:
error_message = _(
"Following Attribute/Value from restriction "
"are not present in template attributes/values. "
"Please make sure you are adding right restriction"
)
error_message += _("\nRestriction: %s", domain_id.name)
error_message += (
invalid_attribute_ids
and _(
"\nAttribute/s: %s", ", ".join(invalid_attribute_ids.mapped("name"))
)
or ""
)
error_message += (
invalid_value_ids
and _("\nValue/s: %s\n", ", ".join(invalid_value_ids.mapped("name")))
or ""
)
if error_message:
raise ValidationError(error_message)
class ProductProduct(models.Model):
_inherit = "product.product"
_rec_name = "config_name"
@api.constrains("product_template_attribute_value_ids")
def _check_duplicate_product(self):
"""Check for prducts with same attribute values/custom values"""
for product in self:
if not product.config_ok:
continue
# At the moment, I don't have enough confidence with my
# understanding of binary attributes, so will leave these
# as not matching...
# In theory, they should just work, if they are set to "non search"
# in custom field def!
# TODO: Check the logic with binary attributes
config_session_obj = product.env["product.config.session"]
ptav_ids = product.product_template_attribute_value_ids.mapped(
"product_attribute_value_id"
)
duplicates = config_session_obj.search_variant(
product_tmpl_id=product.product_tmpl_id,
value_ids=ptav_ids.ids,
).filtered(lambda p, product=product: p.id != product.id)
if duplicates:
raise ValidationError(
_(
"Configurable Products cannot have duplicates "
"(identical attribute values)"
)
)
def _get_config_name(self):
"""Name for configured products
:param: return : String"""
self.ensure_one()
return self.name
def _get_mako_context(self, buf):
"""Return context needed for computing product name based
on mako-tamplate define on it's product template"""
self.ensure_one()
ptav_ids = self.product_template_attribute_value_ids.mapped(
"product_attribute_value_id"
)
return Context(
buf,
product=self,
attribute_values=ptav_ids,
steps=self.product_tmpl_id.config_step_line_ids,
template=self.product_tmpl_id,
)
def _get_mako_tmpl_name(self):
"""Compute and return product name based on mako-tamplate
define on it's product template"""
self.ensure_one()
if self.mako_tmpl_name:
try:
mytemplate = Template(self.mako_tmpl_name or "")
buf = StringIO()
ctx = self._get_mako_context(buf)
mytemplate.render_context(ctx)
return buf.getvalue()
except Exception:
_logger.error(
_("Error while calculating mako product name: %s")
% self.display_name
)
return self.display_name
@api.depends("product_template_attribute_value_ids.weight_extra")
def _compute_product_weight_extra(self):
for product in self:
product.weight_extra = sum(
product.mapped("product_template_attribute_value_ids.weight_extra")
)
def _compute_product_weight(self):
for product in self:
if product.config_ok:
tmpl_weight = product.product_tmpl_id.weight
product.weight = tmpl_weight + product.weight_extra
else:
product.weight = product.weight_dummy
def _search_product_weight(self, operator, value):
return [("weight_dummy", operator, value)]
def _inverse_product_weight(self):
"""Store weight in dummy field"""
self.weight_dummy = self.weight
config_name = fields.Char(
string="Configuration Name", compute="_compute_config_name"
)
weight_extra = fields.Float(compute="_compute_product_weight_extra")
weight_dummy = fields.Float(string="Manual Weight", digits="Stock Weight")
weight = fields.Float(
compute="_compute_product_weight",
inverse="_inverse_product_weight",
search="_search_product_weight",
store=False,
)
# product preset
config_preset_ok = fields.Boolean(string="Is Preset")
def _compute_config_name(self):
"""Compute the name of the configurable products and use template
name for others"""
for product in self:
if product.config_ok:
product.config_name = product._get_config_name()
else:
product.config_name = product.name
def reconfigure_product(self):
"""launches a product configurator wizard with a linked
template and variant in order to re-configure an existing product.
It is essentially a shortcut to pre-fill configuration
data of a variant"""
self.ensure_one()
extra_vals = {"product_id": self.id}
return self.product_tmpl_id.create_config_wizard(extra_vals=extra_vals)
@api.model
def check_config_user_access(self, mode):
"""Check user have access to perform action(create/write/delete)
on configurable products"""
if not self.env["product.template"]._check_config_group_rights():
return True
config_manager = self.env.user.has_group(
"product_configurator.group_product_configurator_manager"
)
config_user = self.env.user.has_group(
"product_configurator.group_product_configurator"
)
user_root = self.env.ref("base.user_root")
user_admin = self.env.ref("base.user_admin")
if (
config_manager
or (config_user and mode not in ["delete"])
or self.env.user.id in [user_root.id, user_admin.id]
):
return True
raise ValidationError(
_(
"Sorry, you are not allowed to create/change this kind of "
"document. For more information please contact your manager."
)
)
def unlink(self):
"""- Signal unlink from product variant through context so
removal can be stopped for configurable templates
- check access rights of user(configurable products)"""
config_product = any(p.config_ok for p in self)
if config_product:
self.env["product.product"].check_config_user_access(mode="delete")
ctx = dict(self.env.context, unlink_from_variant=True)
self.env.context = ctx
return super().unlink()
@api.model_create_multi
def create(self, vals_list):
"""Patch for check access rights of user(configurable products)"""
for vals in vals_list:
config_ok = vals.get("config_ok", False)
if config_ok:
self.check_config_user_access(mode="create")
return super().create(vals_list)
def write(self, vals):
"""Patch for check access rights of user(configurable products)"""
change_config_ok = "config_ok" in vals
configurable_products = self.filtered(lambda product: product.config_ok)
if change_config_ok or configurable_products:
self[:1].check_config_user_access(mode="write")
return super().write(vals)
def _compute_product_price_extra(self):
standard_products = self.filtered(lambda product: not product.config_ok)
config_products = self - standard_products
if standard_products:
result = super(
ProductProduct, standard_products
)._compute_product_price_extra()
else:
result = None
for product in config_products:
attribute_value_obj = self.env["product.attribute.value"]
value_ids = (
product.product_template_attribute_value_ids.product_attribute_value_id
)
extra_prices = attribute_value_obj.get_attribute_value_extra_prices(
product_tmpl_id=product.product_tmpl_id.id, pt_attr_value_ids=value_ids
)
product.price_extra = sum(extra_prices.values())
return result

View file

@ -0,0 +1,454 @@
from ast import literal_eval
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ProductAttribute(models.Model):
_inherit = "product.attribute"
_order = "sequence"
def copy(self, default=None):
"""Add ' (Copy)' in name to prevent attribute
having same name while copying"""
if not default:
default = {}
new_attrs = self.env["product.attribute"]
for attr in self:
default.update({"name": attr.name + " (copy)"})
new_attrs += super(ProductAttribute, attr).copy(default)
return new_attrs
@api.model
def _get_nosearch_fields(self):
"""Return a list of custom field types that do not support searching"""
return ["binary"]
@api.onchange("custom_type")
def onchange_custom_type(self):
if self.custom_type in self._get_nosearch_fields():
self.search_ok = False
if self.custom_type not in ("integer", "float"):
self.min_val = False
self.max_val = False
@api.onchange("val_custom")
def onchange_val_custom_field(self):
if not self.val_custom:
self.custom_type = False
CUSTOM_TYPES = [
("char", "Char"),
("integer", "Integer"),
("float", "Float"),
("text", "Textarea"),
("color", "Color"),
("binary", "Attachment"),
("date", "Date"),
("datetime", "DateTime"),
]
active = fields.Boolean(
default=True,
help="By unchecking the active field you can "
"disable a attribute without deleting it",
)
min_val = fields.Integer(string="Min Value", help="Minimum value allowed")
max_val = fields.Integer(string="Max Value", help="Maximum value allowed")
# TODO: Exclude self from result-set of dependency
val_custom = fields.Boolean(
string="Custom Value", help="Allow custom value for this attribute?"
)
custom_type = fields.Selection(
selection=CUSTOM_TYPES,
string="Field Type",
help="The type of the custom field generated in the frontend",
)
description = fields.Text(translate=True)
search_ok = fields.Boolean(
string="Searchable",
help="When checking for variants with "
"the same configuration, do we "
"include this field in the search?",
)
required = fields.Boolean(
default=True,
help="Determines the required value of this "
"attribute though it can be change on "
"the template level",
)
multi = fields.Boolean(
help="Allow selection of multiple values for this attribute?",
)
uom_id = fields.Many2one(comodel_name="uom.uom", string="Unit of Measure")
image = fields.Binary()
# TODO prevent the same attribute from being defined twice on the
# attribute lines
@api.constrains("custom_type", "search_ok")
def check_searchable_field(self):
for attribute in self:
nosearch_fields = attribute._get_nosearch_fields()
if attribute.custom_type in nosearch_fields and attribute.search_ok:
raise ValidationError(
_(
"Selected custom field type '%s' is not searchable",
attribute.custom_type,
)
)
def validate_custom_val(self, val):
"""Pass in a desired custom value and ensure it is valid.
Probably should check type, etc., but let's assume fine for the moment.
"""
self.ensure_one()
if self.custom_type in ("integer", "float"):
minv = self.min_val
maxv = self.max_val
val = literal_eval(str(val))
if minv and maxv and (val < minv or val > maxv):
raise ValidationError(
_(
"Selected custom value '%(name)s' must be "
"between %(min_val)s and %(max_val)s",
**{
"name": self.name,
"min_val": self.min_val,
"max_val": self.max_val,
},
)
)
elif minv and val < minv:
raise ValidationError(
_(
"Selected custom value '%(name)s' must be at least %(min_val)s",
**{"name": self.name, "min_val": self.min_val},
)
)
elif maxv and val > maxv:
raise ValidationError(
_(
"Selected custom value '%(name)s' "
"must be lower than %(max_value)s",
**{"name": self.name, "max_value": self.max_val + 1},
)
)
@api.constrains("min_val", "max_val")
def _check_constraint_min_max_value(self):
"""Prevent to add Maximun value less than minimum value"""
for attribute in self:
if attribute.custom_type not in ("integer", "float"):
continue
minv = attribute.min_val
maxv = attribute.max_val
if maxv and minv and maxv < minv:
raise ValidationError(
_("Maximum value must be greater than Minimum value")
)
def _configurator_value_ids(self):
"""Values accepted for attributes in `self`."""
values = self.value_ids
if any(self.mapped("val_custom")):
values += self.env["product.config.session"].get_custom_value_id()
return values
class ProductAttributeLine(models.Model):
_inherit = "product.template.attribute.line"
_order = "product_tmpl_id, sequence, id"
# TODO: Order by dependencies first and then sequence so dependent fields
# do not come before master field
@api.onchange("attribute_id")
def onchange_attribute(self):
"""Set default value of required/multi/cutom from attribute"""
self.value_ids = False
self.required = self.attribute_id.required
self.multi = self.attribute_id.multi
self.custom = self.attribute_id.val_custom
# TODO: Remove all dependencies pointed towards the attribute being
# changed
@api.onchange("value_ids")
def onchange_values(self):
if self.default_val and self.default_val not in self.value_ids:
self.default_val = None
custom = fields.Boolean(help="Allow custom values for this attribute?")
required = fields.Boolean(help="Is this attribute required?")
multi = fields.Boolean(
help="Allow selection of multiple values for this attribute?",
)
default_val = fields.Many2one(comodel_name="product.attribute.value")
sequence = fields.Integer(default=10)
@api.constrains("value_ids", "default_val")
def _check_default_values(self):
"""default value should not be outside of the
values selected in attribute line"""
for line in self.filtered(lambda line: line.default_val):
if line.default_val not in line.value_ids:
raise ValidationError(
_(
"Default values for each attribute line must exist in "
"the attribute values (%(attr_name)s: %(default_val)s)",
**{
"attr_name": line.attribute_id.name,
"default_val": line.default_val.name,
},
)
)
@api.constrains("active", "value_ids", "attribute_id")
def _check_valid_values(self):
"""Overwrite to save attribute line without
values when custom is true"""
for ptal in self:
# Customization
if ptal.active and not ptal.value_ids and not ptal.custom:
# Old code
# if ptal.active and not ptal.value_ids:
# Customization End
raise ValidationError(
_(
"The attribute %(attr)s must have at least one value for "
"the product %(product)s.",
**{
"attr": ptal.attribute_id.display_name,
"product": ptal.product_tmpl_id.display_name,
},
)
)
for pav in ptal.value_ids:
if pav.attribute_id != ptal.attribute_id:
raise ValidationError(
_(
"On the product %(product)s you cannot associate the "
"value %(value)s with the attribute %(attr)s because they "
"do not match.",
**{
"product": ptal.product_tmpl_id.display_name,
"value": pav.display_name,
"attr": ptal.attribute_id.display_name,
},
)
)
return True
def _configurator_value_ids(self):
"""Values accepted for template attribute lines in `self`."""
values = self.value_ids
if any(self.mapped("custom")):
values += self.env["product.config.session"].get_custom_value_id()
return values
class ProductAttributeValue(models.Model):
_inherit = "product.attribute.value"
def copy(self, default=None):
"""Add ' (Copy)' in name to prevent attribute
having same name while copying"""
if not default:
default = {}
default.update({"name": self.name + " (copy)"})
product = super().copy(default)
return product
active = fields.Boolean(
default=True,
help="By unchecking the active field you can "
"disable a attribute value without deleting it",
)
product_id = fields.Many2one(comodel_name="product.product")
image = fields.Binary(
attachment=True,
help="Attribute value image (Display on website for radio buttons)",
)
@api.model
def get_attribute_value_extra_prices(
self, product_tmpl_id, pt_attr_value_ids, pricelist=None
):
extra_prices = {}
if not pricelist:
pricelist = self.env.user.partner_id.property_product_pricelist
related_product_av_ids = self.env["product.attribute.value"].search(
[("id", "in", pt_attr_value_ids.ids), ("product_id", "!=", False)]
)
extra_prices = {
av.id: av.product_id.with_context(
pricelist=pricelist.id
)._get_contextual_price()
for av in related_product_av_ids
}
remaining_av_ids = pt_attr_value_ids - related_product_av_ids
pe_lines = self.env["product.template.attribute.value"].search(
[
("product_attribute_value_id", "in", remaining_av_ids.ids),
("product_tmpl_id", "=", product_tmpl_id),
]
)
for line in pe_lines:
attr_val_id = line.product_attribute_value_id
if attr_val_id.id not in extra_prices:
extra_prices[attr_val_id.id] = 0
extra_prices[attr_val_id.id] += line.price_extra
return extra_prices
def name_get(self):
res = super().name_get()
if not self.env.context.get("show_price_extra"):
return res
product_template_id = self.env.context.get("active_id", False)
price_precision = self.env["decimal.precision"].precision_get("Product Price")
extra_prices = self.get_attribute_value_extra_prices(
product_tmpl_id=product_template_id, pt_attr_value_ids=self
)
res_prices = []
for val in res:
price_extra = extra_prices.get(val[0])
if price_extra:
val = (
val[0],
"{} ( +{} )".format(
val[1],
("{0:,.%sf}" % (price_precision)).format(price_extra),
),
)
res_prices.append(val)
return res_prices
@api.model
def name_search(self, name="", args=None, operator="ilike", limit=100):
"""Use name_search as a domain restriction for the frontend to show
only values set on the product template taking all the configuration
restrictions into account.
TODO: This only works when activating the selection not when typing
"""
product_tmpl_id = self.env.context.get("_cfg_product_tmpl_id")
if product_tmpl_id:
# TODO: Avoiding browse here could be a good performance enhancer
product_tmpl = self.env["product.template"].browse(product_tmpl_id)
tmpl_vals = product_tmpl.attribute_line_ids.mapped("value_ids")
attr_restrict_ids = []
preset_val_ids = []
new_args = []
for arg in args:
# Restrict values only to value_ids set on product_template
if arg[0] == "id" and arg[1] == "not in":
preset_val_ids = arg[2]
# TODO: Check if all values are available for configuration
else:
new_args.append(arg)
val_ids = set(tmpl_vals.ids)
if preset_val_ids:
val_ids -= set(arg[2])
val_ids = self.env["product.config.session"].values_available(
val_ids, preset_val_ids, product_tmpl_id=product_tmpl_id
)
new_args.append(("id", "in", val_ids))
mono_tmpl_lines = product_tmpl.attribute_line_ids.filtered(
lambda line: not line.multi
)
for line in mono_tmpl_lines:
line_val_ids = set(line.mapped("value_ids").ids)
if line_val_ids & set(preset_val_ids):
attr_restrict_ids.append(line.attribute_id.id)
if attr_restrict_ids:
new_args.append(("attribute_id", "not in", attr_restrict_ids))
args = new_args
res = super().name_search(name=name, args=args, operator=operator, limit=limit)
return res
# TODO: Prevent unlinking custom options by overriding unlink
# _sql_constraints = [
# ('unique_custom', 'unique(id,allow_custom_value)',
# 'Only one custom value per dimension type is allowed')
# ]
class ProductAttributePrice(models.Model):
_inherit = "product.template.attribute.value"
# Leverage product.template.attribute.value to compute the extra weight
# each attribute adds
weight_extra = fields.Float(string="Attribute Weight Extra", digits="Stock Weight")
class ProductAttributeValueLine(models.Model):
_name = "product.attribute.value.line"
_description = "Product Attribute Value Line"
_order = "sequence"
sequence = fields.Integer(default=10)
product_tmpl_id = fields.Many2one(
comodel_name="product.template",
string="Product Template",
ondelete="cascade",
required=True,
)
value_id = fields.Many2one(
comodel_name="product.attribute.value",
required=True,
string="Attribute Value",
)
attribute_id = fields.Many2one(
comodel_name="product.attribute", related="value_id.attribute_id"
)
value_ids = fields.Many2many(
comodel_name="product.attribute.value",
relation="product_attribute_value_product_attribute_value_line_rel",
column1="product_attribute_value_line_id",
column2="product_attribute_value_id",
string="Values Configuration",
)
product_value_ids = fields.Many2many(
comodel_name="product.attribute.value",
relation="product_attr_values_attr_values_rel",
column1="product_val_id",
column2="attr_val_id",
compute="_compute_get_value_id",
store=True,
)
@api.depends(
"product_tmpl_id",
"product_tmpl_id.attribute_line_ids",
"product_tmpl_id.attribute_line_ids.value_ids",
)
def _compute_get_value_id(self):
for attr_val_line in self:
template = attr_val_line.product_tmpl_id
value_list = template.attribute_line_ids.mapped("value_ids")
attr_val_line.product_value_ids = [(6, 0, value_list.ids)]
@api.constrains("value_ids")
def _validate_configuration(self):
"""Ensure that the passed configuration in value_ids is a valid"""
cfg_session_obj = self.env["product.config.session"]
for attr_val_line in self:
value_ids = attr_val_line.value_ids.ids
value_ids.append(attr_val_line.value_id.id)
valid = cfg_session_obj.validate_configuration(
value_ids=value_ids,
product_tmpl_id=attr_val_line.product_tmpl_id.id,
final=False,
)
if not valid:
raise ValidationError(
_(
"Values provided to the attribute value line are "
"incompatible with the current rules"
)
)

View file

@ -0,0 +1,3 @@
* `Aion Tech <https://aiontech.company/>`_:
* Simone Rubino <simone.rubino@aion-tech.it>

View file

@ -0,0 +1,2 @@
This module has all the mechanics to support product configuration. It serves as a base
dependency for configuration interfaces.

View file

@ -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>

View file

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 product_configurator_config_line Config Line model_product_config_line group_product_configurator 1 0 0 0
3 product_configurator_config_image Config Image model_product_config_image group_product_configurator 1 0 0 0
4 product_configurator_config_step Config Step model_product_config_step group_product_configurator 1 0 0 0
5 product_configurator_config_step_line Config Step Line model_product_config_step_line group_product_configurator 1 0 0 0
6 product_configurator_config_domain Config Domain model_product_config_domain group_product_configurator 1 0 0 0
7 product_configurator_config_domain_line Config Domain Line model_product_config_domain_line group_product_configurator 1 0 0 0
8 product_configurator_custom_attribute_value Attribute Value Line model_product_attribute_value_line group_product_configurator 1 0 0 0
9 product_configurator_config_session Config Session model_product_config_session group_product_configurator 1 1 1 1
10 product_configurator_config_session_custom_value Config Session Custom Value model_product_config_session_custom_value group_product_configurator 1 1 1 1
11 user_config_line User Config Line model_product_config_line base.group_user 1 0 0 0
12 user_config_image User Config Image model_product_config_image base.group_user 1 0 0 0
13 user_config_step User Config Step model_product_config_step base.group_user 1 0 0 0
14 user_config_step_line User Config Step Line model_product_config_step_line base.group_user 1 0 0 0
15 user_config_domain_line User Config Domain Line model_product_config_domain_line base.group_user 1 0 0 0
16 user_config_domain User Config Domain model_product_config_domain base.group_user 1 0 0 0
17 user_custom_attribute_value User Attribute Value Line model_product_attribute_value_line base.group_user 1 0 0 0
18 user_config_session User Config Session model_product_config_session base.group_user 1 0 0 0
19 user_config_session_custom_value User Config Session Custom Value model_product_config_session_custom_value base.group_user 1 0 0 0
20 portal_config_image Portal Config Image model_product_config_image base.group_portal 1 0 0 0
21 portal_config_step Portal Config Step model_product_config_step base.group_portal 1 0 0 0
22 portal_config_session Portal Config Session model_product_config_session base.group_portal 1 0 0 0
23 portal_config_session_custom_value Portal Config Session Custom Value model_product_config_session_custom_value base.group_portal 1 0 0 0
24 portal_configurator_config_line Portal Config Line model_product_config_line base.group_portal 1 0 0 0
25 portal_configurator_config_step_line Portal Config Step Line model_product_config_step_line base.group_portal 1 0 0 0
26 portal_configurator_config_domain Portal Config Domain model_product_config_domain base.group_portal 1 0 0 0
27 portal_configurator_config_domain_line Portal Config Domain Line model_product_config_domain_line base.group_portal 1 0 0 0
28 product_configurator_config_line_manager Config Line Manager product_configurator.model_product_config_line product_configurator.group_product_configurator_manager 1 1 1 1
29 product_configurator_config_image_manager Config Image Manager product_configurator.model_product_config_image product_configurator.group_product_configurator_manager 1 1 1 1
30 product_configurator_config_step_manager Config Step Manager product_configurator.model_product_config_step product_configurator.group_product_configurator_manager 1 1 1 1
31 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
32 product_configurator_config_domain_manager Config Domain Manager product_configurator.model_product_config_domain product_configurator.group_product_configurator_manager 1 1 1 1
33 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
34 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
35 access_product_template_product_config_user product.template Product Config user product.model_product_template product_configurator.group_product_configurator 1 0 0 0
36 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
37 access_product_product_product_config_user product.product Product Config user product.model_product_product product_configurator.group_product_configurator 1 0 0 0
38 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
39 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
40 access_product_configurator_group product_configurator model_product_configurator product_configurator.group_product_configurator 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -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);

View file

@ -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>

View file

@ -0,0 +1,14 @@
/** @odoo-module **/
import {FormController} from "@web/views/form/form_controller";
import {patch} from "@web/core/utils/patch";
patch(FormController.prototype, "Manage special=no_save", {
async beforeExecuteActionButton(clickParams) {
if (clickParams.special === "no_save") {
delete clickParams.special;
return true;
}
return this._super(...arguments);
},
});

View file

@ -0,0 +1,71 @@
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
);
}
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*/
});
});

View file

@ -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;
},
});
});

View file

@ -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;
}

View file

@ -0,0 +1,6 @@
from . import test_create
from . import test_configuration_rules
from . import test_product
from . import test_product_attribute
from . import test_product_config
from . import test_wizard

View file

@ -0,0 +1,116 @@
from odoo.addons.base.tests.common import BaseCommon
class ProductConfiguratorTestCases(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ProductConfWizard = cls.env["product.configurator"]
cls.config_product = cls.env.ref("product_configurator.bmw_2_series")
cls.product_category = cls.env.ref("product.product_category_5")
# attributes
cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel")
cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine")
cls.attr_color = cls.env.ref("product_configurator.product_attribute_color")
cls.attr_rims = cls.env.ref("product_configurator.product_attribute_rims")
cls.attr_model_line = cls.env.ref(
"product_configurator.product_attribute_model_line"
)
cls.attr_tapistry = cls.env.ref(
"product_configurator.product_attribute_tapistry"
)
cls.attr_transmission = cls.env.ref(
"product_configurator.product_attribute_transmission"
)
cls.attr_options = cls.env.ref("product_configurator.product_attribute_options")
# values
cls.value_gasoline = cls.env.ref(
"product_configurator.product_attribute_value_gasoline"
)
cls.value_218i = cls.env.ref(
"product_configurator.product_attribute_value_218i"
)
cls.value_220i = cls.env.ref(
"product_configurator.product_attribute_value_220i"
)
cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red")
cls.value_rims_378 = cls.env.ref(
"product_configurator.product_attribute_value_rims_378"
)
cls.value_sport_line = cls.env.ref(
"product_configurator.product_attribute_value_sport_line"
)
cls.value_model_sport_line = cls.env.ref(
"product_configurator.product_attribute_value_model_sport_line"
)
cls.value_tapistry = cls.env.ref(
"product_configurator.product_attribute_value_tapistry" + "_oyster_black"
)
cls.value_transmission = cls.env.ref(
"product_configurator.product_attribute_value_steptronic"
)
cls.value_options_1 = cls.env.ref(
"product_configurator.product_attribute_value_smoker_package"
)
cls.value_options_2 = cls.env.ref(
"product_configurator.product_attribute_value_sunroof"
)
@classmethod
def _configure_product_nxt_step(cls):
product_config_wizard = cls.ProductConfWizard.create(
{
"product_tmpl_id": cls.config_product.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_fuel.id}": cls.value_gasoline.id,
f"__attribute_{cls.attr_engine.id}": cls.value_218i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_color.id}": cls.value_red.id,
f"__attribute_{cls.attr_rims.id}": cls.value_rims_378.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_model_line.id}": cls.value_sport_line.id,
}
)
product_config_wizard.action_previous_step()
product_config_wizard.action_previous_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_engine.id}": cls.value_220i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.action_next_step()
vals = {
f"__attribute_{cls.attr_model_line.id}": cls.value_model_sport_line.id,
}
product_config_wizard.write(vals)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_tapistry.id}": cls.value_tapistry.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{cls.attr_transmission.id}": cls.value_transmission.id,
f"__attribute_{cls.attr_options.id}": [
[6, 0, [cls.value_options_2.id]]
],
}
)
return product_config_wizard.action_next_step()

View file

@ -0,0 +1,324 @@
# Copyright 2024 Simone Rubino - Aion Tech
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, Command
from odoo.exceptions import ValidationError
from odoo.fields import first
from odoo.tests.common import Form, TransactionCase
from odoo.tools.safe_eval import safe_eval
class ConfigurationRules(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# The product attribute view only shows configuration fields
# (such as `val_custom`)
# when called with a specific context
# that is set by this action
configuration_attributes_action = cls.env.ref(
"product_configurator.action_attributes_view"
)
action_eval_context = configuration_attributes_action._get_eval_context()
configuration_attribute_context = safe_eval(
configuration_attributes_action.context, globals_dict=action_eval_context
)
configuration_attribute_model = cls.env["product.attribute"].with_context(
**configuration_attribute_context
)
cls.generic_custom_attribute_value = cls.env.ref(
"product_configurator.custom_attribute_value"
)
custom_attribute_form = Form(configuration_attribute_model)
custom_attribute_form.name = "Test custom attribute"
with custom_attribute_form.value_ids.new() as value:
value.name = "Test custom value"
custom_attribute_form.val_custom = True
cls.custom_attribute = custom_attribute_form.save()
cls.custom_attribute_value = cls.custom_attribute.value_ids
other_custom_attribute_form = Form(configuration_attribute_model)
other_custom_attribute_form.name = "Test other custom attribute"
other_custom_attribute_form.val_custom = True
with other_custom_attribute_form.value_ids.new() as value:
value.name = "Test other custom value"
cls.other_custom_attribute = other_custom_attribute_form.save()
cls.other_custom_attribute_value = cls.other_custom_attribute.value_ids
regular_attribute_form = Form(configuration_attribute_model)
regular_attribute_form.name = "Test regular attribute"
regular_attribute_form.val_custom = False
with regular_attribute_form.value_ids.new() as value:
value.name = "Test value 1"
with regular_attribute_form.value_ids.new() as value:
value.name = "Test value 2"
cls.regular_attribute = regular_attribute_form.save()
cls.regular_attribute_value_1 = first(cls.regular_attribute.value_ids)
cls.regular_attribute_value_2 = (
cls.regular_attribute.value_ids - cls.regular_attribute_value_1
)
config_domain_form = Form(cls.env["product.config.domain"])
config_domain_form.name = "Regular attribute has value 1"
with config_domain_form.domain_line_ids.new() as line:
line.attribute_id = cls.regular_attribute
line.condition = "in"
line.value_ids.add(cls.regular_attribute_value_1)
regular_has_value_1_domain = config_domain_form.save()
product_template_form = Form(cls.env["product.template"])
product_template_form.name = "Test configurable product"
with product_template_form.attribute_line_ids.new() as regular_line:
regular_line.attribute_id = cls.regular_attribute
for attribute_value in cls.regular_attribute.value_ids:
regular_line.value_ids.add(attribute_value)
with product_template_form.attribute_line_ids.new() as custom_line:
custom_line.attribute_id = cls.custom_attribute
for attribute_value in cls.custom_attribute.value_ids:
custom_line.value_ids.add(attribute_value)
with product_template_form.attribute_line_ids.new() as other_custom_line:
other_custom_line.attribute_id = cls.other_custom_attribute
for attribute_value in cls.other_custom_attribute.value_ids:
other_custom_line.value_ids.add(attribute_value)
product_template = product_template_form.save()
product_template.config_ok = True
# When the regular attribute has value 1,
# the custom attribute must have the generic custom value.
# The other custom attribute id not restricted.
with Form(product_template) as product_template_form:
with product_template_form.config_line_ids.new() as restriction:
restriction.attribute_line_id = (
product_template.attribute_line_ids.filtered(
lambda al: al.attribute_id == cls.custom_attribute
)
)
restriction.value_ids.add(cls.generic_custom_attribute_value)
restriction.domain_id = regular_has_value_1_domain
cls.product_template = product_template
def setUp(self):
super().setUp()
self.cfg_tmpl = self.env.ref("product_configurator.bmw_2_series")
self.cfg_session = self.env["product.config.session"].create(
{"product_tmpl_id": self.cfg_tmpl.id, "user_id": SUPERUSER_ID}
)
attribute_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids")
self.attr_vals = self.cfg_tmpl.attribute_line_ids.mapped("value_ids")
self.attr_val_ext_ids = {
v: k for k, v in attribute_vals.get_external_id().items()
}
def get_attr_val_ids(self, ext_ids):
"""Return a list of database ids using the external_ids
passed via ext_ids argument"""
value_ids = []
attr_val_prefix = "product_configurator.product_attribute_value_%s"
for ext_id in ext_ids:
if ext_id in self.attr_val_ext_ids:
value_ids.append(self.attr_val_ext_ids[ext_id])
elif attr_val_prefix % ext_id in self.attr_val_ext_ids:
value_ids.append(self.attr_val_ext_ids[attr_val_prefix % ext_id])
return value_ids
def test_valid_configuration(self):
"""Test validation of a valid configuration"""
conf = [
"gasoline",
"228i",
"model_luxury_line",
"silver",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
attr_val_ids = self.get_attr_val_ids(conf)
validation = self.cfg_session.validate_configuration(attr_val_ids)
self.assertTrue(validation, "Valid configuration failed validation")
def test_invalid_configuration(self):
conf = [
"diesel",
"228i",
"model_luxury_line",
"silver",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
attr_val_ids = self.get_attr_val_ids(conf)
with self.assertRaises(ValidationError):
self.cfg_session.validate_configuration(attr_val_ids)
def test_missing_val_configuration(self):
conf = [
"diesel",
"228i",
"model_luxury_line",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
attr_val_ids = self.get_attr_val_ids(conf)
with self.assertRaises(ValidationError):
self.cfg_session.validate_configuration(attr_val_ids)
def test_invalid_multi_configuration(self):
conf = [
"gasoline",
"228i",
"model_luxury_line",
"silver",
"red",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
attr_val_ids = self.get_attr_val_ids(conf)
with self.assertRaises(ValidationError):
self.cfg_session.validate_configuration(attr_val_ids)
def test_invalid_custom_value_configuration(self):
conf = [
"gasoline",
"228i",
"model_luxury_line",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
attr_color_id = self.env.ref("product_configurator.product_attribute_color")
custom_vals = {attr_color_id: {"value": "#fefefe"}}
attr_val_ids = self.get_attr_val_ids(conf)
with self.assertRaises(ValidationError):
self.cfg_session.validate_configuration(attr_val_ids, custom_vals)
def test_filled_custom_value(self):
"""When custom values are restricted,
filling them correctly creates a valid configuration."""
# Arrange
generic_custom_attribute_value = self.generic_custom_attribute_value
custom_attribute = self.custom_attribute
custom_value = 5
other_custom_attribute = self.other_custom_attribute
other_custom_attribute_value = self.other_custom_attribute_value
regular_attribute = self.regular_attribute
regular_attribute_value_1 = self.regular_attribute_value_1
product_template = self.product_template
wizard_action = product_template.configure_product()
wizard = self.env[wizard_action["res_model"]].browse(wizard_action["res_id"])
wizard.action_next_step()
fields_prefixes = wizard._prefixes
field_prefix = fields_prefixes.get("field_prefix")
custom_field_prefix = fields_prefixes.get("custom_field_prefix")
# Regular attribute has value 1
# so the custom attribute must have the generic custom value.
# The other custom attribute can have any value.
wizard.write(
{
field_prefix + str(regular_attribute.id): regular_attribute_value_1.id,
field_prefix
+ str(custom_attribute.id): generic_custom_attribute_value.id,
custom_field_prefix + str(custom_attribute.id): custom_value,
field_prefix
+ str(other_custom_attribute.id): other_custom_attribute_value.id,
}
)
# pre-condition
self.assertEqual(wizard.state, "configure")
# Act
wizard.action_config_done()
# Assert
config = wizard.config_session_id
self.assertEqual(config.state, "done")
def test_fill_restricted_custom_value(self):
"""When custom values are restricted,
filling them with the wrong value creates an invalid configuration."""
# Arrange
generic_custom_attribute_value = self.generic_custom_attribute_value
custom_attribute = self.custom_attribute
custom_value = 5
other_custom_attribute = self.other_custom_attribute
other_custom_attribute_value = self.other_custom_attribute_value
regular_attribute = self.regular_attribute
regular_attribute_value_2 = self.regular_attribute_value_2
product_template = self.product_template
wizard_action = product_template.configure_product()
wizard = self.env[wizard_action["res_model"]].browse(wizard_action["res_id"])
wizard.action_next_step()
fields_prefixes = wizard._prefixes
field_prefix = fields_prefixes.get("field_prefix")
custom_field_prefix = fields_prefixes.get("custom_field_prefix")
# Regular attribute has value 2
# so the custom attribute cannot have the generic custom value.
# The other custom attribute can have any value.
regular_attribute_field_name = field_prefix + str(regular_attribute.id)
custom_attribute_field_name = field_prefix + str(custom_attribute.id)
other_custom_attribute_field_name = field_prefix + str(
other_custom_attribute.id
)
wizard_values = {
regular_attribute_field_name: regular_attribute_value_2.id,
custom_attribute_field_name: generic_custom_attribute_value.id,
custom_field_prefix + str(custom_attribute.id): custom_value,
other_custom_attribute_field_name: other_custom_attribute_value.id,
}
# Act
onchange_result = wizard.onchange(
{
"value_ids": [
Command.set([wizard_values[regular_attribute_field_name]]),
],
**{wiz_field: False for wiz_field in wizard_values.keys()},
},
regular_attribute_field_name,
{
regular_attribute_field_name: "1",
},
)
# Assert
domains = onchange_result["domain"]
custom_attribute_domain = domains[custom_attribute_field_name]
self.assertNotIn(
generic_custom_attribute_value,
self.env["product.attribute.value"].search(custom_attribute_domain),
)
other_custom_attribute_domain = domains[other_custom_attribute_field_name]
self.assertIn(
generic_custom_attribute_value,
self.env["product.attribute.value"].search(other_custom_attribute_domain),
)

View file

@ -0,0 +1,184 @@
from odoo.addons.base.tests.common import BaseCommon
class ConfigurationCreate(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ProductConfWizard = cls.env["product.configurator"]
cls.config_product = cls.env.ref("product_configurator.bmw_2_series")
cls.product_category = cls.env.ref("product.product_category_5")
# attributes
cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel")
cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine")
cls.attr_color = cls.env.ref("product_configurator.product_attribute_color")
cls.attr_rims = cls.env.ref("product_configurator.product_attribute_rims")
cls.attr_model_line = cls.env.ref(
"product_configurator.product_attribute_model_line"
)
cls.attr_tapistry = cls.env.ref(
"product_configurator.product_attribute_tapistry"
)
cls.attr_transmission = cls.env.ref(
"product_configurator.product_attribute_transmission"
)
cls.attr_options = cls.env.ref("product_configurator.product_attribute_options")
# values
cls.value_gasoline = cls.env.ref(
"product_configurator.product_attribute_value_gasoline"
)
cls.value_218i = cls.env.ref(
"product_configurator.product_attribute_value_218i"
)
cls.value_220i = cls.env.ref(
"product_configurator.product_attribute_value_220i"
)
cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red")
cls.value_rims_378 = cls.env.ref(
"product_configurator.product_attribute_value_rims_378"
)
cls.value_sport_line = cls.env.ref(
"product_configurator.product_attribute_value_sport_line"
)
cls.value_model_sport_line = cls.env.ref(
"product_configurator.product_attribute_value_model_sport_line"
)
cls.value_tapistry = cls.env.ref(
"product_configurator.product_attribute_value_tapistry" + "_oyster_black"
)
cls.value_transmission = cls.env.ref(
"product_configurator.product_attribute_value_steptronic"
)
cls.value_options_1 = cls.env.ref(
"product_configurator.product_attribute_value_smoker_package"
)
cls.value_options_2 = cls.env.ref(
"product_configurator.product_attribute_value_sunroof"
)
def test_01_create(self):
"""Test configuration item does not make variations"""
attr_test = self.env["product.attribute"].create(
{
"name": "Test",
"value_ids": [
(0, 0, {"name": "1"}),
(0, 0, {"name": "2"}),
],
}
)
test_template = self.env["product.template"].create(
{
"name": "Test Configuration",
"config_ok": True,
"type": "consu",
"categ_id": self.product_category.id,
"attribute_line_ids": [
(
0,
0,
{
"attribute_id": attr_test.id,
"value_ids": [
(6, 0, attr_test.value_ids.ids),
],
"required": True,
},
),
],
}
)
self.assertEqual(
len(test_template.product_variant_ids),
0,
"Create should not have any variants",
)
def test_02_previous_step_incompatible_changes(self):
"""Test changes in previous steps which would makes
values in next configuration steps invalid"""
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.config_product.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
f"__attribute_{self.attr_rims.id}": self.value_rims_378.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id,
}
)
product_config_wizard.action_previous_step()
product_config_wizard.action_previous_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_engine.id}": self.value_220i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.action_next_step()
vals = {
f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id,
}
product_config_wizard.write(vals)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_transmission.id}": self.value_transmission.id,
f"__attribute_{self.attr_options.id}": [
[6, 0, [self.value_options_1.id, self.value_options_2.id]]
],
}
)
product_config_wizard.action_next_step()
value_ids = ( # noqa
self.value_gasoline
+ self.value_220i
+ self.value_red
+ self.value_rims_378
+ self.value_model_sport_line
+ self.value_tapistry
+ self.value_transmission
+ self.value_options_1
+ self.value_options_2
)
# FIXME: broken as
# """
# AttributeError: 'product.product' object
# has no attribute 'attribute_value_ids'.
# Did you mean: 'attribute_line_ids'?
# """
# new_variant = self.config_product.product_variant_ids.filtered(
# lambda variant: variant.attribute_value_ids == value_ids
# )
# self.assertNotEqual(
# new_variant.id,
# False,
# "Variant not generated at the end of the configuration process",
# )

View file

@ -0,0 +1,705 @@
from odoo.exceptions import ValidationError
from ..tests.common import ProductConfiguratorTestCases
# FIXME: many tests here do not have any assertions.
# They simply run something and expect it to not raise an exception.
# This is not a good practice. Tests should have assertions.
class TestProduct(ProductConfiguratorTestCases):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.productTemplate = cls.env["product.template"]
cls.productAttributeLine = cls.env["product.template.attribute.line"]
cls.productConfigStepLine = cls.env["product.config.step.line"]
cls.product_category = cls.env.ref("product.product_category_5")
cls.attributelinefuel = cls.env.ref(
"product_configurator.product_attribute_line_2_series_fuel"
)
cls.attributelineengine = cls.env.ref(
"product_configurator.product_attribute_line_2_series_engine"
)
cls.value_diesel = cls.env.ref(
"product_configurator.product_attribute_value_diesel"
)
cls.value_218d = cls.env.ref(
"product_configurator.product_attribute_value_218d"
)
cls.value_220d = cls.env.ref(
"product_configurator.product_attribute_value_220d"
)
cls.value_silver = cls.env.ref(
"product_configurator.product_attribute_value_silver"
)
cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine")
cls.config_step_body = cls.env.ref("product_configurator.config_step_body")
cls.product_tmpl_id = cls.env["product.template"].create(
{
"name": "Test Configuration",
"config_ok": True,
"type": "consu",
"categ_id": cls.product_category.id,
}
)
# create attribute line 1
cls.attributeLine1 = cls.productAttributeLine.create(
{
"product_tmpl_id": cls.product_tmpl_id.id,
"attribute_id": cls.attr_fuel.id,
"value_ids": [(6, 0, [cls.value_gasoline.id, cls.value_diesel.id])],
"required": True,
}
)
# create attribute line 2
cls.attributeLine2 = cls.productAttributeLine.create(
{
"product_tmpl_id": cls.product_tmpl_id.id,
"attribute_id": cls.attr_engine.id,
"value_ids": [
(
6,
0,
[
cls.value_218i.id,
cls.value_220i.id,
cls.value_218d.id,
cls.value_220d.id,
],
)
],
"required": True,
}
)
# create attribute line 3
cls.attributeLine3 = cls.productAttributeLine.create(
{
"product_tmpl_id": cls.product_tmpl_id.id,
"attribute_id": cls.attr_color.id,
"value_ids": [(6, 0, [cls.value_red.id, cls.value_silver.id])],
"required": True,
}
)
def _get_product_id(self):
self._configure_product_nxt_step()
return self.config_product.product_variant_ids
def test_00__compute_template_attr_vals(self):
value_ids = self.product_tmpl_id.attribute_line_ids.mapped("value_ids")
self.product_tmpl_id._compute_template_attr_vals()
self.assertEqual(
value_ids,
self.product_tmpl_id.attribute_line_val_ids,
"Error: if value are different\
Method: _compute_template_attr_vals() ",
)
def test_01_set_weight(self):
self.product_tmpl_id.weight = 120
self.product_tmpl_id._set_weight()
self.assertEqual(
self.product_tmpl_id.weight,
self.product_tmpl_id.weight_dummy,
"Error: If set diffrent value for dummy_weight\
Method: _set_weight()",
)
self.product_tmpl_id.config_ok = False
set_weight = self.product_tmpl_id._set_weight()
self.assertIsNone(
set_weight,
"Error: If Value none\
Method: _set_weight()",
)
def test_02_compute_weight(self):
self.product_tmpl_id.weight_dummy = 50.0
self.product_tmpl_id._compute_weight()
self.assertEqual(
self.product_tmpl_id.weight_dummy,
self.product_tmpl_id.weight,
"Error: If set diffrent value for weight\
Method: _compute_weight()",
)
def test_03_toggle_config(self):
configFalse = self.product_tmpl_id.toggle_config()
self.assertFalse(
configFalse,
"Error: If Boolean False\
Method: toggle_config()",
)
self.product_tmpl_id.toggle_config()
varient_value = self.product_tmpl_id._create_variant_ids()
self.assertIsNone(
varient_value,
"Error: If its return none\
Method: create_variant_ids()",
)
def test_04_unlink(self):
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
product_config_wizard.action_next_step()
config_session_id = self.env["product.config.session"].search(
[("product_tmpl_id", "=", self.product_tmpl_id.id)]
)
config_session_id.unlink()
varientId = self.product_tmpl_id.product_variant_ids
boolValue = varientId.unlink()
self.assertTrue(
boolValue,
"Error: if record are not unlink\
Method: unlink()",
)
def test_05_check_default_values(self):
self.attributelinefuel.default_val = (self.value_gasoline.id,)
self.attributelineengine.default_val = self.value_218d.id
with self.assertRaises(ValidationError):
self.config_product._check_default_values()
def test_06_configure_product(self):
# configure product
self.product_tmpl_id.configure_product()
self.ProductConfWizard.action_next_step()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
wizard_action = product_config_wizard.action_next_step()
varient_id = wizard_action.get("res_id")
self.assertEqual(
varient_id,
self.product_tmpl_id.product_variant_ids.id,
"Error: If get diffrent varient Id\
Method: action_next_step()",
)
product_config_wizard.action_previous_step()
self.assertEqual(
product_config_wizard.state,
"select",
"Error: If get diffrent State\
Method: action_previous_step()",
)
# create config_step_line 1
self.configStepLine1 = self.productConfigStepLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"config_step_id": self.config_step_engine.id,
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
# create config_step_line 2
self.configStepLine2 = self.productConfigStepLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"config_step_id": self.config_step_body.id,
"attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
}
)
self.product_tmpl_id.write(
{
"config_step_line_ids": [
(6, 0, [self.configStepLine1.id, self.configStepLine2.id])
],
}
)
# configure product
self.product_tmpl_id.configure_product()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
product_config_wizard.action_previous_step()
self.assertEqual(
product_config_wizard.state,
str(self.configStepLine1.id),
"Error: If diffrent previous state and config state\
Method: action_previous_step()",
)
product_config_wizard.action_next_step()
self.assertEqual(
product_config_wizard.config_session_id.config_step,
product_config_wizard.state,
"Error: If diffrent state and config_step\
Method: action_previous_step()",
)
product_config_wizard.action_next_step()
def test_07_get_mako_tmpl_name(self):
# check for product_product
product_product = self._get_product_id()
mako_tmpl_vals = product_product._get_mako_tmpl_name()
self.assertEqual(
mako_tmpl_vals,
product_product.display_name,
"Error: If get display_name are different\
Method: _get_mako_tmpl_name()",
)
self.config_product.write({"mako_tmpl_name": "Test Configuration Product"})
mako_tmpl_vals = product_product._get_mako_tmpl_name()
self.assertEqual(
self.config_product.mako_tmpl_name,
mako_tmpl_vals,
"Error: If Mako Template are not exists or different\
Method: _get_mako_tmpl_name()",
)
def test_08_compute_product_weight(self):
product_product = self._get_product_id()
self.config_product.weight = 10
product_product.weight_extra = 20
product_product._compute_product_weight()
self.assertEqual(
product_product.weight,
30,
"Error: If value are not get 30\
Method: _compute_product_weight()",
)
product_product.config_ok = False
product_product.weight_dummy = 50
product_product._compute_product_weight()
self.assertEqual(
product_product.weight,
50,
"Error: If value are not get 50\
Method: _compute_product_weight()",
)
def test_09_compute_config_name(self):
product_product = self._get_product_id()
product_product.config_ok = False
product_product._compute_config_name()
self.assertEqual(
product_product.config_name,
"2 Series",
"Error: If different product config_name\
Method: _compute_config_name()",
)
product_product.config_ok = True
product_product._compute_config_name()
self.assertEqual(
product_product.config_name,
"2 Series",
"Error: If different product config_name\
Method: _compute_config_name()",
)
def test_10_reconfigure_product(self):
self.product_tmpl_id.configure_product()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
product_config_wizard.action_next_step()
# reconfigure product
product_product = self.product_tmpl_id.product_variant_ids
product_product.reconfigure_product()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218d.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_color.id}": self.value_silver.id,
}
)
product_config_wizard.action_next_step()
value_ids = self.value_gasoline + self.value_218d + self.value_silver
# val_ids = self.value_gasoline + self.value_218i + self.value_red
# pta_val_ids = self.env["product.template.attribute.value"].search(
# [
# ("product_tmpl_id", "=", self.product_tmpl_id.id),
# ("product_attribute_value_id", "in", value_ids.ids),
# ]
# )
new_variant = self.product_tmpl_id.product_variant_ids.filtered(
lambda variant: variant.product_template_attribute_value_ids == value_ids
)
self.assertFalse(
new_variant.id,
"Error: if variant id not exists\
Method: reconfigure_product()",
)
def test_11_compute_product_weight_extra(self):
product_id = self.env.ref("product.product_delivery_01")
product_template_attr_value_ids = self.env.ref(
"product.product_4_attribute_1_value_2"
)
product_template_attr_value_ids.write(
{
"weight_extra": 50.0,
}
)
product_id._compute_product_weight_extra()
vals = {"product_template_attribute_value_ids": product_template_attr_value_ids}
product_id.write(vals)
self.assertEqual(
product_template_attr_value_ids.weight_extra,
50.0,
product_id.weight_extra,
)
# _compute_product_weight_extra
product_product = self._get_product_id()
productAttPrice = self.env["product.template.attribute.value"].search(
[
("product_tmpl_id", "=", self.config_product.id),
("product_attribute_value_id", "=", self.value_gasoline.id),
]
)
productAttPrice.weight_extra = 45
product_product._compute_product_weight_extra()
self.assertEqual(
productAttPrice.weight_extra,
product_product.weight_extra,
"Error: If weight_extra not equal\
Method: _compute_product_weight_extra()",
)
def test_12_unlink(self):
product_product = self._get_product_id()
unlinkVals = product_product.unlink()
self.assertTrue(
unlinkVals,
"Error: If unlink record true\
Method: unlink()",
)
def test_13_copy(self):
vals = self.config_product.copy()
self.assertEqual(
vals.name,
"2 Series (copy)",
"Error: If not equal\
Method: copy()",
)
self.assertTrue(
vals.attribute_line_ids,
"Error: If attribute_line_ids not exists\
Method: copy()",
)
def test_14_validate_unique_config(self):
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_gasoline.id,
"value_ids": [(6, 0, [self.value_218i.id])],
},
)
]
}
)
with self.assertRaises(ValidationError):
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_gasoline.id,
"value_ids": [(6, 0, [self.value_218i.id])],
},
)
]
}
)
def test_15_check_attr_value_ids(self):
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_gasoline.id,
"value_ids": [(6, 0, [self.value_gasoline.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_diesel.id,
"value_ids": [(6, 0, [self.value_diesel.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_218i.id,
"value_ids": [(6, 0, [self.value_218i.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_220i.id,
"value_ids": [(6, 0, [self.value_220i.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_218d.id,
"value_ids": [(6, 0, [self.value_218d.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_220d.id,
"value_ids": [(6, 0, [self.value_220d.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_red.id,
"value_ids": [(6, 0, [self.value_red.id])],
},
)
]
}
)
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_silver.id,
"value_ids": [(6, 0, [self.value_silver.id])],
},
)
]
}
)
with self.assertRaises(ValidationError):
self.product_tmpl_id.write(
{
"attribute_value_line_ids": [
(
0,
0,
{
"product_tmpl_id": self.product_tmpl_id.id,
"value_id": self.value_rims_378.id,
"value_ids": [(6, 0, [self.value_rims_378.id])],
},
)
]
}
)
def test_16_check_duplicate_product(self):
self.product_tmpl_id.configure_product()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
product_config_wizard.action_next_step()
val_ids = self.value_gasoline + self.value_218i + self.value_red
pta_val_ids = self.env["product.template.attribute.value"].search(
[
("product_tmpl_id", "=", self.product_tmpl_id.id),
("product_attribute_value_id", "in", val_ids.ids),
]
)
with self.assertRaises(ValidationError):
self.env["product.product"].create(
{
"name": "Test Configuration",
"product_tmpl_id": self.product_tmpl_id.id,
"product_template_attribute_value_ids": [(6, 0, pta_val_ids.ids)],
}
)
def test_17_fields_view_get(self):
product_product = self._get_product_id()
product_product.with_context(default_config_ok=True).get_view()
def test_19_compute_product_variant_count(self):
self.product_tmpl_id = self.env["product.template"].create(
{
"name": "Test Configuration",
"config_ok": True,
"type": "consu",
"categ_id": self.product_category.id,
}
)
product_variant_count = self.product_tmpl_id.product_variant_count
self.assertEqual(
product_variant_count,
1,
"Error: If not equal\
Method: _compute_product_variant_count()",
)
def test_20_get_config_name(self):
product_product = self._get_product_id()
product_product._get_config_name()
self.assertTrue(
product_product.name,
"Error: If value False\
Method: _get_config_name()",
)
def test_21_search_product_weight(self):
product_product = self._get_product_id()
operator = "and"
value = 10
search_product_weight = product_product._search_product_weight(operator, value)
self.assertTrue(
search_product_weight,
"Error: If value False\
Method: _search_product_weight()",
)
def test_22_search_weight(self):
operator = "and"
value = 10
search_weight = self.product_tmpl_id._search_weight(operator, value)
self.assertTrue(
search_weight,
"Error: If value False\
Method: _search_weight()",
)
def test_23_check_config_line_domain(self):
product_config_line = self.env.ref(
"product_configurator.product_config_line_218_lines"
)
with self.assertRaises(ValidationError):
self.env["product.template"].create(
{
"name": "template_test",
"config_line_ids": product_config_line,
}
)

View file

@ -0,0 +1,192 @@
from odoo.exceptions import ValidationError
from odoo.addons.base.tests.common import BaseCommon
# FIXME: many tests here do not have any assertions.
# They simply run something and expect it to not raise an exception.
# This is not a good practice. Tests should have assertions.
class ProductAttributes(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.productAttributeLine = cls.env["product.template.attribute.line"]
cls.ProductAttributeFuel = cls.env.ref(
"product_configurator.product_attribute_fuel"
)
cls.ProductAttributeLineFuel = cls.env.ref(
"product_configurator.product_attribute_line_2_series_fuel"
)
cls.ProductTemplate = cls.env.ref("product_configurator.bmw_2_series")
cls.product_category = cls.env.ref("product.product_category_5")
cls.ProductAttributePrice = cls.env["product.template.attribute.value"]
cls.attr_fuel = cls.env.ref("product_configurator.product_attribute_fuel")
cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine")
cls.value_diesel = cls.env.ref(
"product_configurator.product_attribute_value_diesel"
)
cls.value_218i = cls.env.ref(
"product_configurator.product_attribute_value_218i"
)
cls.value_gasoline = cls.env.ref(
"product_configurator.product_attribute_value_gasoline"
)
cls.ProductAttributeValueFuel = cls.value_gasoline.attribute_id.id
def test_01_onchange_custome_type(self):
self.ProductAttributeFuel.min_val = 20
self.ProductAttributeFuel.max_val = 30
self.ProductAttributeFuel.custom_type = "char"
self.ProductAttributeFuel.onchange_custom_type()
self.assertEqual(self.ProductAttributeFuel.min_val, 0, "Min value is not False")
self.assertEqual(self.ProductAttributeFuel.max_val, 0, "Max value is not False")
self.ProductAttributeFuel.min_val = 20
self.ProductAttributeFuel.max_val = 30
self.ProductAttributeFuel.custom_type = "integer"
self.ProductAttributeFuel.onchange_custom_type()
self.assertEqual(
self.ProductAttributeFuel.min_val,
20,
"Min value is not equal to existing min value",
)
self.assertEqual(
self.ProductAttributeFuel.max_val,
30,
"Max value is not equal to existing max value",
)
self.ProductAttributeFuel.custom_type = "float"
self.ProductAttributeFuel.onchange_custom_type()
self.assertEqual(
self.ProductAttributeFuel.min_val,
20,
"Min value is equal to existing min value \
when type is changed to integer to float",
)
self.assertEqual(
self.ProductAttributeFuel.max_val,
30,
"Max value is equal to existing max value \
when type is changed to integer to float",
)
self.ProductAttributeFuel.custom_type = "binary"
self.ProductAttributeFuel.onchange_custom_type()
self.assertFalse(
self.ProductAttributeFuel.search_ok,
"Error: if search true\
Method: onchange_custom_type()",
)
def test_02_onchange_val_custom(self):
self.ProductAttributeFuel.val_custom = False
self.ProductAttributeFuel.custom_type = "integer"
self.ProductAttributeFuel.onchange_val_custom_field()
self.assertFalse(
self.ProductAttributeFuel.custom_type, "custom_type is not False"
)
def test_03_check_searchable_field(self):
self.ProductAttributeFuel.custom_type = "binary"
with self.assertRaises(ValidationError):
self.ProductAttributeFuel.search_ok = True
def test_04_validate_custom_val(self):
self.ProductAttributeFuel.write({"max_val": 20, "min_val": 10})
self.ProductAttributeFuel.custom_type = "integer"
with self.assertRaises(ValidationError):
self.ProductAttributeFuel.validate_custom_val(5)
self.ProductAttributeFuel.write({"max_val": 0, "min_val": 10})
self.ProductAttributeFuel.custom_type = "integer"
with self.assertRaises(ValidationError):
self.ProductAttributeFuel.validate_custom_val(5)
self.ProductAttributeFuel.write({"min_val": 0, "max_val": 20})
self.ProductAttributeFuel.custom_type = "integer"
with self.assertRaises(ValidationError):
self.ProductAttributeFuel.validate_custom_val(25)
def test_05_check_constraint_min_max_value(self):
self.ProductAttributeFuel.custom_type = "integer"
with self.assertRaises(ValidationError):
self.ProductAttributeFuel.write({"max_val": 10, "min_val": 20})
# FIXME: broken on call `onchange_attribute` method as
# """
# odoo.exceptions.ValidationError:
# The attribute Fuel must have at least one value for the product 2 Series.
#
# def test_06_onchange_attribute(self):
# self.ProductAttributeLineFuel.onchange_attribute()
# self.assertFalse(
# self.ProductAttributeLineFuel.value_ids, "value_ids is not False"
# )
# self.assertTrue(
# self.ProductAttributeLineFuel.required, "required not exsits value"
# )
# self.ProductAttributeLineFuel.multi = True
# self.assertTrue(
# self.ProductAttributeLineFuel.multi, "multi not exsits value"
# )
# self.ProductAttributeLineFuel.custom = True
# self.assertTrue(
# self.ProductAttributeLineFuel.custom, "custom not exsits value"
# )
def test_07_check_default_values(self):
with self.assertRaises(ValidationError):
self.ProductAttributeLineFuel.default_val = self.value_218i.id
def test_08_copy_attribute(self):
copyAttribute = self.ProductAttributeFuel.copy()
self.assertEqual(
copyAttribute.name,
"Fuel (copy)",
"Error: If not copy attribute\
Method: copy()",
)
def test_09_compute_get_value_id(self):
attrvalline = self.env["product.attribute.value.line"].create(
{
"product_tmpl_id": self.ProductTemplate.id,
"value_id": self.value_gasoline.id,
}
)
self.assertTrue(
attrvalline.product_value_ids,
"Error: If product_value_ids not exists\
Method: _compute_get_value_id()",
)
def test_10_validate_configuration(self):
with self.assertRaises(ValidationError):
self.env["product.attribute.value.line"].create(
{
"product_tmpl_id": self.ProductTemplate.id,
"value_id": self.value_diesel.id,
"value_ids": [(6, 0, [self.value_218i.id])],
}
)
def test_11_copy(self):
default = {}
productattribute = self.value_gasoline.copy(default)
self.assertEqual(
productattribute.name,
self.value_gasoline.name + " (copy)",
"Error: If not equal productattribute name\
Method: copy()",
)
def test_12_onchange_values(self):
productattributeline = self.env["product.template.attribute.line"]
productattributeline.onchange_values()
self.assertEqual(
productattributeline.default_val,
productattributeline.value_ids,
"Error: If default_val not exists\
Method: onchange_values()",
)

View file

@ -0,0 +1,733 @@
from odoo.exceptions import UserError, ValidationError
from ..tests.common import ProductConfiguratorTestCases
# FIXME: many tests here do not have any assertions.
# They simply run something and expect it to not raise an exception.
# This is not a good practice. Tests should have assertions.
class ProductConfig(ProductConfiguratorTestCases):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.productConfWizard = cls.env["product.configurator"]
cls.productTemplate = cls.env["product.template"]
cls.productAttribute = cls.env["product.attribute"]
cls.productAttributeVals = cls.env["product.attribute.value"]
cls.productAttributeLine = cls.env["product.template.attribute.line"]
cls.productConfigSession = cls.env["product.config.session"]
cls.productConfigDomain = cls.env["product.config.domain"]
cls.config_product = cls.env.ref("product_configurator.bmw_2_series")
cls.attr_engine = cls.env.ref("product_configurator.product_attribute_engine")
cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine")
cls.config_product_1 = cls.env.ref(
"product_configurator.product_config_line_gasoline_engines"
)
cls.config_product_2 = cls.env.ref(
"product_configurator.2_series_config_step_body"
)
# domain
cls.domain_gasolin = cls.env.ref(
"product_configurator.product_config_domain_gasoline"
)
cls.domain_engine = cls.env.ref(
"product_configurator.product_config_domain_diesel"
)
cls.config_image_red = cls.env.ref("product_configurator.config_image_1")
# value
cls.value_gasoline = cls.env.ref(
"product_configurator.product_attribute_value_gasoline"
)
cls.value_diesel = cls.env.ref(
"product_configurator.product_attribute_value_diesel"
)
cls.value_red = cls.env.ref("product_configurator.product_attribute_value_red")
# config_step
cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine")
cls.attribute_line = cls.env.ref(
"product_configurator.product_attribute_line_2_series_engine"
)
cls.value_silver = cls.env.ref(
"product_configurator.product_attribute_value_silver"
)
cls.value_rims_387 = cls.env.ref(
"product_configurator.product_attribute_value_rims_387"
)
# attribute line
cls.attribute_line_2_series_rims = cls.env.ref(
"product_configurator.product_attribute_line_2_series_rims"
)
cls.attribute_line_2_series_tapistry = cls.env.ref(
"product_configurator.product_attribute_line_2_series_tapistry"
)
cls.attribute_value_tapistry_oyster_black = cls.env.ref(
"product_configurator." + "product_attribute_value_tapistry_oyster_black"
)
cls.attribute_line_2_series_transmission = cls.env.ref(
"product_configurator.product_attribute_line_2_series_transmission"
)
# attribute value
cls.attribute_rims = cls.env.ref("product_configurator.product_attribute_rims")
cls.attribute_tapistry = cls.env.ref(
"product_configurator.product_attribute_tapistry"
)
cls.attribute_transmission = cls.env.ref(
"product_configurator.product_attribute_transmission"
)
# session id
cls.session_id = cls.productConfigSession.create(
{
"product_tmpl_id": cls.config_product.id,
"value_ids": [
(
6,
0,
[
cls.value_gasoline.id,
cls.value_transmission.id,
cls.value_red.id,
],
)
],
"user_id": cls.env.user.id,
}
)
# ir attachment
cls.irAttachement = cls.env["ir.attachment"].create(
{
"name": "Test attachement",
"datas": "bWlncmF0aW9uIHRlc3Q=",
}
)
# configure product
cls._configure_product_nxt_step()
cls.config_session = cls.productConfigSession.search(
[("product_tmpl_id", "=", cls.config_product.id)]
)
# create product template
cls.product_tmpl_id = cls.productTemplate.create({"name": "Coca-Cola"})
# create attribute 1
cls.attribute_1 = cls.productAttribute.create(
{
"name": "Color",
}
)
# create attribute 2
cls.attribute_2 = cls.productAttribute.create(
{
"name": "Flavour",
}
)
# create attribute value 1
cls.attribute_vals_1 = cls.productAttributeVals.create(
{
"name": "Orange",
"attribute_id": cls.attribute_1.id,
}
)
# create attribute value 2
cls.attribute_vals_2 = cls.productAttributeVals.create(
{
"name": "Balck",
"attribute_id": cls.attribute_1.id,
}
)
# create attribute value 3
cls.attribute_vals_3 = cls.productAttributeVals.create(
{
"name": "Coke",
"attribute_id": cls.attribute_2.id,
}
)
# create attribute value 4
cls.attribute_vals_4 = cls.productAttributeVals.create(
{
"name": "Mango",
"attribute_id": cls.attribute_2.id,
}
)
# TODO :: Left to take review of code
def test_00_check_value_attributes(self):
with self.assertRaises(ValidationError):
self.config_product_1.write(
{"value_ids": [(6, 0, [self.value_gasoline.id])]}
)
def test_01_check_config_step(self):
with self.assertRaises(ValidationError):
self.config_product_2.config_step_id = 4
def test_02_get_trans_implied(self):
self.domain_gasolin.write({"implied_ids": [(6, 0, [self.domain_engine.id])]})
trans_implied_ids = self.domain_gasolin.trans_implied_ids.ids
self.assertEqual(
trans_implied_ids[-1],
self.domain_engine.id,
"Error: If value not exists\
Method: _get_trans_implied()",
)
def test_03_check_config_step(self):
with self.assertRaises(ValidationError):
self.env["product.config.step.line"].create(
{
"product_tmpl_id": self.config_product.id,
"config_step_id": self.config_step_engine.id,
"attribute_line_ids": [(6, 0, [self.attribute_line.id])],
}
)
def test_04_compute_cfg_price(self):
# check for _compute_cfg_price
price = self.config_product.list_price
price += self.value_220i.product_id.lst_price
price += self.value_model_sport_line.product_id.lst_price
price += self.value_transmission.product_id.lst_price
price += self.value_options_2.product_id.lst_price
self.assertEqual(
self.session_id.price,
price,
"Error: If different session price and list_price\
Method: _compute_cfg_price",
)
def test_05_get_custom_vals_dict(self):
# check for _get_custom_vals_dict
productConfigSessionCustVals = self.env[
"product.config.session.custom.value"
].create(
{"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id}
)
# check for custom type Int
self.attr_fuel.custom_type = "integer"
productConfigSessionCustVals.update({"value": 154})
checkIntval = self.session_id._get_custom_vals_dict()
attr_id = productConfigSessionCustVals.attribute_id.id
self.assertEqual(
checkIntval.get(attr_id),
154,
"Error: If Not Integer value or False\
Method: _get_custom_vals_dict()",
)
# check for custom type Float
self.attr_fuel.custom_type = "float"
productConfigSessionCustVals.update({"value": 94.5})
checkFloatval = self.session_id._get_custom_vals_dict()
attr_id = productConfigSessionCustVals.attribute_id.id
self.assertEqual(
checkFloatval.get(attr_id),
94.5,
"Error: If Not Float value or False\
Method: _get_custom_vals_dict()",
)
# check for custom type Binary
self.attr_color.custom_type = "binary"
productConfigSessionCustVals1 = self.env[
"product.config.session.custom.value"
].create(
{
"cfg_session_id": self.session_id.id,
"attribute_id": self.attr_color.id,
"attachment_ids": [(6, 0, [self.irAttachement.id])],
}
)
checkBinaryval = self.session_id._get_custom_vals_dict()
attr_id = productConfigSessionCustVals1.attribute_id.id
self.assertEqual(
checkBinaryval.get(attr_id),
productConfigSessionCustVals1.attachment_ids,
"Error: If Not attachement\
Method: _get_custom_vals_dict()",
)
def test_06_compute_config_step_name(self):
self.config_session._compute_config_step_name()
self.assertTrue(
self.config_session.config_step_name,
"Error: If not config step name\
Method: _compute_config_step_name()",
)
self.config_session._compute_config_step_name()
self.assertEqual(
self.config_session.config_step_name,
"Extras",
"Error: If not equal config_step_name and config_step\
Method: _compute_config_step_name()",
)
session = self.productConfigSession.create(
{
"product_tmpl_id": self.config_product.id,
"value_ids": [
(6, 0, [self.value_gasoline.id, self.value_transmission.id])
],
"user_id": self.env.user.id,
}
)
session._compute_config_step_name()
self.assertFalse(
session.config_step_name,
"Error: If config_step_name not False\
Method: _compute_config_step_name()",
)
def test_07_search_variant(self):
with self.assertRaises(ValidationError):
self.env["product.config.session"].search_variant()
# check for search duplicate variant
variant_id = self.config_product.product_variant_ids
checkSearchvarient = self.config_session.search_variant()
self.assertEqual(
checkSearchvarient,
variant_id,
"Error: If Not Equal Variant or False\
Method: search_variant()",
)
def test_08_check_custom_type(self):
# check for check_custom_type
with self.assertRaises(ValidationError):
self.env["product.config.session.custom.value"].create(
{
"attribute_id": self.value_silver.attribute_id.id,
"cfg_session_id": self.config_session.id,
"value": "Test",
"attachment_ids": [(6, 0, [self.irAttachement.id])],
}
)
self.attr_color.custom_type = "binary"
with self.assertRaises(ValidationError):
self.env["product.config.session.custom.value"].create(
{
"attribute_id": self.value_silver.attribute_id.id,
"cfg_session_id": self.config_session.id,
"value": "Test",
"attachment_ids": [(6, 0, [self.irAttachement.id])],
}
)
def test_09_create_get_variant(self):
# configure new product to check for search not dublicate variant
attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_1.id,
"value_ids": [
(6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
],
}
)
# create attribute line 2
attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_2.id,
"value_ids": [
(6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
],
}
)
self.product_tmpl_id.write(
{
"attribute_line_ids": [(6, 0, [attributeLine1.id, attributeLine2.id])],
}
)
self.product_tmpl_id.configure_product()
self.productConfWizard.action_next_step()
product_config_wizard = self.productConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attribute_1.id}": self.attribute_vals_1.id,
f"__attribute_{self.attribute_2.id}": self.attribute_vals_3.id,
}
)
product_config_wizard.action_next_step()
config_session_1 = self.productConfigSession.search(
[("product_tmpl_id", "=", self.product_tmpl_id.id)]
)
createVarientId = config_session_1.create_get_variant()
self.assertEqual(
createVarientId.name,
self.product_tmpl_id.name,
"Error: If Not Equal variant name\
Method: search_variant()",
)
# FIXME: broken when running `attributeLine1.custom = True`
# """
# psycopg2.errors.UniqueViolation:
# duplicate key value violates unique constraint
# "product_product_combination_unique"
# DETAIL: Key (product_tmpl_id, combination_indices)=(81, 459,461)
# already exists.
# attributeLine1.custom = True
# self.env["product.config.session.custom.value"].create(
# {
# "cfg_session_id": config_session_1.id,
# "attribute_id": self.attribute_1.id,
# "value": "Coke",
# }
# )
# config_session_1.create_get_variant()
def test_10_check_value_ids(self):
with self.assertRaises(ValidationError):
self.config_image_red.write(
{"value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])]}
)
def test_11_unique_attribute(self):
with self.assertRaises(ValidationError):
self.env["product.config.session.custom.value"].create(
{
"cfg_session_id": self.config_session.id,
"attribute_id": self.attr_engine.id,
"value": "1234",
}
)
self.env["product.config.session.custom.value"].create(
{
"cfg_session_id": self.config_session.id,
"attribute_id": self.attr_engine.id,
"value": "1234",
}
)
# FIXME: broken at the first create as
# """
# psycopg2.errors.NotNullViolation
# null value in column "attribute_line_id" of
# relation "product_template_attribute_value"
# violates not-null constraint
# DETAIL: Failing row contains ...
# def test_12_get_cfg_weight(self):
# self.env["product.template.attribute.value"].create(
# {
# "product_tmpl_id": self.config_product.id,
# "product_attribute_value_id": self.value_red.id,
# "weight_extra": 20.0,
# }
# )
# self.config_product.weight = 20
# weightVal = self.config_session.get_cfg_weight()
# self.assertEqual(
# weightVal,
# 40.0,
# "Error: If Value are not equal\
# Method: get_cfg_weight()",
# )
# # check for config weight
# self.assertEqual(
# self.config_session.weight,
# 40.0,
# "Error: If config weight are not equal\
# Method: _compute_cfg_weight()",
# )
def test_13_update_session_configuration_value(self):
# configure new product to check for search not dublicate variant
self.custom_vals = self.productConfigSession.get_custom_value_id()
self.attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_1.id,
"value_ids": [
(6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
],
"custom": True,
"required": True,
}
)
# create attribute line 2
self.attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_2.id,
"value_ids": [
(6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
],
"custom": True,
"required": True,
}
)
self.product_tmpl_id.write(
{
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
self.attribute_1.custom_type = "binary"
self.product_tmpl_id.configure_product()
self.productConfWizard.action_next_step()
product_config_wizard = self.productConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attribute_1.id}": self.custom_vals.id,
f"__custom_{self.attribute_1.id}": "Test",
}
)
# FIXME: broken validation at `product_config.create_get_variant`
# """
# odoo.exceptions.ValidationError: Required attribute 'Flavour' is empty
# product_config_wizard.action_next_step()
# FIXME: broken at the first create as
# """
# psycopg2.errors.NotNullViolation
# null value in column "attribute_line_id" of
# relation "product_template_attribute_value"
# violates not-null constraint
# DETAIL: Failing row contains ...
# def test_14_get_cfg_price(self):
# self.env["product.template.attribute.value"].create(
# {
# "product_tmpl_id": self.config_product.id,
# "product_attribute_value_id": self.value_red.id,
# "weight_extra": 20.0,
# "price_extra": 20.0,
# }
# )
# price = self.config_product.list_price
# price += self.value_220i.product_id.lst_price
# price += self.value_model_sport_line.product_id.lst_price
# price += self.value_transmission.product_id.lst_price
# price += self.value_options_2.product_id.lst_price
# price_extra_val = self.session_id.get_cfg_price()
# self.assertEqual(
# price_extra_val,
# price + 20,
# "Error: If not equal price extra\
# Method: get_cfg_price()",
# )
def test_15_get_next_step(self):
self.session_id.get_next_step(state=None)
self.session_id.get_next_step(state="draft")
with self.assertRaises(UserError):
self.productConfigSession.get_next_step(
state="draft", value_ids=False, custom_value_ids=False
)
def test_16_get_all_step_lines(self):
step_line_value_1 = self.productConfigSession.get_all_step_lines()
self.assertFalse(
step_line_value_1,
"Error: If return True\
Method: get_all_step_lines()",
)
step_line_value_2 = self.session_id.get_all_step_lines()
self.assertTrue(
step_line_value_2,
"Error: If return True\
Method: get_all_step_lines()",
)
def test_17_custom_value_validate_configuration(self):
self.custom_vals = self.productConfigSession.get_custom_value_id()
self.attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_1.id,
"value_ids": [
(6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
],
"custom": True,
"required": True,
}
)
# create attribute line 2
self.attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_2.id,
"value_ids": [
(6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
],
"custom": True,
"required": True,
}
)
self.product_tmpl_id.write(
{
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
self.attribute_1.custom_type = "binary"
self.product_tmpl_id.configure_product()
self.productConfWizard.action_next_step()
product_config_wizard = self.productConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attribute_1.id}": self.custom_vals.id,
f"__custom_{self.attribute_1.id}": "Test",
}
)
self.attributeLine1.custom = False
self.attributeLine2.custom = False
with self.assertRaises(ValidationError):
self.product_tmpl_id.configure_product()
def test_18_onchange_attribute(self):
# create domain
self.productConfigDomainId = self.env["product.config.domain"].create(
{"name": "restriction 1"}
)
self.productConfigDomainId.compute_domain()
# create attribute value line 1
self.env["product.config.domain.line"].create(
{
"domain_id": self.productConfigDomainId.id,
"attribute_id": self.attr_fuel.id,
"condition": "in",
"value_ids": [(6, 0, [self.value_gasoline.id])],
"operator": "and",
}
)
self.env["product.config.domain.line"].create(
{
"domain_id": self.productConfigDomainId.id,
"attribute_id": self.attr_color.id,
"condition": "in",
"value_ids": [(6, 0, [self.value_red.id])],
"operator": "and",
}
)
self.attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_1.id,
"value_ids": [
(6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
],
"required": True,
}
)
# create attribute line 2
self.attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attribute_2.id,
"value_ids": [
(6, 0, [self.attribute_vals_3.id, self.attribute_vals_4.id])
],
"required": True,
}
)
self.product_tmpl_id.write(
{
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
self.productConfigDomainId.compute_domain()
# create attribute value line 1
config_line = self.env["product.config.line"].create( # noqa
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_line_id": self.attributeLine1.id,
"value_ids": [
(6, 0, [self.attribute_vals_1.id, self.attribute_vals_2.id])
],
"domain_id": self.productConfigDomainId.id,
}
)
# FIXME: broken as
# """
# psycopg2.errors.NotNullViolation:
# null value in column "domain_id"
# of relation "product_config_line"
# violates not-null constraint
# DETAIL: Failing row contains ...
# with self.assertRaises(ValidationError):
# config_line.onchange_attribute()
# self.assertFalse(
# config_line.value_ids,
# "Error: If value_ids True\
# Method: onchange_attribute()",
# )
def test_19_eval(self):
self.attr_color.custom_type = "binary"
productConfigSessionCustVals1 = self.env[
"product.config.session.custom.value"
].create(
{
"cfg_session_id": self.session_id.id,
"attribute_id": self.attr_color.id,
"attachment_ids": [(6, 0, [self.irAttachement.id])],
}
)
checkBinary = productConfigSessionCustVals1.eval()
self.assertTrue(
checkBinary,
"Error: If value False\
Method: eval()",
)
productConfigSessionCustVals = self.env[
"product.config.session.custom.value"
].create(
{"cfg_session_id": self.session_id.id, "attribute_id": self.attr_fuel.id}
)
self.attr_fuel.custom_type = "integer"
productConfigSessionCustVals.update({"value": 154})
checkIntval = productConfigSessionCustVals.eval()
self.assertEqual(
154,
checkIntval,
"Error: If Value not equal\
Method: eval()",
)
self.attr_fuel.custom_type = "float"
productConfigSessionCustVals.update({"value": 15.4})
checkfloat = productConfigSessionCustVals.eval()
self.assertEqual(
15.4,
checkfloat,
"Error: If Value not equal\
Method: eval()",
)
def test_20_values_available(self):
check_available_val_ids = (
self.value_gasoline + self.value_218i + self.value_sport_line
).ids
product_tmpl_id = self.config_product.id
values_ids = [self.value_diesel.id]
available_value_ids = self.productConfigSession.values_available(
check_available_val_ids, values_ids, {}, product_tmpl_id
)
self.assertNotIn(
self.value_sport_line.id,
available_value_ids,
"Error: If value exists\
Method: values_available()",
)

View file

@ -0,0 +1,602 @@
from odoo.exceptions import UserError
from ..tests.common import ProductConfiguratorTestCases
# FIXME: many tests here do not have any assertions.
# They simply run something and expect it to not raise an exception.
# This is not a good practice. Tests should have assertions.
class ConfigurationWizard(ProductConfiguratorTestCases):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.productTemplate = cls.env["product.template"]
cls.productAttributeLine = cls.env["product.template.attribute.line"]
cls.productConfigStepLine = cls.env["product.config.step.line"]
cls.productConfigSession = cls.env["product.config.session"]
cls.product_category = cls.env.ref("product.product_category_5")
cls.attr_line_fuel = cls.env.ref(
"product_configurator.product_attribute_line_2_series_fuel"
)
cls.attr_line_engine = cls.env.ref(
"product_configurator.product_attribute_line_2_series_engine"
)
cls.value_diesel = cls.env.ref(
"product_configurator.product_attribute_value_diesel"
)
cls.value_218d = cls.env.ref(
"product_configurator.product_attribute_value_218d"
)
cls.value_220d = cls.env.ref(
"product_configurator.product_attribute_value_220d"
)
cls.value_silver = cls.env.ref(
"product_configurator.product_attribute_value_silver"
)
cls.config_step_engine = cls.env.ref("product_configurator.config_step_engine")
cls.config_step_body = cls.env.ref("product_configurator.config_step_body")
cls.product_tmpl_id = cls.env["product.template"].create(
{
"name": "Test Configuration",
"config_ok": True,
"type": "consu",
"categ_id": cls.product_category.id,
}
)
cls.custom_vals = cls.productConfigSession.get_custom_value_id()
cls.cfg_tmpl = cls.env.ref("product_configurator.bmw_2_series")
attribute_vals = cls.cfg_tmpl.attribute_line_ids.mapped("value_ids")
cls.attr_vals = attribute_vals
cls.attr_val_ext_ids = {
v: k for k, v in attribute_vals.get_external_id().items()
}
def _check_wizard_nxt_step(self):
self.ProductConfWizard.action_next_step()
product_config_wizard = self.ProductConfWizard.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
}
)
# create attribute line 1
self.attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attr_fuel.id,
"value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])],
"required": True,
}
)
# create attribute line 2
self.attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attr_engine.id,
"value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])],
"required": True,
}
)
# create attribute line 2
self.attributeLine3 = self.productAttributeLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"attribute_id": self.attr_engine.id,
"value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])],
"required": True,
}
)
# configure product creating config step
self.configStepLine1 = self.productConfigStepLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"config_step_id": self.config_step_engine.id,
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
# create config_step_line 2
self.configStepLine2 = self.productConfigStepLine.create(
{
"product_tmpl_id": self.product_tmpl_id.id,
"config_step_id": self.config_step_body.id,
"attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
}
)
self.product_tmpl_id.write(
{
"config_step_line_ids": [
(6, 0, [self.configStepLine1.id, self.configStepLine2.id])
],
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
}
)
product_config_wizard.action_next_step()
product_config_wizard.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
return product_config_wizard
def test_01_action_previous_step(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard.action_previous_step()
self.assertEqual(
product_config_wizard.state,
str(self.configStepLine1.id),
"Error: If state are not equal\
Method: action_next_step()",
)
product_config_wizard.action_next_step()
self.assertEqual(
product_config_wizard.state,
str(self.configStepLine2.id),
"Error: If state are not equal\
Method: action_next_step()",
)
wizard_action = product_config_wizard.action_next_step()
variant_id2 = wizard_action.get("res_id")
self.assertTrue(
variant_id2,
"Error: If varient not exists\
Method: action_next_step()",
)
def test_02_action_reset(self):
product_config_wizard = self._check_wizard_nxt_step()
action_wizard = product_config_wizard.action_reset()
product_tmpl_id = action_wizard.get("context")
self.assertTrue(
product_tmpl_id.get("default_product_tmpl_id"),
"Error: If product_tmpl_id not exists\
Method: action_reset()",
)
def test_03_compute_attr_lines(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard._compute_attr_lines()
self.assertTrue(
product_config_wizard.attribute_line_ids,
"Error: If atttribute_line_ids not exists\
Method: _compute_attr_lines()",
)
def test_04_get_state_selection(self):
product_config_wizard = self._check_wizard_nxt_step()
config_wiz = product_config_wizard.with_context(
wizard_id=product_config_wizard.id
).get_state_selection()
self.assertTrue(
config_wiz[1:],
"Error: If not config step selection\
Method: get_state_selection()",
)
def test_05_compute_cfg_image(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard._compute_cfg_image()
self.assertFalse(
product_config_wizard.product_img,
"Error: If product_img exists\
Method: _compute_cfg_image()",
)
def test_06_onchange_product_tmpl(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard.write(
{
"product_tmpl_id": self.config_product.id,
}
)
with self.assertRaises(UserError):
product_config_wizard.onchange_product_tmpl()
def test_07_get_onchange_domains(self):
product_config_wizard = self._check_wizard_nxt_step()
conf = [
"gasoline",
"228i",
"model_luxury_line",
"silver",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
values = [
"gasoline",
"228i",
"model_luxury_line",
"silver",
"rims_384",
"tapistry_black",
"steptronic",
"smoker_package",
"tow_hook",
]
product_config_wizard.get_onchange_domains(values, conf)
def test_08_onchange_state(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard._onchange_state()
def test_09_onchange_product_preset(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard._onchange_product_preset()
def test_10_open_step(self):
wizard = self.env["product.configurator"]
step_to_open = wizard.config_session_id.check_and_open_incomplete_step()
wizard.open_step(step_to_open)
# FIXME: broken test
# Fails at `product_config_wizard.attribute_line_ids.update(` as
# """odoo.exceptions.UserError:
# On the product Test Configuration
# you cannot transform the attribute Engine into the attribute 5."""
#
# Also, the test is not very useful. It does not assert anything.
#
# def test_11_onchange(self):
# field_name = ""
# values = {f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id}
# product_config_wizard = self._check_wizard_nxt_step()
# field_prefix = product_config_wizard._prefixes.get("field_prefix")
# field_name = f"{field_prefix}{field_name}"
# specs = product_config_wizard._onchange_spec()
# product_config_wizard.onchange(values, field_name, specs)
#
# product_config_wizard.attribute_line_ids.update(
# {
# "attribute_id": self.attr_fuel.id,
# "custom": True,
# }
# )
# values2 = {
# f"__attribute_{self.attr_fuel.id}": self.custom_vals.id,
# f"__custom_{self.attr_fuel.id}": "Test1",
# }
# product_config_wizard.onchange(values2, field_name, specs)
def test_12_fields_get(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard.fields_get()
product_config_wizard.with_context(
wizard_id=product_config_wizard.id
).fields_get()
# custom value
self.attr_line_fuel.custom = True
self.attr_line_engine.custom = True
product_config_wizard_1 = self.ProductConfWizard.create(
{
"product_tmpl_id": self.config_product.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__custom_{self.attr_fuel.id}": "Test1",
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__custom_{self.attr_engine.id}": "Test2",
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
f"__attribute_{self.attr_rims.id}": self.value_rims_378.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id,
}
)
product_config_wizard_1.action_previous_step()
product_config_wizard_1.action_previous_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_engine.id}": self.value_220i.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.action_next_step()
vals = {
f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id,
}
product_config_wizard_1.write(vals)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_transmission.id}": self.value_transmission.id,
f"__attribute_{self.attr_options.id}": [
[6, 0, [self.value_options_2.id]]
],
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.with_context(
wizard_id=product_config_wizard_1.id
).fields_get()
def test_13_fields_view_get(self):
product_config_wizard = self._check_wizard_nxt_step()
product_config_wizard.fields_view_get()
product_config_wizard.with_context(
wizard_id=product_config_wizard.id
).fields_view_get()
# custom value
# custom value
self.attr_line_fuel.custom = True
self.attr_line_engine.custom = True
product_config_wizard_1 = self.ProductConfWizard.create(
{
"product_tmpl_id": self.config_product.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__custom_{self.attr_fuel.id}": "Test1",
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__custom_{self.attr_engine.id}": "Test2",
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
f"__attribute_{self.attr_rims.id}": self.value_rims_378.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_model_line.id}": self.value_sport_line.id,
}
)
product_config_wizard_1.action_previous_step()
product_config_wizard_1.action_previous_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_engine.id}": self.value_220i.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.action_next_step()
vals = {
f"__attribute_{self.attr_model_line.id}": self.value_model_sport_line.id,
}
product_config_wizard_1.write(vals)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_tapistry.id}": self.value_tapistry.id,
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_transmission.id}": self.value_transmission.id,
f"__attribute_{self.attr_options.id}": [
[6, 0, [self.value_options_2.id]]
],
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.with_context(
wizard_id=product_config_wizard_1.id
).fields_view_get()
def test_14_unlink(self):
product_config_wizard = self._check_wizard_nxt_step()
unlinkWizard = product_config_wizard.unlink()
self.assertTrue(
unlinkWizard,
"Error: If not unlink record\
Method: unlink()",
)
def test_15_read(self):
product_config_wizard = self._check_wizard_nxt_step()
values = {
f"__attribute_{self.attr_fuel.id}": self.value_gasoline.id,
f"__attribute_{self.attr_engine.id}": self.value_218i.id,
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
product_config_wizard.read(values)
product_tmpl = self.env["product.template"].create(
{
"name": "Test Custom",
"config_ok": True,
"type": "consu",
"categ_id": self.product_category.id,
}
)
self.ProductConfWizard.action_next_step()
product_config_wizard_1 = self.ProductConfWizard.create(
{
"product_tmpl_id": product_tmpl.id,
}
)
# create attribute line 1
self.attributeLine1 = self.productAttributeLine.create(
{
"product_tmpl_id": product_tmpl.id,
"attribute_id": self.attr_fuel.id,
"value_ids": [(6, 0, [self.value_gasoline.id, self.value_diesel.id])],
"required": True,
"custom": True,
}
)
# create attribute line 2
self.attributeLine2 = self.productAttributeLine.create(
{
"product_tmpl_id": product_tmpl.id,
"attribute_id": self.attr_engine.id,
"value_ids": [(6, 0, [self.value_218i.id, self.value_220i.id])],
"required": True,
"custom": True,
}
)
# create attribute line 2
self.attributeLine3 = self.productAttributeLine.create(
{
"product_tmpl_id": product_tmpl.id,
"attribute_id": self.attr_engine.id,
"value_ids": [(6, 0, [self.value_218d.id, self.value_220d.id])],
"required": True,
}
)
# configure product creating config step
self.configStepLine1 = self.productConfigStepLine.create(
{
"product_tmpl_id": product_tmpl.id,
"config_step_id": self.config_step_engine.id,
"attribute_line_ids": [
(6, 0, [self.attributeLine1.id, self.attributeLine2.id])
],
}
)
# create config_step_line 2
self.configStepLine2 = self.productConfigStepLine.create(
{
"product_tmpl_id": product_tmpl.id,
"config_step_id": self.config_step_body.id,
"attribute_line_ids": [(6, 0, [self.attributeLine3.id])],
}
)
product_tmpl.write(
{
"config_step_line_ids": [
(6, 0, [self.configStepLine1.id, self.configStepLine2.id])
],
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_fuel.id}": self.custom_vals.id,
f"__custom_{self.attr_fuel.id}": "#DEFSRE",
f"__attribute_{self.attr_engine.id}": self.custom_vals.id,
f"__custom_{self.attr_engine.id}": "#FERDFGR",
}
)
product_config_wizard_1.action_next_step()
product_config_wizard_1.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
# check for custom value
custom_vals = {
f"__attribute_{self.attr_fuel.id}": self.custom_vals.id,
f"__custom_{self.attr_fuel.id}": "#DEFSRE",
f"__attribute_{self.attr_engine.id}": self.custom_vals.id,
f"__custom_{self.attr_engine.id}": "#FERDFGR",
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
product_config_wizard_1.read(custom_vals)
session = self.productConfigSession.search(
[("product_tmpl_id", "=", product_tmpl.id)]
)
session.unlink()
self.attributeLine1.custom = False
self.attributeLine1.multi = True
self.ProductConfWizard.action_next_step()
product_config_wizard_2 = self.ProductConfWizard.create(
{
"product_tmpl_id": product_tmpl.id,
}
)
product_config_wizard_2.action_next_step()
product_config_wizard_2.write(
{
f"__attribute_{self.attr_fuel.id}": [
(6, 0, [self.value_diesel.id, self.value_gasoline.id])
],
f"__attribute_{self.attr_engine.id}": self.custom_vals.id,
f"__custom_{self.attr_engine.id}": "#FERDFGR",
}
)
product_config_wizard_2.action_next_step()
product_config_wizard_2.write(
{
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
)
# check for multi value
multi_vals = {
f"__attribute_{self.attr_fuel.id}": [
(6, 0, [self.value_diesel.id, self.value_gasoline.id])
],
f"__attribute_{self.attr_engine.id}": self.custom_vals.id,
f"__custom_{self.attr_engine.id}": "#FERDFGR",
f"__attribute_{self.attr_color.id}": self.value_red.id,
}
product_config_wizard_2.read(multi_vals)
def test_16_get_onchange_domains(self):
self.wizard = self.env["product.configurator"]
# session id
session_id = self.productConfigSession.create(
{
"product_tmpl_id": self.config_product.id,
"value_ids": [
(
6,
0,
[
self.value_gasoline.id,
self.value_transmission.id,
self.value_red.id,
],
)
],
"user_id": self.env.user.id,
}
)
field_prefix = self.wizard._prefixes.get("field_prefix")
check_available_val_id = {
field_prefix
+ "%s" % (self.value_gasoline.attribute_id.id): self.value_gasoline.id,
field_prefix + "%s" % (self.value_218i.attribute_id.id): self.value_218i.id,
field_prefix
+ "%s" % (self.value_sport_line.attribute_id.id): self.value_sport_line.id,
}
values_ids = self.value_diesel.ids
product_tmpl_id = self.config_product
domains_available = self.wizard.get_onchange_domains(
check_available_val_id, values_ids, product_tmpl_id, session_id
)
rec = domains_available[
field_prefix + str(self.value_sport_line.attribute_id.id)
][-1][-1]
self.assertNotIn(
self.value_sport_line.id,
rec,
"Error: If value exists\
Method: get_onchange_domains()",
)

View file

@ -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>

View file

@ -0,0 +1,192 @@
<?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="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>
<!-- 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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1 @@
from . import product_configurator

View file

@ -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>

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-product-configurator-product_configurator"
version = "16.0.0"
description = "Product Configurator - Base for product configuration interface modules"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-account>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["product_configurator"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,47 @@
# Product Configurator Manufacturing
Odoo addon: product_configurator_mrp
## Installation
```bash
pip install odoo-bringout-oca-product-configurator-product_configurator_mrp
```
## Dependencies
This addon depends on:
- mrp
- product_configurator
## Manifest Information
- **Name**: Product Configurator Manufacturing
- **Version**: 16.0.1.0.0
- **Category**: Manufacturing
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/product-configurator](https://github.com/OCA/product-configurator) branch 16.0, addon `product_configurator_mrp`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

Some files were not shown because too many files have changed in this diff Show more