@ -0,0 +1,39 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html |
|||
:alt: License: AGPL-3 |
|||
|
|||
Purchase Product Configurator |
|||
============================= |
|||
The module adds the product configurator feature to the purchase module, |
|||
which is only present in sale module by default. |
|||
|
|||
Configuration |
|||
============= |
|||
* No additional configurations are required |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
Credits |
|||
------- |
|||
* Developers: (V17) Unnimaya C O , Contact: odoo@cybrosys.com |
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit `Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
@ -0,0 +1,23 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from . import controllers |
|||
from . import models |
@ -0,0 +1,59 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
{ |
|||
'name': 'Purchase Product Configurator', |
|||
'version': '17.0.1.0.0', |
|||
'category': 'Purchases', |
|||
'summary': """Helps to configure the product in purchase order line""", |
|||
'description': """The module helps you to override purchase_order_line to add |
|||
the product configurate in an RFQ """, |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': 'https://www.cybrosys.com', |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'depends': ['purchase_product_matrix'], |
|||
'data': [ |
|||
'views/optional_product_template.xml', |
|||
'views/purchase_order_views.xml', |
|||
'views/product_template_views.xml' |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'purchase_product_configurator/static/src/js/purchase_product_field.js', |
|||
'purchase_product_configurator/static/src/js/product_configurator_dialog/product_configurator_dialog.js', |
|||
'purchase_product_configurator/static/src/js/product_configurator_dialog/product_configurator_dialog.xml', |
|||
'purchase_product_configurator/static/src/js/product_list/product_list.js', |
|||
'purchase_product_configurator/static/src/js/product_list/product_list.xml', |
|||
'purchase_product_configurator/static/src/js/product/product.js', |
|||
'purchase_product_configurator/static/src/js/product/product_template.xml', |
|||
'purchase_product_configurator/static/src/js/product_template_attribute_line/product_template_attribute_line.js', |
|||
'purchase_product_configurator/static/src/js/product_template_attribute_line/product_template_attribute_line.xml', |
|||
'purchase_product_configurator/static/src/js/product/product.scss', |
|||
'purchase_product_configurator/static/src/js/product_template_attribute_line/product_template_attribute_line.scss' |
|||
], |
|||
}, |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': False |
|||
} |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from . import purchase_product_configurator |
@ -0,0 +1,220 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from odoo.http import Controller, request, route |
|||
|
|||
|
|||
class ProductConfiguratorController(Controller): |
|||
|
|||
@route('/purchase_product_configurator/get_values', type='json', auth='user') |
|||
def get_product_configurator_values( |
|||
self, |
|||
product_template_id, |
|||
quantity, |
|||
currency_id, |
|||
product_uom_id=None, |
|||
company_id=None, |
|||
ptav_ids=None, |
|||
only_main_product=False, |
|||
): |
|||
""" Return all product information needed for the product configurator. |
|||
""" |
|||
if company_id: |
|||
request.update_context(allowed_company_ids=[company_id]) |
|||
product_template = request.env['product.template'].browse(product_template_id) |
|||
combination = request.env['product.template.attribute.value'] |
|||
if ptav_ids: |
|||
combination = request.env['product.template.attribute.value'].browse(ptav_ids).filtered( |
|||
lambda ptav: ptav.product_tmpl_id.id == product_template_id |
|||
) |
|||
# Set missing attributes (unsaved no_variant attributes, or new attribute on existing product) |
|||
unconfigured_ptals = ( |
|||
product_template.attribute_line_ids - combination.attribute_line_id).filtered( |
|||
lambda ptal: ptal.attribute_id.display_type != 'multi') |
|||
combination += unconfigured_ptals.mapped( |
|||
lambda ptal: ptal.product_template_value_ids._only_active()[:1] |
|||
) |
|||
if not combination: |
|||
combination = product_template._get_first_possible_combination() |
|||
return dict( |
|||
products=[ |
|||
dict( |
|||
**self._get_product_information( |
|||
product_template, |
|||
combination, |
|||
currency_id, |
|||
quantity=quantity, |
|||
product_uom_id=product_uom_id |
|||
), |
|||
parent_product_tmpl_ids=[], |
|||
) |
|||
], |
|||
optional_products=[ |
|||
dict( |
|||
**self._get_product_information( |
|||
optional_product_template, |
|||
optional_product_template._get_first_possible_combination( |
|||
parent_combination=combination |
|||
), |
|||
currency_id, |
|||
# giving all the ptav of the parent product to get all the exclusions |
|||
parent_combination=product_template.attribute_line_ids. \ |
|||
product_template_value_ids, |
|||
), |
|||
parent_product_tmpl_ids=[product_template.id], |
|||
) for optional_product_template in product_template.optional_product_ids |
|||
] if not only_main_product else [] |
|||
) |
|||
|
|||
@route('/purchase_product_configurator/create_product', type='json', auth='user') |
|||
def purchase_product_configurator_create_product(self, product_template_id, combination): |
|||
""" Create the product when there is a dynamic attribute in the combination. |
|||
""" |
|||
product_template = request.env['product.template'].browse(product_template_id) |
|||
combination = request.env['product.template.attribute.value'].browse(combination) |
|||
product = product_template._create_product_variant(combination) |
|||
return product.id |
|||
|
|||
@route('/purchase_product_configurator/update_combination', type='json', auth='user') |
|||
def purchase_product_configurator_update_combination( |
|||
self, |
|||
product_template_id, |
|||
combination, |
|||
currency_id, |
|||
quantity, |
|||
product_uom_id=None, |
|||
company_id=None, |
|||
): |
|||
""" Return the updated combination information. |
|||
""" |
|||
if company_id: |
|||
request.update_context(allowed_company_ids=[company_id]) |
|||
product_template = request.env['product.template'].browse(product_template_id) |
|||
product_uom = request.env['uom.uom'].browse(product_uom_id) |
|||
currency = request.env['res.currency'].browse(currency_id) |
|||
combination = request.env['product.template.attribute.value'].browse(combination) |
|||
product = product_template._get_variant_for_combination(combination) |
|||
return self._get_basic_product_information( |
|||
product or product_template, |
|||
combination, |
|||
quantity=quantity or 0.0, |
|||
uom=product_uom, |
|||
currency=currency, |
|||
) |
|||
|
|||
@route('/purchase_product_configurator/get_optional_products', type='json', auth='user') |
|||
def purchase_product_configurator_get_optional_products( |
|||
self, |
|||
product_template_id, |
|||
combination, |
|||
parent_combination, |
|||
currency_id, |
|||
company_id=None, |
|||
): |
|||
""" Return information about optional products for the given `product.template`. |
|||
""" |
|||
if company_id: |
|||
request.update_context(allowed_company_ids=[company_id]) |
|||
product_template = request.env['product.template'].browse(product_template_id) |
|||
parent_combination = request.env['product.template.attribute.value'].browse( |
|||
parent_combination + combination |
|||
) |
|||
return [ |
|||
dict( |
|||
**self._get_product_information( |
|||
optional_product_template, |
|||
optional_product_template._get_first_possible_combination( |
|||
parent_combination=parent_combination |
|||
), |
|||
currency_id, |
|||
parent_combination=parent_combination |
|||
), |
|||
parent_product_tmpl_ids=[product_template.id], |
|||
) for optional_product_template in product_template.optional_product_ids |
|||
] |
|||
|
|||
def _get_product_information( |
|||
self, |
|||
product_template, |
|||
combination, |
|||
currency_id, |
|||
quantity=1, |
|||
product_uom_id=None, |
|||
parent_combination=None, |
|||
): |
|||
""" Return complete information about a product. |
|||
""" |
|||
product_uom = request.env['uom.uom'].browse(product_uom_id) |
|||
currency = request.env['res.currency'].browse(currency_id) |
|||
product = product_template._get_variant_for_combination(combination) |
|||
attribute_exclusions = product_template._get_attribute_exclusions( |
|||
parent_combination=parent_combination, |
|||
combination_ids=combination.ids, |
|||
) |
|||
return dict( |
|||
product_tmpl_id=product_template.id, |
|||
**self._get_basic_product_information( |
|||
product or product_template, |
|||
combination, |
|||
quantity=quantity, |
|||
uom=product_uom, |
|||
currency=currency |
|||
), |
|||
quantity=quantity, |
|||
attribute_lines=[dict( |
|||
id=ptal.id, |
|||
attribute=dict(**ptal.attribute_id.read(['id', 'name', 'display_type'])[0]), |
|||
attribute_values=[ |
|||
dict( |
|||
**ptav.read(['name', 'html_color', 'image', 'is_custom'])[0], |
|||
) for ptav in ptal.product_template_value_ids |
|||
if ptav.ptav_active or combination and ptav.id in combination.ids |
|||
], |
|||
selected_attribute_value_ids=combination.filtered( |
|||
lambda c: ptal in c.attribute_line_id |
|||
).ids, |
|||
create_variant=ptal.attribute_id.create_variant, |
|||
) for ptal in product_template.attribute_line_ids], |
|||
exclusions=attribute_exclusions['exclusions'], |
|||
archived_combinations=attribute_exclusions['archived_combinations'], |
|||
parent_exclusions=attribute_exclusions['parent_exclusions'], |
|||
) |
|||
|
|||
def _get_basic_product_information(self, product_or_template, combination, **kwargs): |
|||
""" Return basic information about a product |
|||
""" |
|||
basic_information = dict( |
|||
**product_or_template.read(['description_sale', 'display_name'])[0] |
|||
) |
|||
# If the product is a template, check the combination to compute the name to take dynamic |
|||
# and no_variant attributes into account. Also, drop the id which was auto-included by the |
|||
# search but isn't relevant since it is supposed to be the id of a `product.product` record. |
|||
if not product_or_template.is_product_variant: |
|||
basic_information['id'] = False |
|||
combination_name = combination._get_combination_name() |
|||
if combination_name: |
|||
basic_information.update( |
|||
display_name=f"{basic_information['display_name']} ({combination_name})" |
|||
) |
|||
return dict( |
|||
**basic_information, |
|||
price=product_or_template.standard_price |
|||
) |
@ -0,0 +1,8 @@ |
|||
## Module <purchase_product_configurator> |
|||
|
|||
#### 24.05.2024 |
|||
#### Version 17.0.1.0.0 |
|||
#### ADD |
|||
- Initial Commit for Purchase Product Configurator. |
|||
|
|||
|
@ -0,0 +1,24 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from . import product_attribute_custom_value |
|||
from . import product_template |
|||
from . import purchase_order_line |
@ -0,0 +1,35 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class ProductAttributeCustomValue(models.Model): |
|||
""" |
|||
Model for representing custom attribute values for a purchase order line. |
|||
Inherits from 'product.attribute.custom.value' model. |
|||
""" |
|||
_inherit = "product.attribute.custom.value" |
|||
|
|||
purchase_order_line_id = fields.Many2one('purchase.order.line', |
|||
string="Purchase Order Line", |
|||
required=True, ondelete='cascade', |
|||
help="purchase order lines") |
@ -0,0 +1,84 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class ProductTemplate(models.Model): |
|||
_inherit = 'product.template' |
|||
_check_company_auto = True |
|||
|
|||
product_config_mode = fields.Selection(selection=[('configurator', |
|||
"Product Configurator"), |
|||
('matrix', |
|||
"Order Grid Entry")], |
|||
string="Product Mode", |
|||
default='configurator', |
|||
help="Configurator: choose " |
|||
"attribute values to add " |
|||
"the matching product variant" |
|||
" to the order. " |
|||
"\nGrid: add several variants" |
|||
" at once from the grid " |
|||
"of attribute values") |
|||
optional_product_ids = fields.Many2many( |
|||
comodel_name='product.template', |
|||
relation='product_optional_rel', |
|||
column1='src_id', |
|||
column2='dest_id', |
|||
string="Optional Products", |
|||
help="Optional Products are suggested " |
|||
"whenever the customer hits *Add to Cart* (cross-sell strategy, " |
|||
"e.g. for computers: warranty, software, etc.).", |
|||
check_company=True) |
|||
|
|||
@api.depends('attribute_line_ids.value_ids.is_custom', 'attribute_line_ids.attribute_id.create_variant') |
|||
def _compute_has_configurable_attributes(self): |
|||
""" A product is considered configurable if: |
|||
- It has dynamic attributes |
|||
- It has any attribute line with at least 2 attribute values configured |
|||
- It has at least one custom attribute value """ |
|||
for product in self: |
|||
product.has_configurable_attributes = ( |
|||
any(attribute.create_variant == 'dynamic' for attribute in product.attribute_line_ids.attribute_id) |
|||
or any(len(attribute_line_id.value_ids) >= 2 for attribute_line_id in product.attribute_line_ids) |
|||
or any(attribute_value.is_custom for attribute_value in product.attribute_line_ids.value_ids) |
|||
) |
|||
|
|||
def get_single_product_variant(self): |
|||
""" Method used by the product configurator to check if the product is configurable or not. |
|||
|
|||
We need to open the product configurator if the product: |
|||
- is configurable (see has_configurable_attributes) |
|||
- has optional products """ |
|||
res = super().get_single_product_variant() |
|||
if res.get('product_id', False): |
|||
has_optional_products = False |
|||
for optional_product in self.product_variant_id.optional_product_ids: |
|||
if optional_product.has_dynamic_attributes() or optional_product._get_possible_variants( |
|||
self.product_variant_id.product_template_attribute_value_ids |
|||
): |
|||
has_optional_products = True |
|||
break |
|||
res.update({ |
|||
'has_optional_products': has_optional_products, |
|||
}) |
|||
return res |
@ -0,0 +1,64 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Unnimaya C O (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class PurchaseOrderLine(models.Model): |
|||
""" |
|||
Model for representing purchase order lines with additional fields and |
|||
methods. |
|||
Inherits from 'purchase.order.line' model. |
|||
""" |
|||
_inherit = 'purchase.order.line' |
|||
|
|||
product_config_mode = fields.Selection( |
|||
related='product_template_id.product_config_mode', |
|||
depends=['product_template_id'], |
|||
help="product configuration mode") |
|||
product_custom_attribute_value_ids = fields.One2many( |
|||
comodel_name='product.attribute.custom.value', |
|||
inverse_name='purchase_order_line_id', |
|||
string="Custom Values", |
|||
compute='_compute_custom_attribute_values', |
|||
help="product custom attribute values", |
|||
store=True, readonly=False, precompute=True, copy=True) |
|||
|
|||
@api.depends('product_id') |
|||
def _compute_custom_attribute_values(self): |
|||
""" |
|||
Checks if the product has custom attribute values associated with it, |
|||
and if those values belong to the valid values of the product template. |
|||
""" |
|||
for line in self: |
|||
if not line.product_id: |
|||
line.product_custom_attribute_value_ids = False |
|||
continue |
|||
if not line.product_custom_attribute_value_ids: |
|||
continue |
|||
valid_values = line.product_id.product_tmpl_id. \ |
|||
valid_product_template_attribute_line_ids. \ |
|||
product_template_value_ids |
|||
# remove the is_custom values that don't belong to this template |
|||
for attribute in line.product_custom_attribute_value_ids: |
|||
if attribute.custom_product_template_attribute_value_id not in \ |
|||
valid_values: |
|||
line.product_custom_attribute_value_ids -= attribute |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 565 B |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 143 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,702 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>Odoo App 3 Index</title> |
|||
<!-- Bootstrap CSS --> |
|||
<link rel="stylesheet" |
|||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" |
|||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" |
|||
crossorigin="anonymous"> |
|||
<link rel="stylesheet" |
|||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"> |
|||
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" |
|||
rel="stylesheet"> |
|||
</head> |
|||
<body> |
|||
<section> |
|||
<div class="container" |
|||
style="font-family: 'Inter', sans-serif !important;background-color: #fff !important;"> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 d-flex justify-content-between flex-wrap align-items-sm-center" |
|||
style="border-bottom:1px solid rgba(0, 0, 0, 0.22)"> |
|||
<div class="my-3"> |
|||
<img src="assets/misc/Cybrosys R.png" |
|||
style="width:auto !important; height:40px !important"> |
|||
</div> |
|||
<div class="my-3 d-flex align-items-center"> |
|||
<div class="text-center" |
|||
style="background-color:#010A7B !important; color:#fff !important;font-size: 0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important;min-width: 120px !important;"> |
|||
Community |
|||
</div> |
|||
<div class="text-center" |
|||
style="background-color:#875A7B !important; color:#fff !important;font-size: 0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important;min-width: 120px !important;"> |
|||
Enterprise |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 text-center d-flex align-items-center flex-column" |
|||
style="margin: 80px 0px !important;"> |
|||
<h1 style="font-size: 2.8rem;font-weight: 700; color: |
|||
#1A202C;"> |
|||
Purchase Product Configurator</h1> |
|||
<p class="my-3 mb-4" |
|||
style="max-width: 80%; font-weight: 400 !important; line-height: 32px; color: #718096;"> |
|||
Configure the Product in Purchase Order Line |
|||
</p> |
|||
<div style="width: 80%; margin-top: 3rem;"> |
|||
<img src="assets/screenshots/hero.gif" |
|||
class="img-responsive" width="100%" height="auto"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5 mb-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#714b67 !important"> |
|||
Key Highlights |
|||
</p> |
|||
</div> |
|||
<div class="row py-4"> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Add products using product configurator</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Switch between product configurator and grid view</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Choose variants, custom attributes and optional products</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container rounded"> |
|||
<ul class="nav nav-tabs d-flex" |
|||
style="width: fit-content;margin: 0 auto;gap: 1rem;"> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
class="active show" data-toggle="tab" href="#tab1" |
|||
style="color: #fff;font-weight: 500; background-color: #714B67; text-decoration: none;"> |
|||
<i class="fa-regular fa-image pr-2" |
|||
style="color: #fff;"></i> |
|||
Screenshots</a></li> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
data-toggle="tab" href="#tab2" |
|||
style="color: #fff;font-weight: 500; text-decoration: none;"><i |
|||
class="fa-solid fa-star pr-2" |
|||
style="color: #fff;"></i>Features</a></li> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
data-toggle="tab" href="#tab3" |
|||
style="color: #fff;font-weight: 500; text-decoration: none; background-color: #714B67;"><i |
|||
class="fa-solid fa-book-open pr-2" |
|||
style="color: #fff;"></i>Released Notes</a></li> |
|||
</ul> |
|||
<div class="tab-content" |
|||
style="background-color: rgba(121, 113, 119, 0.04);"> |
|||
<div id="tab1" class="tab-pane fade in active show"> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
<img src="assets/screenshots/0.png" |
|||
class="img-thumbnail" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Navigate to the Attributes & Variants tab of any configurable product and choose the |
|||
PURCHASE VARIANT SELECTION as Order Grid Entry.</h4> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
<img src="assets/screenshots/1.png" |
|||
class="img-thumbnail" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
When selecting the product in the purchase order line, the Order Grid Entry window |
|||
opens. |
|||
</h4> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
<img src="assets/screenshots/2.png" |
|||
class="img-thumbnail" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Navigate to the Attributes & Variants tab of any configurable product and choose the |
|||
PURCHASE VARIANT SELECTION as Product Configurator.</h4> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
<img src="assets/screenshots/3.png" |
|||
class="img-thumbnail" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
When selecting the product in the purchase order line, the Open Product Configurator |
|||
window |
|||
opens. |
|||
</h4> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div id="tab2" class="tab-pane fade"> |
|||
<div class="col-mg-12" style="padding: 1rem 4rem;"> |
|||
<ul style="list-style: none; padding: 1rem 0;font-weight: 500;"> |
|||
<li class="py-3" |
|||
style="font-weight: 500;background-color: #fff; border-radius: 4px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<span style="margin-right: 12px;"><img |
|||
src="assets/misc/star (1) 2.svg" |
|||
alt="" |
|||
width="16px"></span>Add products using the product configurator, a tool that |
|||
allows you to customize and select specific product variant. |
|||
</li> |
|||
<li class="py-3" |
|||
style="font-weight: 500;background-color: #fff; border-radius: 4px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<span style="margin-right: 12px;"><img |
|||
src="assets/misc/star (1) 2.svg" |
|||
alt="" |
|||
width="16px"></span>Easily switch between the product configurator for |
|||
customized product selection and the grid view for quick, tabular product entry to |
|||
efficiently add products according to your need. |
|||
</li> |
|||
<li class="py-3" |
|||
style="font-weight: 500;background-color: #fff; border-radius: 4px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<span style="margin-right: 12px;"><img |
|||
src="assets/misc/star (1) 2.svg" |
|||
alt="" |
|||
width="16px"></span>Select variants, customize attributes, and add optional |
|||
products to tailor your order precisely to your needs. |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<div id="tab3" class="tab-pane fade"> |
|||
<div class="col-mg-12 active" style="padding: 1rem 4rem;"> |
|||
<div class="py-3" |
|||
style="font-weight: 500;background-color: #fff; border-radius: 4px; padding: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="d-flex mb-3" |
|||
style="font-size: 0.8rem; font-weight: 500;"><span>Version |
|||
17.0.1.0.0</span><span |
|||
class="px-2">|</span><span |
|||
style="color: #714B67;font-weight: 600;">Released on:22nd May 2024</span> |
|||
</div> |
|||
<p class="m-0" |
|||
style=" color:#718096!important; font-size:1rem !important;line-height: 28px;"> |
|||
Initial Commit for Purchase Product Configurator. |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-5"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Related Products</p> |
|||
</div> |
|||
</div> |
|||
<div id="myCarousel" class="carousel slide py-3" data-ride="carousel"> |
|||
<div class="carousel-inner"> |
|||
<div class="carousel-item active"> |
|||
<div class="row p-4"> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/vendor_portal_odoo/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/1.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Odoo Vendor Portal</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/purchase_dashboard_advanced/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/2.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Advanced Purchase Dashboard</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/import_template_download/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/3.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Import Template For Sales / Purchase / Invoice</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="carousel-item"> |
|||
<div class="row p-4"> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/employee_purchase_requisition/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/4.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Employee Purchase Requisition</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/section_wise_subtotal/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px;"> |
|||
<img src="assets/modules/5.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Section Wise Subtotal</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/sale_purchase_previous_product_cost/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px;"> |
|||
<img src="assets/modules/6.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Previous Sale/Purchase Product Rates</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<a class="carousel-control-prev" href="#myCarousel" |
|||
data-slide="prev" style="width: 35px; color: #000;"> |
|||
<span class="carousel-control-prev-icon"> |
|||
<i class="fa fa-chevron-left" |
|||
style="font-size: 24px;"></i> |
|||
</span> |
|||
</a> |
|||
<a class="carousel-control-next" href="#myCarousel" |
|||
data-slide="next" style="width: 35px; color: #000;"> |
|||
<span class="carousel-control-next-icon"> |
|||
<i class="fa fa-chevron-right" |
|||
style="font-size: 24px;"></i> |
|||
</span> |
|||
</a> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Our Services</p> |
|||
</div> |
|||
</div> |
|||
<div class="container my-5"> |
|||
<div class="row py-3"> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#13EA36 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/cogs.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Customization</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#DBC711; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/wrench.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Implementation</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#FF6B6B ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/lifebuoy.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Support</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#FFA801 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/user.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Hire |
|||
Odoo Developer</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
|
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#54A0FF; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/puzzle.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Integration</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#6D7680 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/update.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Migration</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#786FA6 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/consultation.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Consultancy</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#F8A5C2 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/training.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Implementation</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#E6BE26; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/license.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Licensing Consultancy</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Our Industries</p> |
|||
</div> |
|||
</div> |
|||
<div class="container"> |
|||
<div class="row my-5 py-4"> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100 " |
|||
style="border-right: 1px solid rgb(209, 209, 209); border-bottom: 1px solid rgb(209, 209, 209); padding: 30px; box-shadow: 6px 0 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/trading-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Trading</p> |
|||
<p>Easily procure and sell your products</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209);border-bottom: 1px solid rgb(209, 209, 209); padding: 30px;"> |
|||
<img src="assets/icons/pos-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">POS</p> |
|||
<p>Easy configuration and convivial experience</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209);border-bottom: 1px solid rgba(0, 0, 0, 0.2); padding: 30px; box-shadow: 0 5px 10px rgba(228, 227, 227, 0.373)"> |
|||
<img src="assets/icons/education-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Education</p> |
|||
<p>A platform for educational management</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-bottom: 1px solid rgb(209, 209, 209); padding: 30px; "> |
|||
<img src="assets/icons/manufacturing-black.png" |
|||
width="42px" height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Manufacturing</p> |
|||
<p>Plan, track and schedule your operations</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px;"> |
|||
<img src="assets/icons/ecom-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">E-commerce & |
|||
Website</p> |
|||
<p>Mobile friendly, awe-inspiring product pages</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px;box-shadow: 0 -5px 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/service-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Service |
|||
Management</p> |
|||
<p>Keep track of services and invoice</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px; "> |
|||
<img src="assets/icons/restaurant-black.png" |
|||
width="42px" height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Restaurant</p> |
|||
<p>Run your bar or restaurant methodically</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style=" padding: 30px;box-shadow: -5px 0 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/hotel-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Hotel |
|||
Management</p> |
|||
<p>An all-inclusive hotel management application</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-5"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Support</p> |
|||
</div> |
|||
</div> |
|||
<div class="container my-5"> |
|||
<div class="row" style="background-color: #FFFAFE;"> |
|||
<div class="col-md-6 pb-4 d-flex align-items-center justify-content-center" |
|||
style="border-right: 1px solid #D9D9D9;"> |
|||
<div style="padding: 30px;"> |
|||
<div class="d-flex align-items-center"> |
|||
<img src="assets/misc/support (1) 1.svg" alt="" |
|||
width="60px" style="margin-right: 12px;"> |
|||
<div style="padding: 0px 8px;"> |
|||
<span |
|||
style="color: #714B67;font-size: 24px;font-weight: 600;padding-bottom: 1rem;">Need |
|||
Help?</span> |
|||
<p class="m-0" style="color:#718096;">Got |
|||
questions or need help? Get in touch.</p> |
|||
<div style="font-weight: 400;"><span><img |
|||
src="assets/misc/support-email.svg" |
|||
alt="" |
|||
width="18px" |
|||
style="filter: invert(1);margin-right: 0.8rem;"></span>odoo@cybrosys.com |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pb-4 d-flex align-items-center justify-content-center"> |
|||
<div style="padding: 30px;"> |
|||
<div class="d-flex align-items-center"> |
|||
<img src="assets/misc/whatsapp 1.svg" alt="" |
|||
width="60px" style="margin-right: 12px;"> |
|||
<div> |
|||
<span style="color: #714B67;font-size: 24px;font-weight: 600;">WhatsApp</span> |
|||
<p class="m-0" style="color:#718096;">Say hi to |
|||
us on WhatsApp!</p> |
|||
<div style="font-weight: 400; font-size: 16px;"><span><img |
|||
src="assets/misc/phone.svg" |
|||
alt="" width="14px" |
|||
style="filter: invert(1); margin-right: 0.8rem;"></span>+91 |
|||
99456767686 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- Optional JavaScript --> |
|||
<!-- jQuery first, then Popper.js, then Bootstrap JS --> |
|||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> |
|||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,68 @@ |
|||
/** @odoo-module */ |
|||
|
|||
import { Component } from "@odoo/owl"; |
|||
import { formatCurrency } from "@web/core/currency"; |
|||
import { |
|||
ProductTemplateAttributeLine as PTAL |
|||
} from "../product_template_attribute_line/product_template_attribute_line"; |
|||
|
|||
export class Product extends Component { |
|||
static components = { PTAL }; |
|||
static template = "purchase_product_configurator.product"; |
|||
static props = { |
|||
id: { type: [Number, {value: false}], optional: true }, |
|||
product_tmpl_id: Number, |
|||
display_name: String, |
|||
description_sale: [Boolean, String], // backend sends 'false' when there is no description
|
|||
price: { type: [Number, {value: false}], optional: true }, |
|||
quantity: Number, |
|||
attribute_lines: Object, |
|||
optional: Boolean, |
|||
imageURL: { type: String, optional: true }, |
|||
archived_combinations: Array, |
|||
exclusions: Object, |
|||
parent_exclusions: Object, |
|||
parent_product_tmpl_ids: { type: Array, element: Number, optional: true }, |
|||
}; |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* Increase the quantity of the product in the state. |
|||
*/ |
|||
increaseQuantity() { |
|||
this.env.setQuantity(this.props.product_tmpl_id, this.props.quantity+1); |
|||
} |
|||
|
|||
/** |
|||
* Set the quantity of the product in the state. |
|||
* |
|||
* @param {Event} event |
|||
*/ |
|||
setQuantity(event) { |
|||
console.log('parseFloat(event.target.value)',parseFloat(event.target.value)) |
|||
this.env.setQuantity(this.props.product_tmpl_id, parseFloat(event.target.value)); |
|||
} |
|||
|
|||
/** |
|||
* Decrease the quantity of the product in the state. |
|||
*/ |
|||
decreaseQuantity() { |
|||
this.env.setQuantity(this.props.product_tmpl_id, this.props.quantity-1); |
|||
} |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Private
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* Return the price, in the format of the given currency. |
|||
* |
|||
* @return {String} - The price, in the format of the given currency. |
|||
*/ |
|||
getFormattedPrice() { |
|||
return formatCurrency(this.props.price, this.env.currencyId); |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
.table.o_purchase_product_configurator_table { |
|||
& tr:first-child > td { |
|||
padding-top: 0 !important; |
|||
} |
|||
|
|||
&.o_purchase_product_configurator_table_optional > :not(caption) > *:last-child > * { |
|||
border-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.o_purchase_product_configurator_img { |
|||
width: 120px; |
|||
max-height: 240px; |
|||
} |
|||
|
|||
.o_purchase_product_configurator_qty { |
|||
width: 160px; |
|||
|
|||
input { |
|||
max-width: 4rem; |
|||
//removing input field=number arrows as their size might |
|||
//change depending on browser default styling and shift input's position |
|||
&::-webkit-outer-spin-button, |
|||
&::-webkit-inner-spin-button { |
|||
-webkit-appearance: none; |
|||
margin: 0; |
|||
} |
|||
&[type=number] { |
|||
-moz-appearance: textfield; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o_purchase_product_configurator_price { |
|||
width: 160px; |
|||
} |
@ -0,0 +1,79 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!--Template for Product--> |
|||
<templates xml:space="preserve"> |
|||
<t t-name="purchase_product_configurator.product"> |
|||
<td class="o_purchase_product_configurator_img py-3 px-0"> |
|||
<img |
|||
t-if="this.props.id" |
|||
class="w-100" |
|||
t-att-src="'/web/image/product.product/'+this.props.id+'/image_128'" |
|||
alt="Product Image"/> |
|||
<img |
|||
t-else="" |
|||
class="w-100" |
|||
t-att-src="'/web/image/product.template/'+this.props.product_tmpl_id+'/image_128'" |
|||
alt="Product Image"/> |
|||
</td> |
|||
<td class="p-3" t-att-colspan="this.props.optional ? 2:false"> |
|||
<div class="mb-4 text-break" name="o_purchase_product_configurator_name"> |
|||
<h5 t-out="this.props.display_name"/> |
|||
<div |
|||
t-if="this.props.description_sale" |
|||
t-out="this.props.description_sale" |
|||
class="text-muted small"/> |
|||
<div t-if="!this.env.isPossibleCombination(this.props)" class="alert alert-warning mt-3"> |
|||
<span>This option or combination of options is not available</span> |
|||
</div> |
|||
</div> |
|||
<t t-foreach="this.props.attribute_lines" t-as="ptal" t-key="ptal.id"> |
|||
<PTAL t-props="ptal" productTmplId="this.props.product_tmpl_id"/> |
|||
</t> |
|||
</td> |
|||
<td class="o_purchase_product_configurator_qty py-3 px-0 text-end"> |
|||
<div t-if="!this.props.optional" class="input-group justify-content-end"> |
|||
<button |
|||
class="btn btn-secondary d-none d-md-inline-block" |
|||
aria-label="Remove one" |
|||
t-on-click="decreaseQuantity"> |
|||
<i class="fa fa-minus"/> |
|||
</button> |
|||
<input |
|||
class="form-control quantity border-bottom border-top text-center" |
|||
name="product_quantity" |
|||
type="number" |
|||
t-att-value="this.props.quantity" |
|||
t-on-change="setQuantity"/> |
|||
<button |
|||
class="btn btn-secondary d-none d-md-inline-block" |
|||
aria-label="Add one" |
|||
t-on-click="increaseQuantity"> |
|||
<i class="fa fa-plus"/> |
|||
</button> |
|||
</div> |
|||
<div t-else=""> |
|||
<h5 class="text-nowrap" t-out="getFormattedPrice()"/> |
|||
</div> |
|||
<a |
|||
class="d-block mt-2" |
|||
role="button" |
|||
t-if="!this.props.optional && this.env.mainProductTmplId !== this.props.product_tmpl_id" |
|||
t-on-click="() => this.env.removeProduct(this.props.product_tmpl_id)"> |
|||
Remove product |
|||
</a> |
|||
</td> |
|||
<td class="o_purchase_product_configurator_price py-3 px-0 text-end" name="price"> |
|||
<div t-if="!this.props.optional" class="input-group justify-content-end"> |
|||
<h5 class="text-nowrap" t-out="getFormattedPrice()"/> |
|||
</div> |
|||
<div t-else=""> |
|||
<button |
|||
t-if="this.props.optional" |
|||
class="btn btn-secondary" |
|||
t-att-class="{'disabled': !this.env.isPossibleCombination(this.props)}" |
|||
t-on-click="() => this.env.addProduct(this.props.product_tmpl_id)"> |
|||
<i class="fa fa-plus"/> Add |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,376 @@ |
|||
/** @odoo-module **/ |
|||
|
|||
import { _t } from "@web/core/l10n/translation"; |
|||
import { Component, onWillStart, useState, useSubEnv } from "@odoo/owl"; |
|||
import { Dialog } from '@web/core/dialog/dialog'; |
|||
import { PurchaseProductList } from "../product_list/product_list"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
|
|||
export class PurchaseProductConfiguratorDialog extends Component { |
|||
static components = { Dialog, PurchaseProductList}; |
|||
static template = 'purchase_product_configurator.dialog'; |
|||
static props = { |
|||
productTemplateId: Number, |
|||
ptavIds: { type: Array, element: Number }, |
|||
customAttributeValues: { |
|||
type: Array, |
|||
element: Object, |
|||
shape: { |
|||
ptavId: Number, |
|||
value: String, |
|||
} |
|||
}, |
|||
quantity: Number, |
|||
productUOMId: { type: Number, optional: true }, |
|||
companyId: { type: Number, optional: true }, |
|||
currencyId: Number, |
|||
edit: { type: Boolean, optional: true }, |
|||
save: Function, |
|||
discard: Function, |
|||
close: Function, // This is the close from the env of the Dialog Component
|
|||
}; |
|||
static defaultProps = { |
|||
edit: false, |
|||
} |
|||
setup() { |
|||
this.title = _t("Configure your product"); |
|||
this.rpc = useService("rpc"); |
|||
this.state = useState({ |
|||
products: [], |
|||
optionalProducts: [], |
|||
|
|||
}); |
|||
/** |
|||
* Initializes sub-environment for product customization. |
|||
*/ |
|||
useSubEnv({ |
|||
mainProductTmplId: this.props.productTemplateId, |
|||
currencyId: this.props.currencyId, |
|||
addProduct: this._addProduct.bind(this), |
|||
removeProduct: this._removeProduct.bind(this), |
|||
setQuantity: this._setQuantity.bind(this), |
|||
updateProductTemplateSelectedPTAV: this._updateProductTemplateSelectedPTAV.bind(this), |
|||
updatePTAVCustomValue: this._updatePTAVCustomValue.bind(this), |
|||
isPossibleCombination: this._isPossibleCombination, |
|||
}); |
|||
/** |
|||
* Initializes data and performs setup actions before starting. |
|||
* Loads data, sets state, updates custom values, and checks exclusions. |
|||
*/ |
|||
onWillStart(async () => { |
|||
const { products, optional_products } = await this._loadData(this.props.edit); |
|||
this.state.products = products; |
|||
this.state.optionalProducts = optional_products; |
|||
for (const customValue of this.props.customAttributeValues) { |
|||
this._updatePTAVCustomValue( |
|||
this.env.mainProductTmplId, |
|||
customValue.ptavId, |
|||
customValue.value |
|||
); |
|||
} |
|||
this._checkExclusions(this.state.products[0]); |
|||
}); |
|||
} |
|||
/** |
|||
* Loads data for the product configurator. |
|||
*/ |
|||
async _loadData(onlyMainProduct) { |
|||
return this.rpc('/purchase_product_configurator/get_values', { |
|||
product_template_id: this.props.productTemplateId, |
|||
currency_id: this.props.currencyId, |
|||
quantity: this.props.quantity, |
|||
product_uom_id: this.props.productUOMId, |
|||
company_id: this.props.companyId, |
|||
ptav_ids: this.props.ptavIds, |
|||
only_main_product: onlyMainProduct, |
|||
}); |
|||
} |
|||
/** |
|||
* Creates a product using the provided data. |
|||
*/ |
|||
async _createProduct(product) { |
|||
return this.rpc('/purchase_product_configurator/create_product', { |
|||
product_template_id: product.product_tmpl_id, |
|||
combination: this._getCombination(product), |
|||
}); |
|||
} |
|||
/** |
|||
* Updates a product combination with the provided quantity. |
|||
*/ |
|||
async _updateCombination(product, quantity) { |
|||
return this.rpc('/purchase_product_configurator/update_combination', { |
|||
product_template_id: product.product_tmpl_id, |
|||
combination: this._getCombination(product), |
|||
currency_id: this.props.currencyId, |
|||
so_date: this.props.soDate, |
|||
quantity: quantity, |
|||
product_uom_id: this.props.productUOMId, |
|||
company_id: this.props.companyId, |
|||
pricelist_id: this.props.pricelistId, |
|||
}); |
|||
} |
|||
/** |
|||
* Retrieves optional products available for the given product. |
|||
*/ |
|||
async _getOptionalProducts(product) { |
|||
return this.rpc('/purchase_product_configurator/get_optional_products', { |
|||
product_template_id: product.product_tmpl_id, |
|||
combination: this._getCombination(product), |
|||
parent_combination: this._getParentsCombination(product), |
|||
currency_id: this.props.currencyId, |
|||
so_date: this.props.soDate, |
|||
company_id: this.props.companyId, |
|||
pricelist_id: this.props.pricelistId, |
|||
}); |
|||
} |
|||
/** |
|||
* Add the product to the list of products and fetch his optional products. |
|||
*/ |
|||
async _addProduct(productTmplId) { |
|||
const index = this.state.optionalProducts.findIndex( |
|||
p => p.product_tmpl_id === productTmplId |
|||
); |
|||
if (index >= 0) { |
|||
this.state.products.push(...this.state.optionalProducts.splice(index, 1)); |
|||
// Fetch optional product from the server with the parent combination.
|
|||
const product = this._findProduct(productTmplId); |
|||
let newOptionalProducts = await this._getOptionalProducts(product); |
|||
for(const newOptionalProductDict of newOptionalProducts) { |
|||
// If the optional product is already in the list, add the id of the parent product
|
|||
// template in his list of `parent_product_tmpl_ids` instead of adding a second time
|
|||
// the product.
|
|||
const newProduct = this ._findProduct(newOptionalProductDict.product_tmpl_id); |
|||
if (newProduct) { |
|||
newOptionalProducts = newOptionalProducts.filter( |
|||
(p) => p.product_tmpl_id != newOptionalProductDict.product_tmpl_id |
|||
); |
|||
newProduct.parent_product_tmpl_ids.push(productTmplId); |
|||
} |
|||
} |
|||
if (newOptionalProducts) this.state.optionalProducts.push(...newOptionalProducts); |
|||
} |
|||
} |
|||
/** |
|||
* Remove the product and his optional products from the list of products. |
|||
*/ |
|||
_removeProduct(productTmplId) { |
|||
const index = this.state.products.findIndex(p => p.product_tmpl_id === productTmplId); |
|||
if (index >= 0) { |
|||
this.state.optionalProducts.push(...this.state.products.splice(index, 1)); |
|||
for (const childProduct of this._getChildProducts(productTmplId)) { |
|||
// Optional products might have multiple parents so we don't want to remove them if
|
|||
// any of their parents are still on the list of products.
|
|||
childProduct.parent_product_tmpl_ids = childProduct.parent_product_tmpl_ids.filter( |
|||
id => id !== productTmplId |
|||
); |
|||
if (!childProduct.parent_product_tmpl_ids.length) { |
|||
this._removeProduct(childProduct.product_tmpl_id); |
|||
this.state.optionalProducts.splice( |
|||
this.state.optionalProducts.findIndex( |
|||
p => p.product_tmpl_id === childProduct.product_tmpl_id |
|||
), 1 |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
/** |
|||
* Set the quantity of the product to a given value. |
|||
*/ |
|||
async _setQuantity(productTmplId, quantity) { |
|||
if (quantity <= 0) { |
|||
if (productTmplId === this.env.mainProductTmplId) { |
|||
const product = this._findProduct(productTmplId); |
|||
const { price } = await this._updateCombination(product, 1); |
|||
product.quantity = 1; |
|||
product.price = parseFloat(price); |
|||
return; |
|||
}; |
|||
this._removeProduct(productTmplId); |
|||
} else { |
|||
const product = this._findProduct(productTmplId); |
|||
const { price } = await this._updateCombination(product, quantity); |
|||
product.quantity = quantity; |
|||
product.price = parseFloat(price); |
|||
} |
|||
} |
|||
/** |
|||
* Change the value of `selected_attribute_value_ids` on the given PTAL in the product. |
|||
*/ |
|||
async _updateProductTemplateSelectedPTAV(productTmplId, ptalId, ptavId, multiIdsAllowed) { |
|||
const product = this._findProduct(productTmplId); |
|||
let selectedIds = product.attribute_lines.find(ptal => ptal.id === ptalId).selected_attribute_value_ids; |
|||
if (multiIdsAllowed) { |
|||
const ptavID = parseInt(ptavId); |
|||
if (!selectedIds.includes(ptavID)){ |
|||
selectedIds.push(ptavID); |
|||
} else { |
|||
selectedIds = selectedIds.filter(ptav => ptav !== ptavID); |
|||
} |
|||
|
|||
} else { |
|||
selectedIds = [parseInt(ptavId)]; |
|||
} |
|||
product.attribute_lines.find(ptal => ptal.id === ptalId).selected_attribute_value_ids = selectedIds; |
|||
this._checkExclusions(product); |
|||
if (this._isPossibleCombination(product)) { |
|||
const updatedValues = await this._updateCombination(product, product.quantity); |
|||
Object.assign(product, updatedValues); |
|||
// When a combination should exist but was deleted from the database, it should not be
|
|||
// selectable and considered as an exclusion.
|
|||
if (!product.id && product.attribute_lines.every(ptal => ptal.create_variant === "always")) { |
|||
const combination = this._getCombination(product); |
|||
product.archived_combinations = product.archived_combinations.concat([combination]); |
|||
this._checkExclusions(product); |
|||
} |
|||
} |
|||
} |
|||
/** |
|||
* Set the custom value for a given custom PTAV. |
|||
*/ |
|||
_updatePTAVCustomValue(productTmplId, ptavId, customValue) { |
|||
const product = this._findProduct(productTmplId); |
|||
product.attribute_lines.find( |
|||
ptal => ptal.selected_attribute_value_ids.includes(ptavId) |
|||
).customValue = customValue; |
|||
} |
|||
/** |
|||
* Check the exclusions of a given product and his child. |
|||
*/ |
|||
_checkExclusions(product, checked=undefined) { |
|||
const combination = this._getCombination(product); |
|||
const exclusions = product.exclusions; |
|||
const parentExclusions = product.parent_exclusions; |
|||
const archivedCombinations = product.archived_combinations; |
|||
const parentCombination = this._getParentsCombination(product); |
|||
const childProducts = this._getChildProducts(product.product_tmpl_id) |
|||
const ptavList = product.attribute_lines.flat().flatMap(ptal => ptal.attribute_values) |
|||
ptavList.map(ptav => ptav.excluded = false); // Reset all the values
|
|||
if (exclusions) { |
|||
for(const ptavId of combination) { |
|||
for(const excludedPtavId of exclusions[ptavId]) { |
|||
ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true; |
|||
} |
|||
} |
|||
} |
|||
if (parentCombination) { |
|||
for(const ptavId of parentCombination) { |
|||
for(const excludedPtavId of (parentExclusions[ptavId]||[])) { |
|||
ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true; |
|||
} |
|||
} |
|||
} |
|||
if (archivedCombinations) { |
|||
for(const excludedCombination of archivedCombinations) { |
|||
const ptavCommon = excludedCombination.filter((ptav) => combination.includes(ptav)); |
|||
if (ptavCommon.length === combination.length) { |
|||
for(const excludedPtavId of ptavCommon) { |
|||
ptavList.find(ptav => ptav.id === excludedPtavId).excluded = true; |
|||
} |
|||
} else if (ptavCommon.length === (combination.length - 1)) { |
|||
// In this case we only need to disable the remaining ptav
|
|||
const disabledPtavId = excludedCombination.find( |
|||
(ptav) => !combination.includes(ptav) |
|||
); |
|||
const excludedPtav = ptavList.find(ptav => ptav.id === disabledPtavId) |
|||
if (excludedPtav) { |
|||
excludedPtav.excluded = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
const checkedProducts = checked || []; |
|||
for(const optionalProductTmpl of childProducts) { |
|||
// if the product is not checked for exclusions
|
|||
if (!checkedProducts.includes(optionalProductTmpl)) { |
|||
checkedProducts.push(optionalProductTmpl); // remember that this product is checked
|
|||
this._checkExclusions(optionalProductTmpl, checkedProducts); |
|||
} |
|||
} |
|||
} |
|||
/** |
|||
* Return the product given his template id. |
|||
*/ |
|||
_findProduct(productTmplId) { |
|||
// The product might be in either of the two lists `products` or `optional_products`.
|
|||
return this.state.products.find(p => p.product_tmpl_id === productTmplId) || |
|||
this.state.optionalProducts.find(p => p.product_tmpl_id === productTmplId); |
|||
} |
|||
/** |
|||
* Return the list of dependents products for a given product. |
|||
*/ |
|||
_getChildProducts(productTmplId) { |
|||
return [ |
|||
...this.state.products.filter(p => p.parent_product_tmpl_ids?.includes(productTmplId)), |
|||
...this.state.optionalProducts.filter(p => p.parent_product_tmpl_ids?.includes(productTmplId)) |
|||
] |
|||
} |
|||
/** |
|||
* Return the selected PTAV of the product, as a list of `product.template.attribute.value` id. |
|||
*/ |
|||
_getCombination(product) { |
|||
return product.attribute_lines.flatMap(ptal => ptal.selected_attribute_value_ids); |
|||
} |
|||
/** |
|||
* Return the selected PTAV of all the product parents, as a list of |
|||
* `product.template.attribute.value` id. |
|||
*/ |
|||
_getParentsCombination(product) { |
|||
let parentsCombination = []; |
|||
for(const parentProductTmplId of product.parent_product_tmpl_ids || []) { |
|||
parentsCombination.push(this._getCombination(this._findProduct(parentProductTmplId))); |
|||
} |
|||
return parentsCombination.flat(); |
|||
} |
|||
/** |
|||
* Check if a product has a valid combination. |
|||
*/ |
|||
_isPossibleCombination(product) { |
|||
return product.attribute_lines.every(ptal => !ptal.attribute_values.find( |
|||
ptav => ptal.selected_attribute_value_ids.includes(ptav.id) |
|||
)?.excluded); |
|||
} |
|||
|
|||
/** |
|||
* Check if all the products selected have a valid combination. |
|||
*/ |
|||
isPossibleConfiguration() { |
|||
return [...this.state.products].every( |
|||
p => this._isPossibleCombination(p) |
|||
); |
|||
} |
|||
/** |
|||
* Confirm the current combination(s). |
|||
*/ |
|||
async onConfirm() { |
|||
if (!this.isPossibleConfiguration()) return; |
|||
// Create the products with dynamic attributes
|
|||
for (const product of this.state.products) { |
|||
if ( |
|||
!product.id && |
|||
product.attribute_lines.some(ptal => ptal.create_variant === "dynamic") |
|||
) { |
|||
const productId = await this._createProduct(product); |
|||
product.id = parseInt(productId); |
|||
} |
|||
} |
|||
await this.props.save( |
|||
this.state.products.find( |
|||
p => p.product_tmpl_id === this.env.mainProductTmplId |
|||
), |
|||
this.state.products.filter( |
|||
p => p.product_tmpl_id !== this.env.mainProductTmplId |
|||
), |
|||
); |
|||
this.props.close(); |
|||
} |
|||
/** |
|||
* Discard the modal. |
|||
*/ |
|||
onDiscard() { |
|||
if (!this.props.edit) { |
|||
this.props.discard(); // clear the line
|
|||
} |
|||
this.props.close(); |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!--Product configurator dialog template--> |
|||
<templates xml:space="preserve"> |
|||
<t t-name="purchase_product_configurator.dialog"> |
|||
<Dialog size="size" title="title" contentClass="this.state.optionalProducts.length ? 'h-75' : ''"> |
|||
<PurchaseProductList t-if="this.state.products.length" products="this.state.products"/> |
|||
<PurchaseProductList |
|||
t-if="this.state.optionalProducts.length" |
|||
products="this.state.optionalProducts" |
|||
areProductsOptional="true"/> |
|||
<t t-set-slot="footer"> |
|||
<button |
|||
class="btn btn-primary" |
|||
t-on-click="onConfirm" |
|||
t-att-disabled="!isPossibleConfiguration()"> |
|||
Confirm |
|||
</button> |
|||
<button class="btn btn-secondary" t-on-click="onDiscard"> |
|||
Cancel |
|||
</button> |
|||
</t> |
|||
</Dialog> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,31 @@ |
|||
/** @odoo-module */ |
|||
|
|||
import { Component } from "@odoo/owl"; |
|||
import { formatCurrency } from "@web/core/currency"; |
|||
import { Product } from "../product/product"; |
|||
|
|||
export class PurchaseProductList extends Component { |
|||
static components = { Product }; |
|||
static template = "purchaseProductConfigurator.PurchaseProductList"; |
|||
static props = { |
|||
products: Array, |
|||
areProductsOptional: { type: Boolean, optional: true }, |
|||
}; |
|||
static defaultProps = { |
|||
areProductsOptional: false, |
|||
}; |
|||
|
|||
/** |
|||
* Return the total of the product in the list, in the currency of the `sale.order`. |
|||
* |
|||
* @return {String} - The sum of all items in the list, in the currency of the `sale.order`. |
|||
*/ |
|||
getFormattedTotal() { |
|||
return formatCurrency( |
|||
this.props.products.reduce( |
|||
(totalPrice, product) => totalPrice + product.price * product.quantity, 0 |
|||
), |
|||
this.env.currencyId, |
|||
) |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!--PurchaseProductList template--> |
|||
<templates xml:space="preserve"> |
|||
<t t-name="purchaseProductConfigurator.PurchaseProductList"> |
|||
<h4 class="mt-4 mb-3" t-if="this.props.areProductsOptional">Add optional products</h4> |
|||
<table class="o_purchase_product_configurator_table table table-sm position-relative mb-0" t-att-class="{'o_purchase_product_configurator_table_optional': this.props.areProductsOptional}"> |
|||
<thead t-if="!this.props.areProductsOptional"> |
|||
<tr> |
|||
<th class="px-0 border-bottom-0" colspan="2">Product</th> |
|||
<th class="px-0 text-end border-bottom-0">Quantity</th> |
|||
<th class="px-0 text-end border-bottom-0">Price</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody class="border-top-0"> |
|||
<tr t-foreach="this.props.products" t-as="product" t-key="product.product_tmpl_id"> |
|||
<Product t-props="product" optional="this.props.areProductsOptional"/> |
|||
</tr> |
|||
<tr t-if="!this.props.areProductsOptional"> |
|||
<td colspan="4" class="border-bottom-0 text-end"> |
|||
<h4>Total: <span t-out="getFormattedTotal()"/></h4> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,102 @@ |
|||
/** @odoo-module */ |
|||
|
|||
import { Component } from "@odoo/owl"; |
|||
import { formatCurrency } from "@web/core/currency"; |
|||
|
|||
export class ProductTemplateAttributeLine extends Component { |
|||
static template = "purchaseProductConfigurator.ptal"; |
|||
static props = { |
|||
productTmplId: Number, |
|||
id: Number, |
|||
attribute: { |
|||
type: Object, |
|||
shape: { |
|||
id: Number, |
|||
name: String, |
|||
display_type: { |
|||
type: String, |
|||
validate: type => ["color", "multi", "pills", "radio", "select"].includes(type), |
|||
}, |
|||
}, |
|||
}, |
|||
attribute_values: { |
|||
type: Array, |
|||
element: { |
|||
type: Object, |
|||
shape: { |
|||
id: Number, |
|||
name: String, |
|||
html_color: [Boolean, String], // backend sends 'false' when there is no color
|
|||
image: [Boolean, String], // backend sends 'false' when there is no image set
|
|||
is_custom: Boolean, |
|||
excluded: { type: Boolean, optional: true }, |
|||
}, |
|||
}, |
|||
}, |
|||
selected_attribute_value_ids: { type: Array, element: Number }, |
|||
create_variant: { |
|||
type: String, |
|||
validate: type => ["always", "dynamic", "no_variant"].includes(type), |
|||
}, |
|||
customValue: {type: [{value: false}, String], optional: true}, |
|||
}; |
|||
/** |
|||
* Update the selected PTAV in the state. |
|||
*/ |
|||
updateSelectedPTAV(event) { |
|||
this.env.updateProductTemplateSelectedPTAV( |
|||
this.props.productTmplId, this.props.id, event.target.value, this.props.attribute.display_type == 'multi' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Update in the state the custom value of the selected PTAV. |
|||
*/ |
|||
updateCustomValue(event) { |
|||
this.env.updatePTAVCustomValue( |
|||
this.props.productTmplId, this.props.selected_attribute_value_ids[0], event.target.value |
|||
); |
|||
} |
|||
/** |
|||
* Return template name to use by checking the display type in the props. |
|||
*/ |
|||
getPTAVTemplate() { |
|||
switch(this.props.attribute.display_type) { |
|||
case 'color': |
|||
return 'purchaseProductConfigurator.ptav-color'; |
|||
case 'multi': |
|||
return 'purchaseProductConfigurator.ptav-multi'; |
|||
case 'pills': |
|||
return 'purchaseProductConfigurator.ptav-pills'; |
|||
case 'radio': |
|||
return 'purchaseProductConfigurator.ptav-radio'; |
|||
case 'select': |
|||
return 'purchaseProductConfigurator.ptav-select'; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Return the name of the PTAV |
|||
*/ |
|||
getPTAVSelectName(ptav) { |
|||
return ptav.name; |
|||
} |
|||
|
|||
/** |
|||
* Check if the selected ptav is custom or not. |
|||
*/ |
|||
isSelectedPTAVCustom() { |
|||
return this.props.attribute_values.find( |
|||
ptav => this.props.selected_attribute_value_ids.includes(ptav.id) |
|||
)?.is_custom; |
|||
} |
|||
|
|||
/** |
|||
* Check if the line has a custom ptav or not. |
|||
*/ |
|||
hasPTAVCustom() { |
|||
return this.props.attribute_values.some( |
|||
ptav => ptav.is_custom |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
.o_purchase_product_configurator_ptav_color { |
|||
border: 5px solid $border-color; |
|||
transition: $input-transition; |
|||
|
|||
@include o-field-pointer(); |
|||
|
|||
&:before { |
|||
content: ""; |
|||
display: block; |
|||
@include o-position-absolute(-3px, -3px, -3px, -3px); |
|||
border: 4px solid $o-view-background-color; |
|||
border-radius: 50%; |
|||
box-shadow: inset 0 0 3px rgba(black, 0.3); |
|||
} |
|||
|
|||
input { |
|||
margin: 8px; |
|||
height: 13px; |
|||
width: 13px; |
|||
opacity: 0; |
|||
} |
|||
|
|||
&.active { |
|||
border: 5px solid map-get($theme-colors, 'primary'); |
|||
} |
|||
|
|||
&.custom_value { |
|||
background-image: linear-gradient(to bottom right, #FF0000, #FFF200, #1E9600); |
|||
} |
|||
|
|||
&.transparent { |
|||
background-image: url(/web/static/img/transparent.png); |
|||
} |
|||
|
|||
&.css_not_available { |
|||
opacity: 1; |
|||
|
|||
&:after { |
|||
content: ""; |
|||
@include o-position-absolute(-5px, -5px, -5px, -5px); |
|||
border: 2px solid map-get($theme-colors, 'danger'); |
|||
border-radius: 50%; |
|||
background: str-replace(url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='39' height='39'><line y2='0' x2='39' y1='39' x1='0' style='stroke:#{map-get($theme-colors, 'danger')};stroke-width:2'/><line y2='1' x2='40' y1='40' x1='1' style='stroke:rgb(255,255,255);stroke-width:1'/></svg>"), "#", "%23") ; |
|||
background-position: center; |
|||
background-repeat: no-repeat; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o_purchase_product_configurator_ptav_pills.active label { |
|||
$-btn-secondary-design: map-get($o-btns-bs-override, "secondary"); |
|||
|
|||
background-color: map-get($-btn-secondary-design, active-background); |
|||
border-color: map-get($-btn-secondary-design, active-border); |
|||
color: map-get($-btn-secondary-design, active-color); |
|||
} |
|||
|
|||
.css_not_available { |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
option.css_not_available { |
|||
opacity: 1; |
|||
color: #ccc; |
|||
} |
@ -0,0 +1,135 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<templates xml:space="preserve"> |
|||
<!-- Product attributes line template --> |
|||
<t t-name="purchaseProductConfigurator.ptal"> |
|||
<div name="ptal" t-attf-class="#{this.props.attribute_values.length === 1 && hasPTAVCustom() ? 'd-flex' : ''}"> |
|||
<!-- If the attribute line contains only one attribute value, we don't show the attribute |
|||
value template or the attribute line title unless the single attribute value is custom, |
|||
whereas in this case, only the title of the attribute line and the custom value |
|||
template are rendered. --> |
|||
<div class="d-flex flex-column flex-lg-row gap-2 mb-2"> |
|||
<label |
|||
t-if="this.props.attribute_values.length === 1 && isSelectedPTAVCustom() || this.props.attribute_values.length > 1" |
|||
t-out="this.props.attribute.name" |
|||
t-attf-class="fw-bold text-break #{this.props.attribute_values.length === 1 && hasPTAVCustom() ? '' : 'w-lg-25'}"/> |
|||
<t t-if="this.props.attribute_values.length > 1" t-call="{{getPTAVTemplate()}}"/> |
|||
</div> |
|||
<input |
|||
class="o_input w-75 mb-4 ms-lg-auto" |
|||
type="text" |
|||
placeholder="Enter a customized value" |
|||
t-if="hasPTAVCustom && isSelectedPTAVCustom()" |
|||
t-att-value="this.props.customValue" |
|||
t-on-change="updateCustomValue" |
|||
/> |
|||
</div> |
|||
</t> |
|||
<!-- Attributes value templates --> |
|||
<t t-name="purchaseProductConfigurator.ptav-select"> |
|||
<select class="o_input w-50 flex-grow-1" |
|||
t-on-change="updateSelectedPTAV" |
|||
t-att-name="'ptal-' + this.props.id"> |
|||
<option |
|||
t-foreach="this.props.attribute_values" |
|||
t-as="ptav" |
|||
t-key="ptav.id" |
|||
t-att-value="ptav.id" |
|||
t-att-selected="this.props.selected_attribute_value_ids.includes(ptav.id)" |
|||
t-out="getPTAVSelectName(ptav)" |
|||
t-att-class="{ 'css_not_available': ptav.excluded }"/> |
|||
</select> |
|||
</t> |
|||
<t t-name="purchaseProductConfigurator.ptav-radio"> |
|||
<ul class="list-unstyled flex-grow-1 m-0"> |
|||
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id" |
|||
class="mb-2"> |
|||
<div class="form-check"> |
|||
<label |
|||
class="form-check-label d-inline-flex align-items-center" |
|||
t-att-class="{ 'css_not_available': ptav.excluded }" |
|||
t-att-for="ptav.id"> |
|||
<span t-out="ptav.name"/> |
|||
</label> |
|||
<input |
|||
type="radio" |
|||
class="form-check-input" |
|||
t-att-id="ptav.id" |
|||
t-att-value="ptav.id" |
|||
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)" |
|||
t-att-name="'ptal-' + this.props.id" |
|||
t-on-change="updateSelectedPTAV"/> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
</t> |
|||
<t t-name="purchaseProductConfigurator.ptav-pills"> |
|||
<ul class="list-inline list-unstyled flex-grow-1 mb-0"> |
|||
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id" |
|||
t-att-class="{'active': this.props.selected_attribute_value_ids.includes(ptav.id)}" |
|||
class="o_purchase_product_configurator_ptav_pills list-inline-item"> |
|||
<label |
|||
class="btn btn-outline-secondary" |
|||
t-att-class="{ 'css_not_available': ptav.excluded }" |
|||
t-att-for="ptav.id"> |
|||
<span t-out="ptav.name"/> |
|||
</label> |
|||
<input |
|||
class="btn-check" |
|||
type="radio" |
|||
t-att-id="ptav.id" |
|||
t-att-value="ptav.id" |
|||
t-att-name="'ptal-' + this.props.id" |
|||
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)" |
|||
t-on-change="updateSelectedPTAV"/> |
|||
</li> |
|||
</ul> |
|||
</t> |
|||
<t t-name="purchaseProductConfigurator.ptav-color"> |
|||
<ul class="list-inline flex-grow-1 mb-0"> |
|||
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id" |
|||
class="list-inline-item me-2"> |
|||
<t t-set="img_style" t-value="ptav.image ? 'background:url(/web/image/product.template.attribute.value/'+ptav.id+'/image); background-size:cover;' : ''"/> |
|||
<t t-set="color_style" t-value="ptav.is_custom ? '' : 'background-color:' + ptav.html_color"/> |
|||
<label |
|||
class="position-relative d-inline-block rounded-pill text-center" |
|||
t-att-title="ptav.name" |
|||
t-attf-style="#{img_style or color_style}" |
|||
t-att-class="{'o_purchase_product_configurator_ptav_color': true, |
|||
'active': this.props.selected_attribute_value_ids.includes(ptav.id), |
|||
'custom_value': ptav.is_custom, |
|||
'transparent': !ptav.is_custom and !ptav.html_color, |
|||
'css_not_available': ptav.excluded }"> |
|||
<input |
|||
type="radio" |
|||
t-att-id="ptav.id" |
|||
t-att-value="ptav.id" |
|||
t-att-name="'ptal-' + this.props.id" |
|||
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)" |
|||
t-on-change="updateSelectedPTAV"/> |
|||
</label> |
|||
</li> |
|||
</ul> |
|||
</t> |
|||
<t t-name="purchaseProductConfigurator.ptav-multi"> |
|||
<ul class="list-unstyled flex-grow-1 m-0"> |
|||
<li t-foreach="this.props.attribute_values" t-as="ptav" t-key="ptav.id" |
|||
class="mb-2"> |
|||
<div class="form-check"> |
|||
<input |
|||
type="checkbox" |
|||
class="form-check-input" |
|||
t-att-id="ptav.id" |
|||
t-att-value="ptav.id" |
|||
t-att-name="'ptal-' + this.props.id" |
|||
t-att-checked="this.props.selected_attribute_value_ids.includes(ptav.id)" |
|||
t-on-change="updateSelectedPTAV"/> |
|||
<label |
|||
class="form-check-label" |
|||
t-att-for="ptav.id"> |
|||
<span t-out="ptav.name"/> |
|||
</label> |
|||
</div> |
|||
</li> |
|||
</ul> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,149 @@ |
|||
/** @odoo-module */ |
|||
|
|||
import { PurchaseOrderLineProductField } from '@purchase_product_matrix/js/purchase_product_field'; |
|||
import { serializeDateTime } from "@web/core/l10n/dates"; |
|||
import { x2ManyCommands } from "@web/core/orm_service"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
import { patch } from "@web/core/utils/patch"; |
|||
import { PurchaseProductConfiguratorDialog } from "./product_configurator_dialog/product_configurator_dialog"; |
|||
import { ormService } from "@web/core/orm_service"; |
|||
import { jsonrpc } from "@web/core/network/rpc_service"; |
|||
|
|||
async function applyProduct(record, product) { |
|||
// handle custom values & no variants
|
|||
const customAttributesCommands = [ |
|||
x2ManyCommands.set([]), // Command.clear isn't supported in static_list/_applyCommands
|
|||
]; |
|||
for (const ptal of product.attribute_lines) { |
|||
const selectedCustomPTAV = ptal.attribute_values.find( |
|||
ptav => ptav.is_custom && ptal.selected_attribute_value_ids.includes(ptav.id) |
|||
); |
|||
if (selectedCustomPTAV) { |
|||
customAttributesCommands.push( |
|||
x2ManyCommands.create(undefined, { |
|||
custom_product_template_attribute_value_id: [selectedCustomPTAV.id, "we don't care"], |
|||
custom_value: ptal.customValue, |
|||
}) |
|||
); |
|||
}; |
|||
} |
|||
const noVariantPTAVIds = product.attribute_lines.filter( |
|||
ptal => ptal.create_variant === "no_variant" && ptal.attribute_values.length > 1 |
|||
).flatMap(ptal => ptal.selected_attribute_value_ids); |
|||
await record.update({ |
|||
product_id: [product.id, product.display_name], |
|||
product_no_variant_attribute_value_ids: [x2ManyCommands.set(noVariantPTAVIds)], |
|||
product_custom_attribute_value_ids: customAttributesCommands, |
|||
}); |
|||
await record.update({ |
|||
product_qty: product.quantity, |
|||
}); |
|||
}; |
|||
|
|||
patch(PurchaseOrderLineProductField.prototype, { |
|||
setup() { |
|||
super.setup(...arguments); |
|||
this.dialog = useService("dialog"); |
|||
this.orm = useService("orm"); |
|||
}, |
|||
async _onProductTemplateUpdate() { |
|||
const result = await this.orm.call( |
|||
'product.template', |
|||
'get_single_product_variant', |
|||
[this.props.record.data.product_template_id[0]], |
|||
); |
|||
const product_config_mode = await this.orm.read( |
|||
'product.template', |
|||
[this.props.record.data.product_template_id[0]], |
|||
["product_config_mode"] |
|||
); |
|||
if(result && result.product_id) { |
|||
if (this.props.record.data.product_id != result.product_id.id) { |
|||
this.props.record.update({ |
|||
// TODO right name get (same problem as configurator)
|
|||
product_id: [result.product_id, 'whatever'], |
|||
}); |
|||
} |
|||
} |
|||
else { |
|||
if (!product_config_mode[0].product_config_mode || product_config_mode[0].product_config_mode === 'configurator') { |
|||
this._openProductConfigurator(); |
|||
} else { |
|||
// only triggered when purchase_product_matrix is installed.
|
|||
this._openGridConfigurator(false); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Checks if the template is configurable. |
|||
*/ |
|||
get isConfigurableTemplate() { |
|||
return super.isConfigurableTemplate || this.props.record.data.is_configurable_product; |
|||
}, |
|||
/** |
|||
* Opens the product configurator. |
|||
*/ |
|||
async _openProductConfigurator(jsonInfo, productTemplateId, editedCellAttributes,edit=false) { |
|||
const purchaseOrderRecord = this.props.record.model.root; |
|||
let ptavIds = this.props.record.data.product_template_attribute_value_ids.records.map( |
|||
record => record.resId |
|||
); |
|||
let customAttributeValues = []; |
|||
if (edit) { |
|||
/** |
|||
* no_variant and custom attribute don't need to be given to the configurator for new |
|||
* products. |
|||
*/ |
|||
ptavIds = ptavIds.concat(this.props.record.data.product_no_variant_attribute_value_ids.records.map( |
|||
record => record.resId |
|||
)); |
|||
/** |
|||
* `product_custom_attribute_value_ids` records are not loaded in the view bc sub templates |
|||
* are not loaded in list views. Therefore, we fetch them from the server if the record is |
|||
* saved. Else we use the value stored on the line. |
|||
*/ |
|||
customAttributeValues = |
|||
this.props.record.data.product_custom_attribute_value_ids.records[0]?.isNew ? |
|||
this.props.record.data.product_custom_attribute_value_ids.records.map( |
|||
record => record.data |
|||
) : |
|||
await this.orm.read( |
|||
'product.attribute.custom.value', |
|||
this.props.record.data.product_custom_attribute_value_ids.currentIds, |
|||
["custom_product_template_attribute_value_id", "custom_value"] |
|||
) |
|||
} |
|||
this.dialog.add(PurchaseProductConfiguratorDialog, { |
|||
productTemplateId: this.props.record.data.product_template_id[0], |
|||
ptavIds: ptavIds, |
|||
customAttributeValues: customAttributeValues.map( |
|||
data => { |
|||
return { |
|||
ptavId: data.custom_product_template_attribute_value_id[0], |
|||
value: data.custom_value, |
|||
} |
|||
} |
|||
), |
|||
quantity:1.0, |
|||
productUOMId: this.props.record.data.product_uom[0], |
|||
companyId: purchaseOrderRecord.data.company_id[0], |
|||
currencyId: this.props.record.data.currency_id[0], |
|||
edit: edit, |
|||
save: async (mainProduct, optionalProducts) => { |
|||
await applyProduct(this.props.record, mainProduct); |
|||
purchaseOrderRecord.data.order_line.leaveEditMode(); |
|||
for (const optionalProduct of optionalProducts) { |
|||
const line = await purchaseOrderRecord.data.order_line.addNewRecord({ |
|||
position: 'bottom', |
|||
mode: "readonly", |
|||
}); |
|||
await applyProduct(line, optionalProduct); |
|||
} |
|||
}, |
|||
discard: () => { |
|||
purchaseOrderRecord.data.order_line.delete(this.props.record); |
|||
}, |
|||
}); |
|||
}, |
|||
}); |
@ -0,0 +1,254 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!--Template code for displaying the optional products modal in the purchase order form view.--> |
|||
<template id="purchase_optional_products_modal" name="Optional Products"> |
|||
<main class="modal-body"> |
|||
<t t-call="purchase_product_configurator.configure_optional_products"/> |
|||
</main> |
|||
</template> |
|||
<!--Template code for displaying a quantity configuration input with add and remove buttons.--> |
|||
<template id="product_quantity_config"> |
|||
<div class="css_quantity input-group"> |
|||
<button t-attf-href="#" class="btn btn-primary float_left js_add_cart_json d-none d-md-inline-block" |
|||
aria-label="Remove one" title="Remove one"> |
|||
<i class="fa fa-minus"/> |
|||
</button> |
|||
<input type="text" |
|||
class="js_quantity form-control quantity text-center" |
|||
style="max-width: 4rem" |
|||
data-min="1" |
|||
name="add_qty" |
|||
t-att-value="add_qty or 1"/> |
|||
<button t-attf-href="#" class="btn btn-primary float_left js_add_cart_json d-none d-md-inline-block" |
|||
aria-label="Add one" title="Add one"> |
|||
<i class="fa fa-plus"/> |
|||
</button> |
|||
</div> |
|||
</template> |
|||
<!-- Template code for configuring a product variant with price, quantity configuration, and product image display. --> |
|||
<template id="configure" name="Configure"> |
|||
<div class="js_product main_product"> |
|||
<t t-set="combination" |
|||
t-value="product_combination if product_combination else product._get_first_possible_combination()"/> |
|||
<t t-set="combination_info" |
|||
t-value="product._get_combination_info(combination, add_qty=add_qty or 1, pricelist=pricelist)"/> |
|||
<t t-set="product_variant" t-value="product.env['product.product'].browse(combination_info['product_id'])"/> |
|||
<input type="hidden" class="product_template_id" t-att-value="product.id"/> |
|||
<input type="hidden" class="product_id" t-attf-name="product_id" t-att-value="product_variant.id"/> |
|||
<input type="hidden" class="has_optional_products" t-attf-name="has_optional_products" |
|||
t-att-value="product_variant.optional_product_ids.filtered(lambda p: p._is_add_to_cart_possible(combination))"/> |
|||
<div class="col-lg-12 text-center mt-5"> |
|||
<t t-if="product._is_add_to_cart_possible()"> |
|||
<div class="col-lg-5 d-inline-block text-start"> |
|||
<t t-if="combination" t-call="sale.variants"> |
|||
<t t-set="parent_combination" t-value="None"/> |
|||
</t> |
|||
<h2> |
|||
<span t-attf-class="text-danger oe_default_price oe_striked_price {{'' if combination_info['has_discounted_price'] else 'd-none'}}" |
|||
t-out="combination_info['list_price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
<span class="oe_price product_id mt-3" style="white-space: nowrap;" |
|||
t-att-data-product-id="product.id" |
|||
t-out="combination_info['price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
</h2> |
|||
<t t-if="product.visible_qty_configurator"> |
|||
<t t-call="purchase_product_configurator.product_quantity_config"/> |
|||
</t> |
|||
<p class="css_not_available_msg alert alert-warning">This combination does not exist.</p> |
|||
</div> |
|||
<div class="col-lg-1 d-inline-block"/> |
|||
<div class="col-lg-5 d-inline-block align-top text-start"> |
|||
<img t-if="product_variant" |
|||
t-att-src="'/web/image/product.product/%s/image_1024' % product_variant.id" |
|||
class="d-block product_detail_img" alt="Product Image"/> |
|||
<img t-else="" t-att-src="'/web/image/product.template/%s/image_1024' % product.id" |
|||
class="d-block product_detail_img" alt="Product Image"/> |
|||
</div> |
|||
</t> |
|||
<t t-else=""> |
|||
<div class="col-lg-5 d-inline-block text-start"> |
|||
<p class="alert alert-warning">This product has no valid combination.</p> |
|||
</div> |
|||
</t> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<!-- modal: full table, currently selected products at top --> |
|||
<template id="configure_optional_products"> |
|||
<table class="table table-striped table-sm"> |
|||
<thead> |
|||
<tr> |
|||
<th class="td-img"> |
|||
<span class='label'>Product</span> |
|||
</th> |
|||
<th> |
|||
<span class='label'/> |
|||
</th> |
|||
<th class="text-center td-qty"> |
|||
<span class='label'>Quantity</span> |
|||
</th> |
|||
<th class="text-center td-price"> |
|||
<span class='label'>Price</span> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr class="js_product in_cart main_product"> |
|||
|
|||
<t t-set="combination_info" |
|||
t-value="product.product_tmpl_id._get_combination_info(combination, product.id, add_qty or 1, pricelist)"/> |
|||
<t t-set="product_variant" |
|||
t-value="product.env['product.product'].browse(combination_info['product_id'])"/> |
|||
|
|||
<input type="hidden" class="product_template_id" t-att-value="product.product_tmpl_id.id"/> |
|||
<input type="hidden" class="product_id" t-att-value="product_variant.id"/> |
|||
<td class='td-img'> |
|||
<img class="product_detail_img" t-if="product_variant" |
|||
t-att-src="'/web/image/product.product/%s/image_128' % product_variant.id" |
|||
alt="Product Image"/> |
|||
<img class="product_detail_img" t-else="" |
|||
t-att-src="'/web/image/product.template/%s/image_128' % product.id" alt="Product Image"/> |
|||
</td> |
|||
<td class='td-product_name'> |
|||
<strong class="product-name product_display_name" t-out="combination_info['display_name']"/> |
|||
<div class="text-muted small"> |
|||
<div t-field="product.description_sale"/> |
|||
<div class="js_attributes"/> |
|||
</div> |
|||
<div> |
|||
<t t-if="product.product_tmpl_id and not combination"> |
|||
<t t-set="combination" |
|||
t-value="product.product_tmpl_id._get_first_possible_combination()"/> |
|||
</t> |
|||
<t t-if="combination and not already_configured" t-call="sale.variants"> |
|||
<t t-set="ul_class" t-valuef="flex-column"/> |
|||
<t t-set="product" t-value="product.product_tmpl_id"/> |
|||
</t> |
|||
<t t-else=""> |
|||
<ul class="d-none js_add_cart_variants mb-0" |
|||
t-att-data-attribute_exclusions="{'exclusions: []'}"/> |
|||
<div class="d-none oe_unchanged_value_ids" |
|||
t-att-data-unchanged_value_ids="variant_values"/> |
|||
</t> |
|||
</div> |
|||
</td> |
|||
<td class="text-center td-qty"> |
|||
<t t-call='purchase_product_configurator.product_quantity_config'/> |
|||
</td> |
|||
<td class="text-center td-price" name="price"> |
|||
<div t-attf-class="text-danger oe_default_price oe_striked_price {{'' if combination_info['has_discounted_price'] else 'd-none'}}" |
|||
t-out="combination_info['list_price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}' |
|||
/> |
|||
<span class="oe_price product_id" style="white-space: nowrap;" |
|||
t-att-data-product-id="product.id" |
|||
t-out="combination_info['price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
<span class="js_raw_price d-none" t-out="product._get_contextual_price()"/> |
|||
<p class="css_not_available_msg alert alert-warning">Option not available</p> |
|||
</td> |
|||
</tr> |
|||
<tr class="o_total_row"> |
|||
<td colspan="4" class="text-end"> |
|||
<strong>Total:</strong> |
|||
<span class="js_price_total fw-bold" style="white-space: nowrap;" |
|||
t-att-data-product-id="product.id" |
|||
t-out="combination_info['price'] * (add_qty or 1)" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
</td> |
|||
</tr> |
|||
<t t-if="product.optional_product_ids and mode != 'edit'"> |
|||
<tr class="o_select_options"> |
|||
<td colspan="4"> |
|||
<h4>Available Options:</h4> |
|||
</td> |
|||
</tr> |
|||
<t t-call="purchase_product_configurator.optional_product_items"> |
|||
<t t-set="parent_combination" t-value="combination"/> |
|||
</t> |
|||
</t> |
|||
</tbody> |
|||
</table> |
|||
</template> |
|||
<!-- modal: optional products --> |
|||
<template id="optional_product_items"> |
|||
<t t-foreach="product.optional_product_ids" t-as="product"> |
|||
<t t-set="combination" t-value="product._get_first_possible_combination(parent_combination)"/> |
|||
<t t-if="product._is_add_to_cart_possible(parent_combination)"> |
|||
|
|||
<t t-set="combination_info" |
|||
t-value="product._get_combination_info(combination, add_qty=add_qty or 1, pricelist=pricelist)"/> |
|||
<t t-set="product_variant" |
|||
t-value="product.env['product.product'].browse(combination_info['product_id'])"/> |
|||
|
|||
<tr class="js_product"> |
|||
<td class="td-img"> |
|||
<input type="hidden" class="product_template_id" t-att-value="product.id"/> |
|||
<input type="hidden" class="product_id" t-attf-name="optional-product-#{product.id}" |
|||
t-att-value="product_variant.id"/> |
|||
<img t-if="product_variant" |
|||
t-att-src="'/web/image/product.product/%s/image_128' % product_variant.id" |
|||
class="variant_image" alt="Product Image"/> |
|||
<img t-else="" t-att-src="'/web/image/product.template/%s/image_128' % product.id" |
|||
class="variant_image" alt="Product Image"/> |
|||
</td> |
|||
<td class='td-product_name' colspan="2"> |
|||
<div class="mb-3"> |
|||
<strong class="product-name product_display_name" t-out="combination_info['display_name']"/> |
|||
<div class="text-muted small" t-field="product.description_sale"/> |
|||
</div> |
|||
<t t-call="sale.variants"> |
|||
<t t-set="combination" |
|||
t-value="product._get_first_possible_combination(parent_combination)"/> |
|||
</t> |
|||
</td> |
|||
<td class="text-center td-qty d-none"> |
|||
<t t-call='purchase_product_configurator.product_quantity_config'/> |
|||
</td> |
|||
<td class="text-center td-price"> |
|||
<div t-attf-class="text-danger oe_default_price oe_optional oe_striked_price {{'' if combination_info['has_discounted_price'] else 'd-none'}}" |
|||
t-out="combination_info['list_price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
<div class="oe_price" style="white-space: nowrap;" |
|||
t-out="combination_info['price']" |
|||
t-options='{ |
|||
"widget": "monetary", |
|||
"display_currency": (pricelist or product).currency_id |
|||
}'/> |
|||
<span class="js_raw_price d-none" t-out="combination_info['price']"/> |
|||
<p class="css_not_available_msg alert alert-warning">Option not available</p> |
|||
|
|||
<a role="button" href="#" class="js_add btn btn-primary btn-sm"> |
|||
<i class="fa fa-shopping-cart add-optionnal-item"/> |
|||
Add to cart |
|||
</a> |
|||
<span class="js_remove d-none"> |
|||
<a role="button" href="#" class="js_remove"> |
|||
<i class="fa fa-trash-o remove-optionnal-item"/> |
|||
</a> |
|||
</span> |
|||
</td> |
|||
</tr> |
|||
</t> |
|||
</t> |
|||
</template> |
|||
</odoo> |
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!--Adding a group in the purchase configurator section of the |
|||
product template form view with the fields 'has_configurable_attributes' |
|||
and 'product_config_mode'.--> |
|||
<record id="product_template_only_form_view" model="ir.ui.view"> |
|||
<field name="name">product.template.view.form.inherit.purchase.product.configurator</field> |
|||
<field name="model">product.template</field> |
|||
<field name="inherit_id" ref="product.product_template_only_form_view"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//page[@name='variants']" position="inside"> |
|||
<group name="product_mode_config" invisible="purchase_ok==False"> |
|||
<group string="Purchase Variant Selection" invisible="has_configurable_attributes == False"> |
|||
<field name="has_configurable_attributes" invisible="1"/> |
|||
<field name="product_config_mode" invisible="has_configurable_attributes == False" |
|||
widget="radio" nolabel="1" colspan="2"/> |
|||
</group> |
|||
</group> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo> |
|||
<!--Adding invisible fields (product_config_mode and |
|||
product_custom_attribute_value_ids) after the product_template_id field in |
|||
the purchase order form view.--> |
|||
<record id="purchase_order_form" model="ir.ui.view"> |
|||
<field name="name">purchase.order.view.form.inherit.purchase.product.configurator</field> |
|||
<field name="model">purchase.order</field> |
|||
<field name="inherit_id" ref="purchase.purchase_order_form"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//tree/field[@name='product_template_id']" |
|||
position="after"> |
|||
<field name="product_config_mode" invisible="1"/> |
|||
<field name="product_custom_attribute_value_ids" invisible="1" /> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |