diff --git a/purchase_product_configurator/README.rst b/purchase_product_configurator/README.rst new file mode 100644 index 000000000..4f787876d --- /dev/null +++ b/purchase_product_configurator/README.rst @@ -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 `__ + +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 `__ + +Further information +=================== +HTML Description: ``__ diff --git a/purchase_product_configurator/__init__.py b/purchase_product_configurator/__init__.py new file mode 100644 index 000000000..c11d9aacb --- /dev/null +++ b/purchase_product_configurator/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies() +# 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 . +# +############################################################################# +from . import controllers +from . import models diff --git a/purchase_product_configurator/__manifest__.py b/purchase_product_configurator/__manifest__.py new file mode 100644 index 000000000..ca93442a3 --- /dev/null +++ b/purchase_product_configurator/__manifest__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies() +# 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 . +# +############################################################################# +{ + '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 +} diff --git a/purchase_product_configurator/controllers/__init__.py b/purchase_product_configurator/controllers/__init__.py new file mode 100644 index 000000000..7a7529ac6 --- /dev/null +++ b/purchase_product_configurator/controllers/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies(). +# 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 . +# +############################################################################# +from . import purchase_product_configurator diff --git a/purchase_product_configurator/controllers/purchase_product_configurator.py b/purchase_product_configurator/controllers/purchase_product_configurator.py new file mode 100644 index 000000000..78b67dca6 --- /dev/null +++ b/purchase_product_configurator/controllers/purchase_product_configurator.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies(). +# 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 . +# +############################################################################# +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)) diff --git a/purchase_product_configurator/doc/RELEASE_NOTES.md b/purchase_product_configurator/doc/RELEASE_NOTES.md new file mode 100644 index 000000000..debdeefca --- /dev/null +++ b/purchase_product_configurator/doc/RELEASE_NOTES.md @@ -0,0 +1,8 @@ +## Module + +#### 23.07.2023 +#### Version 16.0.1.0.0 +#### ADD +- Initial Commit for Purchase Product Configurator. + + diff --git a/purchase_product_configurator/models/__init__.py b/purchase_product_configurator/models/__init__.py new file mode 100644 index 000000000..22d9c19d1 --- /dev/null +++ b/purchase_product_configurator/models/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies() +# 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 . +# +############################################################################# +from . import product_attribute +from . import product_template +from . import purchase_order_line diff --git a/purchase_product_configurator/models/product_attribute.py b/purchase_product_configurator/models/product_attribute.py new file mode 100644 index 000000000..d859dd270 --- /dev/null +++ b/purchase_product_configurator/models/product_attribute.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies(). +# 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 . +# +############################################################################# +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") diff --git a/purchase_product_configurator/models/product_template.py b/purchase_product_configurator/models/product_template.py new file mode 100644 index 000000000..e20c1d884 --- /dev/null +++ b/purchase_product_configurator/models/product_template.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies(). +# 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 . +# +############################################################################# +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 diff --git a/purchase_product_configurator/models/purchase_order_line.py b/purchase_product_configurator/models/purchase_order_line.py new file mode 100644 index 000000000..d834392a6 --- /dev/null +++ b/purchase_product_configurator/models/purchase_order_line.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +############################################################################# +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2023-TODAY Cybrosys Technologies(). +# 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 . +# +############################################################################# +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 diff --git a/purchase_product_configurator/static/description/assets/icons/check.png b/purchase_product_configurator/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/check.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/chevron.png b/purchase_product_configurator/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/chevron.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/cogs.png b/purchase_product_configurator/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/cogs.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/consultation.png b/purchase_product_configurator/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/consultation.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/ecom-black.png b/purchase_product_configurator/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/ecom-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/education-black.png b/purchase_product_configurator/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/education-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/hotel-black.png b/purchase_product_configurator/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/hotel-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/license.png b/purchase_product_configurator/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/license.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/lifebuoy.png b/purchase_product_configurator/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/lifebuoy.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/manufacturing-black.png b/purchase_product_configurator/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/manufacturing-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/pos-black.png b/purchase_product_configurator/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/pos-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/puzzle.png b/purchase_product_configurator/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/puzzle.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/restaurant-black.png b/purchase_product_configurator/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/restaurant-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/service-black.png b/purchase_product_configurator/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/service-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/trading-black.png b/purchase_product_configurator/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/trading-black.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/training.png b/purchase_product_configurator/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/training.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/update.png b/purchase_product_configurator/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/update.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/user.png b/purchase_product_configurator/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/user.png differ diff --git a/purchase_product_configurator/static/description/assets/icons/wrench.png b/purchase_product_configurator/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/purchase_product_configurator/static/description/assets/icons/wrench.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/categories.png b/purchase_product_configurator/static/description/assets/misc/categories.png new file mode 100644 index 000000000..bedf1e0b1 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/categories.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/check-box.png b/purchase_product_configurator/static/description/assets/misc/check-box.png new file mode 100644 index 000000000..42caf24b9 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/check-box.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/compass.png b/purchase_product_configurator/static/description/assets/misc/compass.png new file mode 100644 index 000000000..d5fed8faa Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/compass.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/corporate.png b/purchase_product_configurator/static/description/assets/misc/corporate.png new file mode 100644 index 000000000..2eb13edbf Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/corporate.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/customer-support.png b/purchase_product_configurator/static/description/assets/misc/customer-support.png new file mode 100644 index 000000000..79efc72ed Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/customer-support.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/cybrosys-logo.png b/purchase_product_configurator/static/description/assets/misc/cybrosys-logo.png new file mode 100644 index 000000000..cc3cc0ccf Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/cybrosys-logo.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/features.png b/purchase_product_configurator/static/description/assets/misc/features.png new file mode 100644 index 000000000..b41769f77 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/features.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/logo.png b/purchase_product_configurator/static/description/assets/misc/logo.png new file mode 100644 index 000000000..478462d3e Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/logo.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/pictures.png b/purchase_product_configurator/static/description/assets/misc/pictures.png new file mode 100644 index 000000000..56d255fe9 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/pictures.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/pie-chart.png b/purchase_product_configurator/static/description/assets/misc/pie-chart.png new file mode 100644 index 000000000..426e05244 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/pie-chart.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/right-arrow.png b/purchase_product_configurator/static/description/assets/misc/right-arrow.png new file mode 100644 index 000000000..730984a06 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/right-arrow.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/star.png b/purchase_product_configurator/static/description/assets/misc/star.png new file mode 100644 index 000000000..2eb9ab29f Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/star.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/support.png b/purchase_product_configurator/static/description/assets/misc/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/support.png differ diff --git a/purchase_product_configurator/static/description/assets/misc/whatsapp.png b/purchase_product_configurator/static/description/assets/misc/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/misc/whatsapp.png differ diff --git a/purchase_product_configurator/static/description/assets/modules/1.png b/purchase_product_configurator/static/description/assets/modules/1.png new file mode 100644 index 000000000..359d3e4d6 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/1.png differ diff --git a/purchase_product_configurator/static/description/assets/modules/2.png b/purchase_product_configurator/static/description/assets/modules/2.png new file mode 100644 index 000000000..5c56f0bcd Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/2.png differ diff --git a/purchase_product_configurator/static/description/assets/modules/3.png b/purchase_product_configurator/static/description/assets/modules/3.png new file mode 100644 index 000000000..c1f30354a Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/3.png differ diff --git a/purchase_product_configurator/static/description/assets/modules/4.png b/purchase_product_configurator/static/description/assets/modules/4.png new file mode 100644 index 000000000..33372bdc1 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/4.png differ diff --git a/purchase_product_configurator/static/description/assets/modules/5.gif b/purchase_product_configurator/static/description/assets/modules/5.gif new file mode 100644 index 000000000..d0f36b007 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/5.gif differ diff --git a/purchase_product_configurator/static/description/assets/modules/6.png b/purchase_product_configurator/static/description/assets/modules/6.png new file mode 100644 index 000000000..29d072e4b Binary files /dev/null and b/purchase_product_configurator/static/description/assets/modules/6.png differ diff --git a/purchase_product_configurator/static/description/assets/screenshots/1.png b/purchase_product_configurator/static/description/assets/screenshots/1.png new file mode 100644 index 000000000..6b5258d0a Binary files /dev/null and b/purchase_product_configurator/static/description/assets/screenshots/1.png differ diff --git a/purchase_product_configurator/static/description/assets/screenshots/2.png b/purchase_product_configurator/static/description/assets/screenshots/2.png new file mode 100644 index 000000000..c3c0744dc Binary files /dev/null and b/purchase_product_configurator/static/description/assets/screenshots/2.png differ diff --git a/purchase_product_configurator/static/description/assets/screenshots/3.png b/purchase_product_configurator/static/description/assets/screenshots/3.png new file mode 100644 index 000000000..2876bc343 Binary files /dev/null and b/purchase_product_configurator/static/description/assets/screenshots/3.png differ diff --git a/purchase_product_configurator/static/description/assets/screenshots/hero.gif b/purchase_product_configurator/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..29e771c6d Binary files /dev/null and b/purchase_product_configurator/static/description/assets/screenshots/hero.gif differ diff --git a/purchase_product_configurator/static/description/banner.jpg b/purchase_product_configurator/static/description/banner.jpg new file mode 100644 index 000000000..767e050d1 Binary files /dev/null and b/purchase_product_configurator/static/description/banner.jpg differ diff --git a/purchase_product_configurator/static/description/icon.png b/purchase_product_configurator/static/description/icon.png new file mode 100644 index 000000000..f4c6e4243 Binary files /dev/null and b/purchase_product_configurator/static/description/icon.png differ diff --git a/purchase_product_configurator/static/description/index.html b/purchase_product_configurator/static/description/index.html new file mode 100644 index 000000000..556f8bfc8 --- /dev/null +++ b/purchase_product_configurator/static/description/index.html @@ -0,0 +1,621 @@ +
+ +
+ +
+
+ Community +
+
+
+ + + +
+
+
+

