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