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>
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for product_configurator. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [account](https://github.com/bringout/oca-ocb-accounting/tree/b11fb50e2ed11eec1e305a0df730b49554c01199/odoo-bringout-oca-ocb-account)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in product_configurator.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class class
|
||||
class ProductConfigurator
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
||||
from .init_hook import post_init_hook
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import product_config
|
||||
from . import product_attribute
|
||||
from . import product
|
||||
from . import ir_ui_view
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
* `Aion Tech <https://aiontech.company/>`_:
|
||||
|
||||
* Simone Rubino <simone.rubino@aion-tech.it>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
This module has all the mechanics to support product configuration. It serves as a base
|
||||
dependency for configuration interfaces.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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*/
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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",
|
||||
# )
|
||||
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
@ -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()",
|
||||
)
|
||||
|
|
@ -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()",
|
||||
)
|
||||
|
|
@ -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()",
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="{"active": "Configurable", "inactive": "Standard"}"
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import product_configurator
|
||||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||