@ -0,0 +1,39 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:target: http://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: (v16) Ayisha Sumayya K, Vivek , 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) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Ayisha Sumayya K, Vivek (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# 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 LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER 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,51 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Ayisha Sumayya K, Vivek (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# 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 LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################# |
||||
|
{ |
||||
|
'name': 'Purchase Product Configurator', |
||||
|
'version': '16.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 configurate product in 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', 'sale', 'base', '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/basic_model.js', |
||||
|
'purchase_product_configurator/static/src/js/product_configurator.js', |
||||
|
'purchase_product_configurator/static/src/js/purchase_product_field.js', |
||||
|
], |
||||
|
}, |
||||
|
'installable': True, |
||||
|
'auto_install': False, |
||||
|
'application': False |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Ayisha Sumayya K, Vivek (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,142 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Ayisha Sumayya K, Vivek (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 http |
||||
|
from odoo.http import request |
||||
|
|
||||
|
|
||||
|
class ProductConfiguratorController(http.Controller): |
||||
|
""" |
||||
|
Controller for handling product configuration in the purchase module. |
||||
|
""" |
||||
|
@http.route(['/purchase_product_configurator/configure'], type='json', |
||||
|
auth="user", methods=['POST']) |
||||
|
def configure(self, product_template_id, pricelist_id, **kw): |
||||
|
""" |
||||
|
Configure a product with the specified template and pricelist. |
||||
|
""" |
||||
|
add_qty = float(kw.get('add_qty', 1)) |
||||
|
product_template = request.env['product.template'].browse( |
||||
|
int(product_template_id)) |
||||
|
pricelist = self._get_pricelist(pricelist_id) |
||||
|
product_combination = False |
||||
|
attribute_value_ids = set( |
||||
|
kw.get('product_template_attribute_value_ids', [])) |
||||
|
attribute_value_ids |= set( |
||||
|
kw.get('product_no_variant_attribute_value_ids', [])) |
||||
|
if attribute_value_ids: |
||||
|
product_combination = request.env[ |
||||
|
'product.template.attribute.value'].browse(attribute_value_ids) |
||||
|
if pricelist: |
||||
|
product_template = product_template.with_context( |
||||
|
pricelist=pricelist.id, partner=request.env.user.partner_id) |
||||
|
return request.env['ir.ui.view']._render_template( |
||||
|
"purchase_product_configurator.configure", |
||||
|
{ |
||||
|
'product': product_template, |
||||
|
'pricelist': pricelist, |
||||
|
'add_qty': add_qty, |
||||
|
'product_combination': product_combination |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
@http.route(['/purchase_product_configurator/show_advanced_configurator'], |
||||
|
type='json', auth="user", methods=['POST']) |
||||
|
def show_advanced_configurator(self, product_id, variant_values, |
||||
|
pricelist_id, **kw): |
||||
|
""" |
||||
|
Show the advanced configurator for a product with the specified ID, |
||||
|
variant values, and pricelist. |
||||
|
""" |
||||
|
pricelist = self._get_pricelist(pricelist_id) |
||||
|
return self._show_advanced_configurator(product_id, variant_values, |
||||
|
pricelist, False, **kw) |
||||
|
|
||||
|
@http.route(['/purchase_product_configurator/optional_product_items'], |
||||
|
type='json', auth="user", methods=['POST']) |
||||
|
def optional_product_items(self, product_id, pricelist_id, **kw): |
||||
|
""" |
||||
|
Get the optional product items for the specified product ID and |
||||
|
pricelist. |
||||
|
""" |
||||
|
pricelist = self._get_pricelist(pricelist_id) |
||||
|
return self._optional_product_items(product_id, pricelist, **kw) |
||||
|
|
||||
|
def _optional_product_items(self, product_id, pricelist, **kw): |
||||
|
""" |
||||
|
Helper method to get the optional product items for the specified |
||||
|
product ID and pricelist. |
||||
|
""" |
||||
|
add_qty = float(kw.get('add_qty', 1)) |
||||
|
product = request.env['product.product'].browse(int(product_id)) |
||||
|
parent_combination = product.product_template_attribute_value_ids |
||||
|
if product.env.context.get('no_variant_attribute_values'): |
||||
|
parent_combination |= product.env.context.get( |
||||
|
'no_variant_attribute_values') |
||||
|
return request.env['ir.ui.view']._render_template( |
||||
|
"purchase_product_configurator.optional_product_items", { |
||||
|
'product': product, |
||||
|
'parent_name': product.name, |
||||
|
'parent_combination': parent_combination, |
||||
|
'pricelist': pricelist, |
||||
|
'add_qty': add_qty, |
||||
|
}) |
||||
|
|
||||
|
def _show_advanced_configurator(self, product_id, variant_values, pricelist, |
||||
|
handle_stock, **kw): |
||||
|
""" |
||||
|
Helper method to show the advanced configurator for a product with the |
||||
|
specified ID, variant values, pricelist, and other parameters. |
||||
|
""" |
||||
|
product = request.env['product.product'].browse(int(product_id)) |
||||
|
combination = request.env['product.template.attribute.value'].browse( |
||||
|
variant_values) |
||||
|
add_qty = float(kw.get('add_qty', 1)) |
||||
|
|
||||
|
no_variant_attribute_values = combination.filtered( |
||||
|
lambda |
||||
|
product_template_attribute_value: |
||||
|
product_template_attribute_value.attribute_id. |
||||
|
create_variant == 'no_variant' |
||||
|
) |
||||
|
if no_variant_attribute_values: |
||||
|
product = product.with_context( |
||||
|
no_variant_attribute_values=no_variant_attribute_values) |
||||
|
return request.env['ir.ui.view']._render_template( |
||||
|
"purchase_product_configurator.purchase_optional_products_modal", { |
||||
|
'product': product, |
||||
|
'combination': combination, |
||||
|
'add_qty': add_qty, |
||||
|
'parent_name': product.name, |
||||
|
'variant_values': variant_values, |
||||
|
'pricelist': pricelist, |
||||
|
'handle_stock': handle_stock, |
||||
|
'already_configured': kw.get("already_configured", False), |
||||
|
'mode': kw.get('mode', 'add'), |
||||
|
'product_custom_attribute_values': kw.get( |
||||
|
'product_custom_attribute_values', None) |
||||
|
}) |
||||
|
|
||||
|
def _get_pricelist(self, pricelist_id, pricelist_fallback=False): |
||||
|
""" |
||||
|
Helper method to get the pricelist based on the specified pricelist ID. |
||||
|
""" |
||||
|
return request.env['product.pricelist'].browse(int(pricelist_id or 0)) |
@ -0,0 +1,8 @@ |
|||||
|
## Module <purchase_product_configurator> |
||||
|
|
||||
|
#### 23.07.2023 |
||||
|
#### Version 16.0.1.0.0 |
||||
|
#### ADD |
||||
|
- Initial Commit for Purchase Product Configurator. |
||||
|
|
||||
|
|
@ -0,0 +1,24 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Ayisha Sumayya K, Vivek (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# 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 LESSER GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################# |
||||
|
from . import product_attribute |
||||
|
from . import product_template |
||||
|
from . import purchase_order_line |
@ -0,0 +1,35 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Ayisha Sumayya K, Vivek (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,88 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Ayisha Sumayya K, Vivek (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): |
||||
|
""" |
||||
|
Model for representing product templates with additional fields and methods. |
||||
|
Inherits from 'product.template' model. |
||||
|
""" |
||||
|
_inherit = 'product.template' |
||||
|
|
||||
|
product_config_mode = fields.Selection(selection=[('configurator', |
||||
|
"Product Configurator"), |
||||
|
('matrix', |
||||
|
"Order Grid Entry")], |
||||
|
string="Add 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") |
||||
|
|
||||
|
@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}) |
||||
|
if self.has_configurable_attributes: |
||||
|
res['mode'] = self.product_config_mode |
||||
|
else: |
||||
|
res['mode'] = 'configurator' |
||||
|
return res |
@ -0,0 +1,64 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Ayisha Sumayya K, Vivek (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: 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.2 KiB |
After Width: | Height: | Size: 673 B |
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: 1.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 967 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 228 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 199 KiB |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,621 @@ |
|||||
|
<div style="background-color: #714B67; min-height: 600px; width: 100%; padding: 15px; position: relative;"> |
||||
|
<!-- TITLE BAR --> |
||||
|
<div |
||||
|
style="border-bottom: 1px solid #875A7B; padding: 15px; display: flex; justify-content: space-between; align-items: center;"> |
||||
|
<img src="assets/misc/cybrosys-logo.png" width="42" height="42" style="width: 42px; height: 42px;"/> |
||||
|
<div> |
||||
|
<div style="color: #7C7BAD; font-size: 14px; font-family: 'Montserrat', sans-serif; font-weight: bold; background-color: white; display: inline-block; padding: 3px 10px; border-radius: 50px;" |
||||
|
class="mr-2"> |
||||
|
<i class="fa fa-check mr-1"></i>Community |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF TITLE BAR --> |
||||
|
|
||||
|
<!-- APP HERO --> |
||||
|
<div class="container"> |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-12 col-md-12 col-lg-12"> |
||||
|
<h1 style="color: #FFFFFF; font-weight: bolder; font-size: 50px; text-align: center; margin-top: 50px;">Purchase Product Configurator</h1> |
||||
|
<p style="color:#FFFFFF; padding: 8px 15px; text-align: center; font-size: 24px;">Configure The Product In Purchase Order Line</p> |
||||
|
<!-- END OF APP HERO --> |
||||
|
<img src="assets/screenshots/hero.gif" |
||||
|
style="width: 75%; height: auto; position: absolute; margin-left: auto; margin-right: auto; top: 45%; left: 12%; right: auto;"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<!-- NAVIGATION SECTION --> |
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px; margin-top: 300px;"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/compass.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Explore This |
||||
|
Module</h2> |
||||
|
</div> |
||||
|
<div class="row my-4" style="font-family: 'Montserrat', sans-serif;"> |
||||
|
<div class="col-sm-12 col-md-6 my-3"> |
||||
|
<a href="#overview"> |
||||
|
<div class="d-flex justify-content-between align-items-center" |
||||
|
style="background-color: #f5f5f5; padding: 30px; width: 100%;"> |
||||
|
<div> |
||||
|
<span style="color: #714B67; font-size: 24px; font-weight: 500; display: block;">Overview</span> |
||||
|
<span |
||||
|
style="color: #714B67; font-size: 16px; font-weight: 400; color:#282F33; display: block;">Learn |
||||
|
more about this |
||||
|
module</span> |
||||
|
</div> |
||||
|
<img src="assets/misc/right-arrow.png" width="36" height="36"/> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-sm-12 col-md-6 my-3"> |
||||
|
<a href="#configuration"> |
||||
|
<div class="d-flex justify-content-between align-items-center" |
||||
|
style="background-color: #f5f5f5; padding: 30px; width: 100%;"> |
||||
|
<div> |
||||
|
<span style="color: #714B67; font-size: 24px; font-weight: 500; display: block;">Configuration</span> |
||||
|
<span |
||||
|
style="color: #714B67; font-size: 16px; font-weight: 400; color:#282F33; display: block;">View configurations of this module</span> |
||||
|
</div> |
||||
|
<img src="assets/misc/right-arrow.png" width="36" height="36"/> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-sm-12 col-md-6 my-3"> |
||||
|
<a href="#features"> |
||||
|
<div class="d-flex justify-content-between align-items-center" |
||||
|
style="background-color: #f5f5f5; padding: 30px; width: 100%;"> |
||||
|
<div> |
||||
|
<span style="color: #714B67; font-size: 24px; font-weight: 500; display: block;">Features</span> |
||||
|
<span |
||||
|
style="color: #714B67; font-size: 16px; font-weight: 400; color:#282F33; display: block;">View features of this module</span> |
||||
|
</div> |
||||
|
<img src="assets/misc/right-arrow.png" width="36" height="36"/> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-sm-12 col-md-6 my-3"> |
||||
|
<a href="#screenshots"> |
||||
|
<div class="d-flex justify-content-between align-items-center" |
||||
|
style="background-color: #f5f5f5; padding: 30px; width: 100%;"> |
||||
|
<div> |
||||
|
<span style="color: #714B67; font-size: 24px; font-weight: 500; display: block;">Screenshots</span> |
||||
|
<span |
||||
|
style="color: #714B67; font-size: 16px; font-weight: 400; color:#282F33; display: block;"> |
||||
|
See key screenshots of this module |
||||
|
</span> |
||||
|
</div> |
||||
|
<img src="assets/misc/right-arrow.png" width="36" height="36"/> |
||||
|
</div> |
||||
|
</a> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF NAVIGATION SECTION --> |
||||
|
|
||||
|
<!-- OVERVIEW SECTION --> |
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px;" id="overview"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/pie-chart.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Overview |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="row" style="font-family: 'Montserrat', sans-serif; font-weight: 400; font-size: 14px; line-height: 200%;"> |
||||
|
<div class="col-sm-12 py-4"> |
||||
|
This module allows users to add products to RFQ through a product |
||||
|
configurator. After installing this module, users can use product |
||||
|
configurator to add products to the purchase order line instead of |
||||
|
grid view to add products, which is used in the purchase module by |
||||
|
default. |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF OVERVIEW SECTION --> |
||||
|
|
||||
|
<!-- CONFIGURATION SECTION --> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="border-bottom: 2px solid #714B67; padding: 15px 0px;" |
||||
|
id="configuration"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/star.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" |
||||
|
style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;"> |
||||
|
Configuration |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="row" |
||||
|
style="font-family: 'Montserrat', sans-serif; font-weight: 400; font-size: 14px; line-height: 200%;"> |
||||
|
<div class="col-sm-12 py-4"> |
||||
|
No additional configuration required. |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF CONFIGURATION SECTION --> |
||||
|
|
||||
|
|
||||
|
<!-- FEATURES SECTION --> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="border-bottom: 2px solid #714B67; padding: 15px 0px;" |
||||
|
id="features"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/features.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" |
||||
|
style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;"> |
||||
|
Features |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="row" |
||||
|
style="font-family: 'Montserrat', sans-serif; font-weight: 400; font-size: 14px; line-height: 200%;"> |
||||
|
<div class="col-sm-12 col-md-6"> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="margin-top: 40px; margin-bottom: 40px"> |
||||
|
<img src="assets/misc/check-box.png" class="mr-2"/> |
||||
|
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Add Products Using Product Configurator</span> |
||||
|
<p style="font-family: 'Roboto', sans-serif !important; font-weight: 450 !important; color: #282F33 !important; font-size: 1rem !important;"> |
||||
|
Allows users to add product to order line using product configurator.</p> |
||||
|
</div> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="margin-top: 30px; margin-bottom: 30px"> |
||||
|
<img src="assets/misc/check-box.png" class="mr-2"/> |
||||
|
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Choose Variants or Custom Attributes, and Optional |
||||
|
Products</span> |
||||
|
<p style="font-family: 'Roboto', sans-serif !important; font-weight: 450 !important; color: #282F33 !important; font-size: 1rem !important;"> |
||||
|
Users can add variants, or custom attributes from the available |
||||
|
options. Also, they can choose the optional products from |
||||
|
the displayed options</p> |
||||
|
</div> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="margin-top: 30px; margin-bottom: 30px"> |
||||
|
<img src="assets/misc/check-box.png" class="mr-2"/> |
||||
|
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Switch Between Product Configurator and Grid View</span> |
||||
|
<p style="font-family: 'Roboto', sans-serif !important; font-weight: 450 !important; color: #282F33 !important; font-size: 1rem !important;"> |
||||
|
Users can switch between the Product Configurator and |
||||
|
Grid view options according to their needs to add products to |
||||
|
the order line |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF FEATURES SECTION --> |
||||
|
|
||||
|
<!-- SCREENSHOTS SECTION --> |
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px;" id="screenshots"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/pictures.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Screenshots |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-12"> |
||||
|
|
||||
|
<div style="display: block; margin: 30px auto;"> |
||||
|
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Add Products Using Product Configurator</h3> |
||||
|
<img src="assets/screenshots/1.png" class="img-thumbnail"> |
||||
|
</div> |
||||
|
|
||||
|
<div style="display: block; margin: 30px auto;"> |
||||
|
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Choose Variants or Custom Attributes, and Optional |
||||
|
Products</h3> |
||||
|
|
||||
|
<img src="assets/screenshots/2.png" class="img-thumbnail"> |
||||
|
</div> |
||||
|
|
||||
|
<div style="display: block; margin: 30px auto;"> |
||||
|
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Switch Between Product Configurator and Grid View</h3> |
||||
|
|
||||
|
<img src="assets/screenshots/3.png" class="img-thumbnail"> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF SCREENSHOTS SECTION --> |
||||
|
|
||||
|
<!-- RELATED PRODUCTS --> |
||||
|
<div class="d-flex align-items-center" |
||||
|
style="border-bottom: 2px solid #714B67; padding: 15px 0px;"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/categories.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" |
||||
|
style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;"> |
||||
|
Related |
||||
|
Products |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-12"> |
||||
|
<div id="demo1" class="row carousel slide" data-ride="carousel"> |
||||
|
<!-- The slideshow --> |
||||
|
<div class="carousel-inner" style="padding: 30px;"> |
||||
|
<div class="carousel-item" style="min-height: 198.656px;"> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/export_stockinfo_xls/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/1.png"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/inventory_stock_dashboard_odoo/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/2.png"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/customer_product_qrcode/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/3.png"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="carousel-item active" |
||||
|
style="min-height: 198.656px;"> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/list_view_sticky_header/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/4.png"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/openai_product_tag_descrption/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/5.gif"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" |
||||
|
style="float:left"> |
||||
|
<a href="https://apps.odoo.com/apps/modules/16.0/advanced_vat_invoice/" |
||||
|
target="_blank"> |
||||
|
<div style="border-radius:10px"> |
||||
|
<img class="img img-responsive center-block" |
||||
|
style="border-radius: 0px;" |
||||
|
src="assets/modules/6.png"> |
||||
|
</div> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- Left and right controls --> |
||||
|
<a class="carousel-control-prev" href="#demo1" 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="#demo1" |
||||
|
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> |
||||
|
</div> |
||||
|
<!-- END OF RELATED PRODUCTS --> |
||||
|
|
||||
|
<!-- OUR SERVICES --> |
||||
|
|
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px;"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/star.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Our Services |
||||
|
</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="container my-5"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #1dd1a1 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/cogs.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Customization</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #ff6b6b !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/wrench.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Implementation</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #6462CD !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/lifebuoy.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Support</h6> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #ffa801 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/user.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Hire |
||||
|
Odoo |
||||
|
Developer</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #54a0ff !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/puzzle.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Integration</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #6d7680 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/update.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Migration</h6> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #786fa6 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/consultation.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Consultancy</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #f8a5c2 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/training.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Implementation</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #e6be26 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/license.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Licensing Consultancy</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<!-- END OF END OF OUR SERVICES --> |
||||
|
|
||||
|
<!-- OUR INDUSTRIES --> |
||||
|
|
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px;"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/corporate.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Our |
||||
|
Industries |
||||
|
</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="container my-5"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/trading-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Trading |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Easily procure |
||||
|
and |
||||
|
sell your products</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/pos-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
POS |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Easy |
||||
|
configuration |
||||
|
and convivial experience</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/education-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Education |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
A platform for |
||||
|
educational management</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/manufacturing-black.png" class="img-responsive mb-3" height="48px" |
||||
|
width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Manufacturing |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Plan, track and |
||||
|
schedule your operations</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/ecom-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
E-commerce & Website |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Mobile |
||||
|
friendly, |
||||
|
awe-inspiring product pages</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/service-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Service Management |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Keep track of |
||||
|
services and invoice</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/restaurant-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Restaurant |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
Run your bar or |
||||
|
restaurant methodically</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 0px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="assets/icons/hotel-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Hotel Management |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
||||
|
An |
||||
|
all-inclusive |
||||
|
hotel management application</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- END OF END OF OUR INDUSTRIES --> |
||||
|
|
||||
|
<!-- SUPPORT --> |
||||
|
<div class="d-flex align-items-center" style="border-bottom: 2px solid #714B67; padding: 15px 0px;"> |
||||
|
<div class="d-flex justify-content-center align-items-center mr-2" |
||||
|
style="background-color: #F5F5F5; border-radius: 0px; width: 40px; height: 40px;"> |
||||
|
<img src="assets/misc/customer-support.png"/> |
||||
|
</div> |
||||
|
<h2 class="mt-2" style="font-family: 'Montserrat', sans-serif; font-size: 24px; font-weight: bold;">Support |
||||
|
</h2> |
||||
|
</div> |
||||
|
<div class="container mt-5"> |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-12 col-md-6"> |
||||
|
<div style="background-color: #F6F8F9; padding: 30px; display: flex; align-items: center;"> |
||||
|
<div class="mr-4" |
||||
|
style="background-color: #714B67; display: inline-block; height: 70px; width: 70px; display: flex; align-items: center; justify-content: center;"> |
||||
|
<img src="assets/misc/support.png" height="48" width="48" style="width: 42px; height: 42px;"/> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>Need Help?</h4> |
||||
|
<p style="line-height: 100%;">Got questions or need help? Get in touch.</p> |
||||
|
<a href="mailto:odoo@cybrosys.com"> |
||||
|
<p style="font-weight: 400; font-size: 28px; line-height: 80%; color: #714B67;"> |
||||
|
odoo@cybrosys.com</p> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-sm-12 col-md-6"> |
||||
|
<div style="background-color: #F6F8F9; padding: 30px; display: flex; align-items: center;"> |
||||
|
<div class="mr-4" |
||||
|
style="background-color: #2AC44D; display: inline-block; height: 70px; width: 70px; display: flex; align-items: center; justify-content: center;"> |
||||
|
<img src="assets/misc/whatsapp.png" height="52" width="52" style="width: 52px; height: 52px;"/> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h4>WhatsApp</h4> |
||||
|
<p style="line-height: 100%;">Say hi to us on WhatsApp!</p> |
||||
|
<a href="https://api.whatsapp.com/send?phone=918606827707"> |
||||
|
<p style="font-weight: 400; font-size: 28px; line-height: 80%; color: #714B67;">+91 86068 |
||||
|
27707</p> |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-sm-12 my-5 d-flex justify-content-center align-items-center"> |
||||
|
<img src="assets/misc/logo.png" width="144" height="31" |
||||
|
style="width:144px; height: 31px; margin-top: 40px;"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF SUPPORT --> |
@ -0,0 +1,84 @@ |
|||||
|
odoo.define('purchase_product_configurator.BasicModel', function (require) { |
||||
|
'use strict'; |
||||
|
|
||||
|
var BasicModel = require('web.BasicModel'); |
||||
|
const { patch, unpatch } = require('web.utils'); |
||||
|
|
||||
|
|
||||
|
patch(BasicModel.prototype, 'purchase_product_configurator/static/src/js/basic_model.js', { |
||||
|
|
||||
|
/** |
||||
|
* Patch the _applyChange method of the BasicModel to handle specific field changes. |
||||
|
*/ |
||||
|
_applyChange (recordID, changes, options) { |
||||
|
var self = this; |
||||
|
var record = this.localData[recordID]; |
||||
|
var field; |
||||
|
var defs = []; |
||||
|
options = options || {}; |
||||
|
record._changes = record._changes || {}; |
||||
|
if (!options.doNotSetDirty) { |
||||
|
record._isDirty = true; |
||||
|
} |
||||
|
var initialData = {}; |
||||
|
this._visitChildren(record, function (elem) { |
||||
|
initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes')); |
||||
|
}); |
||||
|
// apply changes to local data
|
||||
|
for (var fieldName in changes) { |
||||
|
field = record.fields[fieldName]; |
||||
|
|
||||
|
if (field && (field.type === 'one2many' || field.type === 'many2many')) { |
||||
|
if (fieldName == "product_custom_attribute_value_ids") { |
||||
|
continue |
||||
|
}; |
||||
|
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options)); |
||||
|
} else if (field && (field.type === 'many2one' || field.type === 'reference')) { |
||||
|
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName], options)); |
||||
|
} else { |
||||
|
record._changes[fieldName] = changes[fieldName]; |
||||
|
} |
||||
|
} |
||||
|
if (options.notifyChange === false) { |
||||
|
return Promise.all(defs).then(function () { |
||||
|
return Promise.resolve(_.keys(changes)); |
||||
|
}); |
||||
|
} |
||||
|
return Promise.all(defs).then(function () { |
||||
|
var onChangeFields = []; // the fields that have changed and that have an on_change
|
||||
|
|
||||
|
for (var fieldName in changes) { |
||||
|
field = record.fields[fieldName]; |
||||
|
if (field && field.onChange) { |
||||
|
var isX2Many = field.type === 'one2many' || field.type === 'many2many'; |
||||
|
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) { |
||||
|
onChangeFields.push(fieldName); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return new Promise(function (resolve, reject) { |
||||
|
if (onChangeFields.length) { |
||||
|
self._performOnChange(record, onChangeFields, { viewType: options.viewType }) |
||||
|
.then(function (result) { |
||||
|
delete record._warning; |
||||
|
resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); |
||||
|
}).guardedCatch(function () { |
||||
|
self._visitChildren(record, function (elem) { |
||||
|
_.extend(elem, initialData[elem.id]); |
||||
|
}); |
||||
|
reject(); |
||||
|
}); |
||||
|
} else { |
||||
|
resolve(_.keys(changes)); |
||||
|
} |
||||
|
}).then(function (fieldNames) { |
||||
|
return self._fetchSpecialData(record).then(function (fieldNames2) { |
||||
|
// Return the names of the fields that changed (onchange or
|
||||
|
// associated special data change)
|
||||
|
return _.union(fieldNames, fieldNames2); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,437 @@ |
|||||
|
/** @odoo-module */ |
||||
|
|
||||
|
import ajax from 'web.ajax'; |
||||
|
import Dialog from 'web.Dialog'; |
||||
|
import OwlDialog from 'web.OwlDialog'; |
||||
|
import ServicesMixin from 'web.ServicesMixin'; |
||||
|
import VariantMixin from 'sale.VariantMixin'; |
||||
|
|
||||
|
export const OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { |
||||
|
events: _.extend({}, Dialog.prototype.events, VariantMixin.events, { |
||||
|
'click a.js_add, a.js_remove': '_onAddOrRemoveOption', |
||||
|
'click button.js_add_cart_json': 'onClickAddCartJSON', |
||||
|
'change .in_cart input.js_quantity': '_onChangeQuantity', |
||||
|
'change .js_raw_price': '_computePriceTotal' |
||||
|
}), |
||||
|
/** |
||||
|
* Initializes the optional products modal |
||||
|
**/ |
||||
|
init: function (parent, params) { |
||||
|
var self = this; |
||||
|
|
||||
|
var options = _.extend({ |
||||
|
size: 'large', |
||||
|
buttons: [{ |
||||
|
text: params.okButtonText, |
||||
|
click: this._onConfirmButtonClick, |
||||
|
// the o_sale_product_configurator_edit class is used for tours.
|
||||
|
classes: 'btn-primary o_sale_product_configurator_edit' |
||||
|
}, { |
||||
|
text: params.cancelButtonText, |
||||
|
click: this._onCancelButtonClick |
||||
|
}], |
||||
|
technical: !params.isWebsite, |
||||
|
}, params || {}); |
||||
|
|
||||
|
this._super(parent, options); |
||||
|
|
||||
|
this.context = params.context; |
||||
|
this.rootProduct = params.rootProduct; |
||||
|
this.container = parent; |
||||
|
this.pricelistId = params.pricelistId; |
||||
|
this.previousModalHeight = params.previousModalHeight; |
||||
|
this.mode = params.mode; |
||||
|
this.dialogClass = 'oe_advanced_configurator_modal'; |
||||
|
this._productImageField = 'image_128'; |
||||
|
|
||||
|
this._opened.then(function () { |
||||
|
if (self.previousModalHeight) { |
||||
|
self.$el.closest('.modal-content').css('min-height', self.previousModalHeight + 'px'); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
willStart: function () { |
||||
|
var self = this; |
||||
|
|
||||
|
var uri = this._getUri("/sale_product_configurator/show_advanced_configurator"); |
||||
|
var getModalContent = ajax.jsonRpc(uri, 'call', { |
||||
|
mode: self.mode, |
||||
|
product_id: self.rootProduct.product_id, |
||||
|
variant_values: self.rootProduct.variant_values, |
||||
|
product_custom_attribute_values: self.rootProduct.product_custom_attribute_values, |
||||
|
pricelist_id: self.pricelistId || false, |
||||
|
add_qty: self.rootProduct.quantity, |
||||
|
force_dialog: self.forceDialog, |
||||
|
context: _.extend({'quantity': self.rootProduct.quantity}, this.context), |
||||
|
}) |
||||
|
.then(function (modalContent) { |
||||
|
if (modalContent) { |
||||
|
var $modalContent = $(modalContent); |
||||
|
$modalContent = self._postProcessContent($modalContent); |
||||
|
self.$content = $modalContent; |
||||
|
} else { |
||||
|
self.trigger('options_empty'); |
||||
|
self.preventOpening = true; |
||||
|
} |
||||
|
}); |
||||
|
var parentInit = self._super.apply(self, arguments); |
||||
|
return Promise.all([getModalContent, parentInit]); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* This is overridden to append the modal to the provided container (see init("parent")). |
||||
|
* We need this to have the modal contained in the web shop product form. |
||||
|
* The additional products data will then be contained in the form and sent on submit. |
||||
|
*/ |
||||
|
open: function (options) { |
||||
|
$('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened
|
||||
|
|
||||
|
var self = this; |
||||
|
this.appendTo($('<div/>')).then(function () { |
||||
|
if (!self.preventOpening) { |
||||
|
self.$modal.find(".modal-body").replaceWith(self.$el); |
||||
|
self.$modal.attr('open', true); |
||||
|
self.$modal.removeAttr("aria-hidden"); |
||||
|
self.$modal.appendTo(self.container); |
||||
|
const modal = new Modal(self.$modal[0], { |
||||
|
focus: true, |
||||
|
}); |
||||
|
modal.show(); |
||||
|
self._openedResolver(); |
||||
|
|
||||
|
// Notifies OwlDialog to adjust focus/active properties on owl dialogs
|
||||
|
OwlDialog.display(self); |
||||
|
} |
||||
|
}); |
||||
|
if (options && options.shouldFocusButtons) { |
||||
|
self._onFocusControlButton(); |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
}, |
||||
|
/** |
||||
|
* Will update quantity input to synchronize with previous window |
||||
|
*/ |
||||
|
start: function () { |
||||
|
var def = this._super.apply(this, arguments); |
||||
|
var self = this; |
||||
|
|
||||
|
this.$el.find('input[name="add_qty"]').val(this.rootProduct.quantity); |
||||
|
|
||||
|
// set a unique id to each row for options hierarchy
|
||||
|
var $products = this.$el.find('tr.js_product'); |
||||
|
_.each($products, function (el) { |
||||
|
var $el = $(el); |
||||
|
var uniqueId = self._getUniqueId(el); |
||||
|
var productId = parseInt($el.find('input.product_id').val(), 10); |
||||
|
if (productId === self.rootProduct.product_id) { |
||||
|
self.rootProduct.unique_id = uniqueId; |
||||
|
} else { |
||||
|
el.dataset.parentUniqueId = self.rootProduct.unique_id; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return def.then(function () { |
||||
|
// This has to be triggered to compute the "out of stock" feature
|
||||
|
self._opened.then(function () { |
||||
|
self.triggerVariantChange(self.$el); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
getAndCreateSelectedProducts: async function () { |
||||
|
var self = this; |
||||
|
const products = []; |
||||
|
let productCustomVariantValues; |
||||
|
let noVariantAttributeValues; |
||||
|
for (const product of self.$modal.find('.js_product.in_cart')) { |
||||
|
var $item = $(product); |
||||
|
var quantity = parseFloat($item.find('input[name="add_qty"]').val().replace(',', '.') || 1); |
||||
|
var parentUniqueId = product.dataset.parentUniqueId; |
||||
|
var uniqueId = product.dataset.uniqueId; |
||||
|
productCustomVariantValues = self.getCustomVariantValues($item); |
||||
|
noVariantAttributeValues = self.getNoVariantAttributeValues($item); |
||||
|
const productID = await self.selectOrCreateProduct( |
||||
|
$item, |
||||
|
parseInt($item.find('input.product_id').val(), 10), |
||||
|
parseInt($item.find('input.product_template_id').val(), 10), |
||||
|
true |
||||
|
); |
||||
|
products.push({ |
||||
|
'product_id': productID, |
||||
|
'product_template_id': parseInt($item.find('input.product_template_id').val(), 10), |
||||
|
'quantity': quantity, |
||||
|
'parent_unique_id': parentUniqueId, |
||||
|
'unique_id': uniqueId, |
||||
|
'product_custom_attribute_values': productCustomVariantValues, |
||||
|
'no_variant_attribute_values': noVariantAttributeValues |
||||
|
}); |
||||
|
} |
||||
|
return products; |
||||
|
}, |
||||
|
/** |
||||
|
* Adds the product image and updates the product description |
||||
|
* based on attribute values that are either "no variant" or "custom". |
||||
|
*/ |
||||
|
_postProcessContent: function ($modalContent) { |
||||
|
var productId = this.rootProduct.product_id; |
||||
|
$modalContent |
||||
|
.find('img:first') |
||||
|
.attr("src", "/web/image/product.product/" + productId + "/image_128"); |
||||
|
|
||||
|
if (this.rootProduct && |
||||
|
(this.rootProduct.product_custom_attribute_values || |
||||
|
this.rootProduct.no_variant_attribute_values)) { |
||||
|
var $productDescription = $modalContent |
||||
|
.find('.main_product') |
||||
|
.find('td.td-product_name div.text-muted.small > div:first'); |
||||
|
var $updatedDescription = $('<div/>'); |
||||
|
$updatedDescription.append($('<p>', { |
||||
|
text: $productDescription.text() |
||||
|
})); |
||||
|
$.each(this.rootProduct.product_custom_attribute_values, function () { |
||||
|
if (this.custom_value) { |
||||
|
const $customInput = $modalContent |
||||
|
.find(".main_product [data-is_custom='True']") |
||||
|
.closest(`[data-value_id='${this.custom_product_template_attribute_value_id.res_id}']`); |
||||
|
$customInput.attr('previous_custom_value', this.custom_value); |
||||
|
VariantMixin.handleCustomValues($customInput); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
$.each(this.rootProduct.no_variant_attribute_values, function () { |
||||
|
if (this.is_custom !== 'True') { |
||||
|
$updatedDescription.append($('<div>', { |
||||
|
text: this.attribute_name + ': ' + this.attribute_value_name |
||||
|
})); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
$productDescription.replaceWith($updatedDescription); |
||||
|
} |
||||
|
|
||||
|
return $modalContent; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @private |
||||
|
*/ |
||||
|
_onConfirmButtonClick: function () { |
||||
|
this.trigger('confirm'); |
||||
|
this.close(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @private |
||||
|
*/ |
||||
|
_onCancelButtonClick: function () { |
||||
|
this.trigger('back'); |
||||
|
this.close(); |
||||
|
}, |
||||
|
|
||||
|
_onAddOrRemoveOption: function (ev) { |
||||
|
ev.preventDefault(); |
||||
|
var self = this; |
||||
|
var $target = $(ev.currentTarget); |
||||
|
var $modal = $target.parents('.oe_advanced_configurator_modal'); |
||||
|
var $parent = $target.parents('.js_product:first'); |
||||
|
$parent.find("a.js_add, span.js_remove").toggleClass('d-none'); |
||||
|
$parent.find(".js_remove"); |
||||
|
|
||||
|
var productTemplateId = $parent.find(".product_template_id").val(); |
||||
|
if ($target.hasClass('js_add')) { |
||||
|
self._onAddOption($modal, $parent, productTemplateId); |
||||
|
} else { |
||||
|
self._onRemoveOption($modal, $parent); |
||||
|
} |
||||
|
|
||||
|
self._computePriceTotal(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @param {integer} productTemplateId |
||||
|
*/ |
||||
|
_onAddOption: function ($modal, $parent, productTemplateId) { |
||||
|
var self = this; |
||||
|
var $selectOptionsText = $modal.find('.o_select_options'); |
||||
|
|
||||
|
var parentUniqueId = $parent[0].dataset.parentUniqueId; |
||||
|
var $optionParent = $modal.find('tr.js_product[data-unique-id="' + parentUniqueId + '"]'); |
||||
|
|
||||
|
// remove attribute values selection and update + show quantity input
|
||||
|
$parent.find('.td-product_name').removeAttr("colspan"); |
||||
|
$parent.find('.td-qty').removeClass('d-none'); |
||||
|
|
||||
|
var productCustomVariantValues = self.getCustomVariantValues($parent); |
||||
|
var noVariantAttributeValues = self.getNoVariantAttributeValues($parent); |
||||
|
if (productCustomVariantValues || noVariantAttributeValues) { |
||||
|
var $productDescription = $parent |
||||
|
.find('td.td-product_name div.float-start'); |
||||
|
|
||||
|
var $customAttributeValuesDescription = $('<div>', { |
||||
|
class: 'custom_attribute_values_description text-muted small' |
||||
|
}); |
||||
|
if (productCustomVariantValues.length !== 0 || noVariantAttributeValues.length !== 0) { |
||||
|
$customAttributeValuesDescription.append($('<br/>')); |
||||
|
} |
||||
|
|
||||
|
$.each(productCustomVariantValues, function (){ |
||||
|
$customAttributeValuesDescription.append($('<div>', { |
||||
|
text: this.attribute_value_name + ': ' + this.custom_value |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
$.each(noVariantAttributeValues, function (){ |
||||
|
if (this.is_custom !== 'True'){ |
||||
|
$customAttributeValuesDescription.append($('<div>', { |
||||
|
text: this.attribute_name + ': ' + this.attribute_value_name |
||||
|
})); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
$productDescription.append($customAttributeValuesDescription); |
||||
|
} |
||||
|
|
||||
|
// place it after its parent and its parent options
|
||||
|
var $tmpOptionParent = $optionParent; |
||||
|
while ($tmpOptionParent.length) { |
||||
|
$optionParent = $tmpOptionParent; |
||||
|
$tmpOptionParent = $modal.find('tr.js_product.in_cart[data-parent-unique-id="' + $optionParent[0].dataset.uniqueId + '"]').last(); |
||||
|
} |
||||
|
$optionParent.after($parent); |
||||
|
$parent.addClass('in_cart'); |
||||
|
|
||||
|
this.selectOrCreateProduct( |
||||
|
$parent, |
||||
|
$parent.find('.product_id').val(), |
||||
|
productTemplateId, |
||||
|
true |
||||
|
).then(function (productId) { |
||||
|
$parent.find('.product_id').val(productId); |
||||
|
|
||||
|
ajax.jsonRpc(self._getUri("/sale_product_configurator/optional_product_items"), 'call', { |
||||
|
'product_id': productId, |
||||
|
'pricelist_id': self.pricelistId || false, |
||||
|
}).then(function (addedItem) { |
||||
|
var $addedItem = $(addedItem); |
||||
|
$modal.find('tr:last').after($addedItem); |
||||
|
|
||||
|
self.$el.find('input[name="add_qty"]').trigger('change'); |
||||
|
self.triggerVariantChange($addedItem); |
||||
|
|
||||
|
// add a unique id to the new products
|
||||
|
var parentUniqueId = $parent[0].dataset.uniqueId; |
||||
|
var parentQty = $parent.find('input[name="add_qty"]').val(); |
||||
|
$addedItem.filter('.js_product').each(function () { |
||||
|
var $el = $(this); |
||||
|
var uniqueId = self._getUniqueId(this); |
||||
|
this.dataset.uniqueId = uniqueId; |
||||
|
this.dataset.parentUniqueId = parentUniqueId; |
||||
|
$el.find('input[name="add_qty"]').val(parentQty); |
||||
|
}); |
||||
|
|
||||
|
if ($selectOptionsText.nextAll('.js_product').length === 0) { |
||||
|
// no more optional products to select -> hide the header
|
||||
|
$selectOptionsText.hide(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @param {$.Element} $parent |
||||
|
*/ |
||||
|
_onRemoveOption: function ($modal, $parent) { |
||||
|
// restore attribute values selection
|
||||
|
var uniqueId = $parent[0].dataset.parentUniqueId; |
||||
|
var qty = $modal.find('tr.js_product.in_cart[data-unique-id="' + uniqueId + '"]').find('input[name="add_qty"]').val(); |
||||
|
$parent.removeClass('in_cart'); |
||||
|
$parent.find('.td-product_name').attr("colspan", 2); |
||||
|
$parent.find('.td-qty').addClass('d-none'); |
||||
|
$parent.find('input[name="add_qty"]').val(qty); |
||||
|
$parent.find('.custom_attribute_values_description').remove(); |
||||
|
|
||||
|
$modal.find('.o_select_options').show(); |
||||
|
|
||||
|
var productUniqueId = $parent[0].dataset.uniqueId; |
||||
|
this._removeOptionOption($modal, productUniqueId); |
||||
|
|
||||
|
$modal.find('tr:last').after($parent); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @param {integer} optionUniqueId The removed optional product id |
||||
|
*/ |
||||
|
_removeOptionOption: function ($modal, optionUniqueId) { |
||||
|
var self = this; |
||||
|
$modal.find('tr.js_product[data-parent-unique-id="' + optionUniqueId + '"]').each(function () { |
||||
|
var uniqueId = this.dataset.uniqueId; |
||||
|
$(this).remove(); |
||||
|
self._removeOptionOption($modal, uniqueId); |
||||
|
}); |
||||
|
}, |
||||
|
/** |
||||
|
* @override |
||||
|
*/ |
||||
|
_onChangeCombination: function (ev, $parent, combination) { |
||||
|
$parent |
||||
|
.find('.td-product_name .product-name') |
||||
|
.first() |
||||
|
.text(combination.display_name); |
||||
|
|
||||
|
VariantMixin._onChangeCombination.apply(this, arguments); |
||||
|
this._computePriceTotal(); |
||||
|
}, |
||||
|
/** |
||||
|
* @param {MouseEvent} ev |
||||
|
*/ |
||||
|
_onChangeQuantity: function (ev) { |
||||
|
var $product = $(ev.target.closest('tr.js_product')); |
||||
|
var qty = parseFloat($(ev.currentTarget).val()); |
||||
|
|
||||
|
var uniqueId = $product[0].dataset.uniqueId; |
||||
|
this.$el.find('tr.js_product:not(.in_cart)[data-parent-unique-id="' + uniqueId + '"] input[name="add_qty"]').each(function () { |
||||
|
$(this).val(qty); |
||||
|
}); |
||||
|
|
||||
|
if (this._triggerPriceUpdateOnChangeQuantity()) { |
||||
|
this.onChangeAddQuantity(ev); |
||||
|
} |
||||
|
if ($product.hasClass('main_product')) { |
||||
|
this.rootProduct.quantity = qty; |
||||
|
} |
||||
|
this.trigger('update_quantity', this.rootProduct.quantity); |
||||
|
this._computePriceTotal(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* we need to refresh the total price row |
||||
|
*/ |
||||
|
_computePriceTotal: function () { |
||||
|
if (this.$modal.find('.js_price_total').length) { |
||||
|
var price = 0; |
||||
|
this.$modal.find('.js_product.in_cart').each(function () { |
||||
|
var quantity = parseFloat($(this).find('input[name="add_qty"]').first().val().replace(',', '.') || 1); |
||||
|
price += parseFloat($(this).find('.js_raw_price').html()) * quantity; |
||||
|
}); |
||||
|
|
||||
|
this.$modal.find('.js_price_total .oe_currency_value').text( |
||||
|
this._priceToStr(parseFloat(price)) |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
/** |
||||
|
* @private |
||||
|
*/ |
||||
|
_triggerPriceUpdateOnChangeQuantity: function () { |
||||
|
return true; |
||||
|
}, |
||||
|
/** |
||||
|
* @returns {integer} |
||||
|
*/ |
||||
|
_getUniqueId: function (el) { |
||||
|
if (!el.dataset.uniqueId) { |
||||
|
el.dataset.uniqueId = parseInt(_.uniqueId(), 10); |
||||
|
} |
||||
|
return el.dataset.uniqueId; |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,307 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { patch } from "@web/core/utils/patch"; |
||||
|
import { useService } from "@web/core/utils/hooks"; |
||||
|
import { PurchaseOrderLineProductField } from '@purchase_product_matrix/js/purchase_product_field'; |
||||
|
import { OptionalProductsModal } from "@purchase_product_configurator/js/product_configurator"; |
||||
|
|
||||
|
import { |
||||
|
selectOrCreateProduct, |
||||
|
getSelectedVariantValues, |
||||
|
getNoVariantAttributeValues, |
||||
|
} from "sale.VariantMixin"; |
||||
|
|
||||
|
|
||||
|
patch(PurchaseOrderLineProductField.prototype, 'purchase_product_configurator', { |
||||
|
|
||||
|
setup() { |
||||
|
this._super(...arguments); |
||||
|
|
||||
|
this.rpc = useService("rpc"); |
||||
|
this.ui = useService("ui"); |
||||
|
}, |
||||
|
|
||||
|
async _onProductTemplateUpdate() { |
||||
|
const result = await this.orm.call( |
||||
|
'product.template', |
||||
|
'get_single_product_variant', |
||||
|
[this.props.record.data.product_template_id[0]], |
||||
|
{ |
||||
|
context: this.context, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
if(result && result.product_id) { |
||||
|
if (this.props.record.data.product_id != result.product_id.id) { |
||||
|
await this.props.record.update({ |
||||
|
product_id: [result.product_id, result.product_name], |
||||
|
}); |
||||
|
if (result.has_optional_products) { |
||||
|
this._openProductConfigurator('options'); |
||||
|
} else { |
||||
|
this._onProductUpdate(); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
if (!result.mode || result.mode === 'configurator') { |
||||
|
this._openProductConfigurator('add'); |
||||
|
} else { |
||||
|
// only triggered when sale_product_matrix is installed.
|
||||
|
this._openGridConfigurator(result.mode); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async _onProductUpdate() { }, |
||||
|
|
||||
|
_editProductConfiguration() { |
||||
|
this._super(...arguments); |
||||
|
if (this.props.record.data.is_configurable_product) { |
||||
|
this._openProductConfigurator('edit'); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
get isConfigurableTemplate() { |
||||
|
return this._super(...arguments) || this.props.record.data.is_configurable_product; |
||||
|
}, |
||||
|
|
||||
|
async _openProductConfigurator(mode) { |
||||
|
if (mode === 'edit' && this.props.record.data.product_config_mode == 'matrix') { |
||||
|
this._openGridConfigurator('edit'); |
||||
|
} else { |
||||
|
this._super(...arguments); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
async _openProductConfigurator(mode) { |
||||
|
const PurchaseOrderRecord = this.props.record.model.root; |
||||
|
const pricelistId = PurchaseOrderRecord.data.pricelist_id ? PurchaseOrderRecord.data.pricelist_id[0] : false; |
||||
|
const productTemplateId = this.props.record.data.product_template_id[0]; |
||||
|
const $modal = $( |
||||
|
await this.rpc( |
||||
|
"/purchase_product_configurator/configure", |
||||
|
{ |
||||
|
product_template_id: productTemplateId, |
||||
|
quantity: this.props.record.data.product_qty || 1, |
||||
|
pricelist_id: pricelistId, |
||||
|
product_template_attribute_value_ids: this.props.record.data.product_template_attribute_value_ids.records.map( |
||||
|
record => record.data.id |
||||
|
), |
||||
|
product_no_variant_attribute_value_ids: this.props.record.data.product_no_variant_attribute_value_ids.records.map( |
||||
|
record => record.data.id |
||||
|
), |
||||
|
context: this.context, |
||||
|
}, |
||||
|
) |
||||
|
); |
||||
|
const productSelector = `input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked`; |
||||
|
// TODO VFE drop this selectOrCreate and make it so that
|
||||
|
// get_single_product_variant returns first variant as well.
|
||||
|
// and use specified product on edition mode.
|
||||
|
const productId = await selectOrCreateProduct.call( |
||||
|
this, |
||||
|
$modal, |
||||
|
parseInt($modal.find(productSelector).first().val(), 10), |
||||
|
productTemplateId, |
||||
|
false |
||||
|
); |
||||
|
|
||||
|
$modal.find(productSelector).val(productId); |
||||
|
const variantValues = getSelectedVariantValues($modal); |
||||
|
|
||||
|
const noVariantAttributeValues = getNoVariantAttributeValues($modal); |
||||
|
|
||||
|
const customAttributeValues = this.props.record.data.product_custom_attribute_value_ids.records.map( |
||||
|
record => { |
||||
|
// NOTE: this dumb formatting is necessary to avoid
|
||||
|
// modifying the shared code between frontend & backend for now.
|
||||
|
return { |
||||
|
custom_value: record.data.custom_value, |
||||
|
custom_product_template_attribute_value_id: { |
||||
|
res_id: record.data.custom_product_template_attribute_value_id[0], |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.rootProduct = { |
||||
|
product_id: productId, |
||||
|
product_template_id: productTemplateId, |
||||
|
quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1), |
||||
|
variant_values: variantValues, |
||||
|
product_custom_attribute_values: customAttributeValues, |
||||
|
no_variant_attribute_values: noVariantAttributeValues, |
||||
|
}; |
||||
|
|
||||
|
const optionalProductsModal = new OptionalProductsModal(null, { |
||||
|
rootProduct: this.rootProduct, |
||||
|
pricelistId: pricelistId, |
||||
|
okButtonText: this.env._t("Confirm"), |
||||
|
cancelButtonText: this.env._t("Back"), |
||||
|
title: this.env._t("Configure"), |
||||
|
context: this.context, |
||||
|
mode: mode, |
||||
|
}); |
||||
|
let modalEl; |
||||
|
optionalProductsModal.opened(() => { |
||||
|
modalEl = optionalProductsModal.el; |
||||
|
this.ui.activateElement(modalEl); |
||||
|
}); |
||||
|
|
||||
|
optionalProductsModal.on("closed", null, async () => { |
||||
|
// Wait for the event that caused the close to bubble
|
||||
|
await new Promise(resolve => setTimeout(resolve, 0)); |
||||
|
this.ui.deactivateElement(modalEl); |
||||
|
}); |
||||
|
optionalProductsModal.open(); |
||||
|
|
||||
|
let confirmed = false; |
||||
|
optionalProductsModal.on("confirm", null, async () => { |
||||
|
confirmed = true; |
||||
|
const [ |
||||
|
mainProduct, |
||||
|
...optionalProducts |
||||
|
] = await optionalProductsModal.getAndCreateSelectedProducts(); |
||||
|
await this.props.record.update(await this._convertConfiguratorDataToUpdateData(mainProduct)) |
||||
|
this._onProductUpdate(); |
||||
|
const optionalProductLinesCreationContext = this._convertConfiguratorDataToLinesCreationContext(optionalProducts); |
||||
|
for (let optionalProductLineCreationContext of optionalProductLinesCreationContext) { |
||||
|
const line = await PurchaseOrderRecord.data.order_line.addNew({ |
||||
|
position: 'bottom', |
||||
|
context: optionalProductLineCreationContext, |
||||
|
mode: 'readonly', // whatever but not edit !
|
||||
|
}); |
||||
|
// FIXME: update sets the field dirty otherwise on the next edit and click out it gets deleted
|
||||
|
line.data.product_qty = optionalProductLineCreationContext.default_product_qty; |
||||
|
}; |
||||
|
for (let line of PurchaseOrderRecord.data.order_line.records) { |
||||
|
for (let optionalProductLineCreationContext of optionalProductLinesCreationContext) { |
||||
|
if (line.data.product_id[0] == optionalProductLineCreationContext.default_product_id) { |
||||
|
line.data.product_qty = optionalProductLineCreationContext.default_product_qty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
PurchaseOrderRecord.data.order_line.unselectRecord(); |
||||
|
this.props.record.data.product_qty = mainProduct.quantity; |
||||
|
}); |
||||
|
optionalProductsModal.on("closed", null, () => { |
||||
|
if (confirmed) { |
||||
|
return; |
||||
|
} |
||||
|
if (mode != 'edit') { |
||||
|
this.props.record.update({ |
||||
|
product_template_id: false, |
||||
|
product_id: false, |
||||
|
product_qty: 1.0, |
||||
|
// TODO reset custom/novariant values (and remove onchange logic?)
|
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
async _convertConfiguratorDataToUpdateData(mainProduct) { |
||||
|
const nameGet = await this.orm.nameGet( |
||||
|
'product.product', |
||||
|
[mainProduct.product_id], |
||||
|
{ context: this.context } |
||||
|
); |
||||
|
let result = { |
||||
|
product_id: nameGet[0], |
||||
|
product_qty: mainProduct.quantity, |
||||
|
}; |
||||
|
var customAttributeValues = mainProduct.product_custom_attribute_values; |
||||
|
var customValuesCommands = [{ operation: "DELETE_ALL" }]; |
||||
|
if (customAttributeValues && customAttributeValues.length !== 0) { |
||||
|
_.each(customAttributeValues, function (customValue) { |
||||
|
customValuesCommands.push({ |
||||
|
operation: "CREATE", |
||||
|
context: [ |
||||
|
{ |
||||
|
default_custom_product_template_attribute_value_id: |
||||
|
customValue.custom_product_template_attribute_value_id, |
||||
|
default_custom_value: customValue.custom_value, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
result.product_custom_attribute_value_ids = { |
||||
|
operation: "MULTI", |
||||
|
commands: customValuesCommands, |
||||
|
}; |
||||
|
var noVariantAttributeValues = mainProduct.no_variant_attribute_values; |
||||
|
var noVariantCommands = [{ operation: "DELETE_ALL" }]; |
||||
|
if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) { |
||||
|
var resIds = _.map(noVariantAttributeValues, function (noVariantValue) { |
||||
|
return { id: parseInt(noVariantValue.value) }; |
||||
|
}); |
||||
|
noVariantCommands.push({ |
||||
|
operation: "ADD_M2M", |
||||
|
ids: resIds, |
||||
|
}); |
||||
|
} |
||||
|
result.product_no_variant_attribute_value_ids = { |
||||
|
operation: "MULTI", |
||||
|
commands: noVariantCommands, |
||||
|
}; |
||||
|
return result; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Will map the optional products data to sale.order.line |
||||
|
*/ |
||||
|
_convertConfiguratorDataToLinesCreationContext: function (optionalProductsData) { |
||||
|
return optionalProductsData.map(productData => { |
||||
|
return { |
||||
|
default_product_id: productData.product_id, |
||||
|
default_product_template_id: productData.product_template_id, |
||||
|
default_product_qty: parseFloat(productData.quantity), |
||||
|
default_product_no_variant_attribute_value_ids: productData.no_variant_attribute_values.map( |
||||
|
noVariantAttributeData => { |
||||
|
return [4, parseInt(noVariantAttributeData.value)]; |
||||
|
} |
||||
|
), |
||||
|
default_product_custom_attribute_value_ids: productData.product_custom_attribute_values.map( |
||||
|
customAttributeData => { |
||||
|
return [ |
||||
|
0, |
||||
|
0, |
||||
|
{ |
||||
|
custom_product_template_attribute_value_id: |
||||
|
customAttributeData.custom_product_template_attribute_value_id, |
||||
|
custom_value: customAttributeData.custom_value, |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
) |
||||
|
}; |
||||
|
}); |
||||
|
}, |
||||
|
async _openGridConfigurator(mode) { |
||||
|
const PurchaseOrderRecord = this.props.record.model.root; |
||||
|
|
||||
|
// fetch matrix information from server;
|
||||
|
await PurchaseOrderRecord.update({ |
||||
|
grid_product_tmpl_id: this.props.record.data.product_template_id, |
||||
|
}); |
||||
|
let updatedLineAttributes = []; |
||||
|
if (mode === 'edit') { |
||||
|
// provide attributes of edited line to automatically focus on matching cell in the matrix
|
||||
|
for (let ptnvav of this.props.record.data.product_no_variant_attribute_value_ids.records) { |
||||
|
updatedLineAttributes.push(ptnvav.data.id); |
||||
|
} |
||||
|
for (let ptav of this.props.record.data.product_template_attribute_value_ids.records) { |
||||
|
updatedLineAttributes.push(ptav.data.id); |
||||
|
} |
||||
|
updatedLineAttributes.sort((a, b) => { return a - b; }); |
||||
|
} |
||||
|
this._openMatrixConfigurator( |
||||
|
PurchaseOrderRecord.data.grid, |
||||
|
this.props.record.data.product_template_id[0], |
||||
|
updatedLineAttributes, |
||||
|
); |
||||
|
if (mode !== 'edit') { |
||||
|
// remove new line used to open the matrix
|
||||
|
PurchaseOrderRecord.data.order_line.removeRecord(this.props.record); |
||||
|
} |
||||
|
}, |
||||
|
}); |
@ -0,0 +1,221 @@ |
|||||
|
<?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,21 @@ |
|||||
|
<?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.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" attrs="{'invisible': [('purchase_ok', '=', False)]}"> |
||||
|
<group string="Purchase Variant Selection"> |
||||
|
<field name="has_configurable_attributes" /> |
||||
|
<field name="product_config_mode" 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.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> |