Purchase Product Configurator

+

Configure The Product In Purchase Order Line

+ + +
+
+
+ +
+ + +
+
+ +
+

Explore This + Module

+
+ + + + +
+
+ +
+

Overview +

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

+ Configuration +

+
+
+
+ No additional configuration required. +
+
+ + + + +
+
+ +
+

+ Features +

+
+
+
+
+ + Add Products Using Product Configurator +

+ Allows users to add product to order line using product configurator.

+
+
+ + Choose Variants or Custom Attributes, and Optional + Products +

+ Users can add variants, or custom attributes from the available + options. Also, they can choose the optional products from + the displayed options

+
+
+ + Switch Between Product Configurator and Grid View +

+ Users can switch between the Product Configurator and + Grid view options according to their needs to add products to + the order line +

+
+ +
+
+ + + +
+
+ +
+

Screenshots +

+
+
+
+ +
+

Add Products Using Product Configurator

+ +
+ +
+

Choose Variants or Custom Attributes, and Optional + Products

+ + +
+ +
+

Switch Between Product Configurator and Grid View

+ + +
+ +
+
+ + + +
+
+ +
+

+ Related + Products +

+
+
+
+ +
+
+ + + + +
+
+ +
+

Our Services +

+
+ +
+
+
+
+ +
+
+ Odoo + Customization
+
+ +
+
+ +
+
+ Odoo + Implementation
+
+ +
+
+ +
+
+ Odoo + Support
+
+ + +
+
+ +
+
+ Hire + Odoo + Developer
+
+ +
+
+ +
+
+ Odoo + Integration
+
+ +
+
+ +
+
+ Odoo + Migration
+
+ + +
+
+ +
+
+ Odoo + Consultancy
+
+ +
+
+ +
+
+ Odoo + Implementation
+
+ +
+
+ +
+
+ Odoo + Licensing Consultancy
+
+
+ +
+ + + + + +
+
+ +
+

