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