Our + Industries +

+
+ +
+
+
+
+ +
+ Trading +
+

+ Easily procure + and + sell your products

+
+
+ +
+
+ +
+ POS +
+

+ Easy + configuration + and convivial experience

+
+
+ +
+
+ +
+ Education +
+

+ A platform for + educational management

+
+
+ +
+
+ +
+ Manufacturing +
+

+ Plan, track and + schedule your operations

+
+
+ +
+
+ +
+ E-commerce & Website +
+

+ Mobile + friendly, + awe-inspiring product pages

+
+
+ +
+
+ +
+ Service Management +
+

+ Keep track of + services and invoice

+
+
+ +
+
+ +
+ Restaurant +
+

+ Run your bar or + restaurant methodically

+
+
+ +
+
+ +
+ Hotel Management +
+

+ An + all-inclusive + hotel management application

+
+
+
+
+ + + + +
+
+ +
+

Support +

+
+
+
+
+
+
+ +
+
+

Need Help?

+

Got questions or need help? Get in touch.

+ +

+ odoo@cybrosys.com

+
+
+
+
+
+
+
+ +
+
+

WhatsApp

+

Say hi to us on WhatsApp!

+ +

+91 86068 + 27707

+
+
+
+
+
+
+
+ +
+
+
+ diff --git a/purchase_product_configurator/static/src/js/basic_model.js b/purchase_product_configurator/static/src/js/basic_model.js new file mode 100644 index 000000000..1974aa8e3 --- /dev/null +++ b/purchase_product_configurator/static/src/js/basic_model.js @@ -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); + }); + }); + }); + }, + }) +}) \ No newline at end of file diff --git a/purchase_product_configurator/static/src/js/product_configurator.js b/purchase_product_configurator/static/src/js/product_configurator.js new file mode 100644 index 000000000..0d6f229b3 --- /dev/null +++ b/purchase_product_configurator/static/src/js/product_configurator.js @@ -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($('
')).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 = $('
'); + $updatedDescription.append($('

', { + 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($('

', { + 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 = $('
', { + class: 'custom_attribute_values_description text-muted small' + }); + if (productCustomVariantValues.length !== 0 || noVariantAttributeValues.length !== 0) { + $customAttributeValuesDescription.append($('
')); + } + + $.each(productCustomVariantValues, function (){ + $customAttributeValuesDescription.append($('
', { + text: this.attribute_value_name + ': ' + this.custom_value + })); + }); + + $.each(noVariantAttributeValues, function (){ + if (this.is_custom !== 'True'){ + $customAttributeValuesDescription.append($('
', { + 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; + }, +}); diff --git a/purchase_product_configurator/static/src/js/purchase_product_field.js b/purchase_product_configurator/static/src/js/purchase_product_field.js new file mode 100644 index 000000000..5900abe48 --- /dev/null +++ b/purchase_product_configurator/static/src/js/purchase_product_field.js @@ -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); + } + }, +}); diff --git a/purchase_product_configurator/views/optional_product_template.xml b/purchase_product_configurator/views/optional_product_template.xml new file mode 100644 index 000000000..032a5ff16 --- /dev/null +++ b/purchase_product_configurator/views/optional_product_template.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + diff --git a/purchase_product_configurator/views/product_template_views.xml b/purchase_product_configurator/views/product_template_views.xml new file mode 100644 index 000000000..12f397012 --- /dev/null +++ b/purchase_product_configurator/views/product_template_views.xml @@ -0,0 +1,21 @@ + + + + + product.template.form.inherit.purchase.product.configurator + product.template + + + + + + + + + + + + + diff --git a/purchase_product_configurator/views/purchase_order_views.xml b/purchase_product_configurator/views/purchase_order_views.xml new file mode 100644 index 000000000..cfd4d5f33 --- /dev/null +++ b/purchase_product_configurator/views/purchase_order_views.xml @@ -0,0 +1,18 @@ + + + + + purchase.order.view.form.purchase.product.configurator + purchase.order + + + + + + + + +