Browse Source

Jan 16 [ADD] : Initial Commit 'purchase_product_configurator'

pull/302/head
AjmalCybro 1 year ago
parent
commit
0f24700b45
  1. 39
      purchase_product_configurator/README.rst
  2. 23
      purchase_product_configurator/__init__.py
  3. 51
      purchase_product_configurator/__manifest__.py
  4. 22
      purchase_product_configurator/controllers/__init__.py
  5. 142
      purchase_product_configurator/controllers/purchase_product_configurator.py
  6. 8
      purchase_product_configurator/doc/RELEASE_NOTES.md
  7. 24
      purchase_product_configurator/models/__init__.py
  8. 35
      purchase_product_configurator/models/product_attribute.py
  9. 88
      purchase_product_configurator/models/product_template.py
  10. 64
      purchase_product_configurator/models/purchase_order_line.py
  11. BIN
      purchase_product_configurator/static/description/assets/icons/check.png
  12. BIN
      purchase_product_configurator/static/description/assets/icons/chevron.png
  13. BIN
      purchase_product_configurator/static/description/assets/icons/cogs.png
  14. BIN
      purchase_product_configurator/static/description/assets/icons/consultation.png
  15. BIN
      purchase_product_configurator/static/description/assets/icons/ecom-black.png
  16. BIN
      purchase_product_configurator/static/description/assets/icons/education-black.png
  17. BIN
      purchase_product_configurator/static/description/assets/icons/hotel-black.png
  18. BIN
      purchase_product_configurator/static/description/assets/icons/license.png
  19. BIN
      purchase_product_configurator/static/description/assets/icons/lifebuoy.png
  20. BIN
      purchase_product_configurator/static/description/assets/icons/manufacturing-black.png
  21. BIN
      purchase_product_configurator/static/description/assets/icons/pos-black.png
  22. BIN
      purchase_product_configurator/static/description/assets/icons/puzzle.png
  23. BIN
      purchase_product_configurator/static/description/assets/icons/restaurant-black.png
  24. BIN
      purchase_product_configurator/static/description/assets/icons/service-black.png
  25. BIN
      purchase_product_configurator/static/description/assets/icons/trading-black.png
  26. BIN
      purchase_product_configurator/static/description/assets/icons/training.png
  27. BIN
      purchase_product_configurator/static/description/assets/icons/update.png
  28. BIN
      purchase_product_configurator/static/description/assets/icons/user.png
  29. BIN
      purchase_product_configurator/static/description/assets/icons/wrench.png
  30. BIN
      purchase_product_configurator/static/description/assets/misc/categories.png
  31. BIN
      purchase_product_configurator/static/description/assets/misc/check-box.png
  32. BIN
      purchase_product_configurator/static/description/assets/misc/compass.png
  33. BIN
      purchase_product_configurator/static/description/assets/misc/corporate.png
  34. BIN
      purchase_product_configurator/static/description/assets/misc/customer-support.png
  35. BIN
      purchase_product_configurator/static/description/assets/misc/cybrosys-logo.png
  36. BIN
      purchase_product_configurator/static/description/assets/misc/features.png
  37. BIN
      purchase_product_configurator/static/description/assets/misc/logo.png
  38. BIN
      purchase_product_configurator/static/description/assets/misc/pictures.png
  39. BIN
      purchase_product_configurator/static/description/assets/misc/pie-chart.png
  40. BIN
      purchase_product_configurator/static/description/assets/misc/right-arrow.png
  41. BIN
      purchase_product_configurator/static/description/assets/misc/star.png
  42. BIN
      purchase_product_configurator/static/description/assets/misc/support.png
  43. BIN
      purchase_product_configurator/static/description/assets/misc/whatsapp.png
  44. BIN
      purchase_product_configurator/static/description/assets/modules/1.png
  45. BIN
      purchase_product_configurator/static/description/assets/modules/2.png
  46. BIN
      purchase_product_configurator/static/description/assets/modules/3.png
  47. BIN
      purchase_product_configurator/static/description/assets/modules/4.png
  48. BIN
      purchase_product_configurator/static/description/assets/modules/5.gif
  49. BIN
      purchase_product_configurator/static/description/assets/modules/6.png
  50. BIN
      purchase_product_configurator/static/description/assets/screenshots/1.png
  51. BIN
      purchase_product_configurator/static/description/assets/screenshots/2.png
  52. BIN
      purchase_product_configurator/static/description/assets/screenshots/3.png
  53. BIN
      purchase_product_configurator/static/description/assets/screenshots/hero.gif
  54. BIN
      purchase_product_configurator/static/description/banner.jpg
  55. BIN
      purchase_product_configurator/static/description/icon.png
  56. 621
      purchase_product_configurator/static/description/index.html
  57. 84
      purchase_product_configurator/static/src/js/basic_model.js
  58. 437
      purchase_product_configurator/static/src/js/product_configurator.js
  59. 307
      purchase_product_configurator/static/src/js/purchase_product_field.js
  60. 221
      purchase_product_configurator/views/optional_product_template.xml
  61. 21
      purchase_product_configurator/views/product_template_views.xml
  62. 18
      purchase_product_configurator/views/purchase_order_views.xml

39
purchase_product_configurator/README.rst

@ -0,0 +1,39 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
Purchase Product Configurator
=============================
The module adds the product configurator feature to the purchase module,
which is only present in sale module by default.
Configuration
=============
* No additional configurations are required
* `Cybrosys Techno Solutions <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>`__

23
purchase_product_configurator/__init__.py

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

51
purchase_product_configurator/__manifest__.py

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

22
purchase_product_configurator/controllers/__init__.py

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

142
purchase_product_configurator/controllers/purchase_product_configurator.py

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<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))

8
purchase_product_configurator/doc/RELEASE_NOTES.md

@ -0,0 +1,8 @@
## Module <purchase_product_configurator>
#### 23.07.2023
#### Version 16.0.1.0.0
#### ADD
- Initial Commit for Purchase Product Configurator.

24
purchase_product_configurator/models/__init__.py

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

35
purchase_product_configurator/models/product_attribute.py

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

88
purchase_product_configurator/models/product_template.py

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

64
purchase_product_configurator/models/purchase_order_line.py

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2023-TODAY Cybrosys Technologies(<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

BIN
purchase_product_configurator/static/description/assets/icons/check.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
purchase_product_configurator/static/description/assets/icons/chevron.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

BIN
purchase_product_configurator/static/description/assets/icons/cogs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
purchase_product_configurator/static/description/assets/icons/consultation.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
purchase_product_configurator/static/description/assets/icons/ecom-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

BIN
purchase_product_configurator/static/description/assets/icons/education-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

BIN
purchase_product_configurator/static/description/assets/icons/hotel-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

BIN
purchase_product_configurator/static/description/assets/icons/license.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
purchase_product_configurator/static/description/assets/icons/lifebuoy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
purchase_product_configurator/static/description/assets/icons/manufacturing-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

BIN
purchase_product_configurator/static/description/assets/icons/pos-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
purchase_product_configurator/static/description/assets/icons/puzzle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

BIN
purchase_product_configurator/static/description/assets/icons/restaurant-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

BIN
purchase_product_configurator/static/description/assets/icons/service-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

BIN
purchase_product_configurator/static/description/assets/icons/trading-black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

BIN
purchase_product_configurator/static/description/assets/icons/training.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

BIN
purchase_product_configurator/static/description/assets/icons/update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
purchase_product_configurator/static/description/assets/icons/user.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

BIN
purchase_product_configurator/static/description/assets/icons/wrench.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
purchase_product_configurator/static/description/assets/misc/categories.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
purchase_product_configurator/static/description/assets/misc/check-box.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
purchase_product_configurator/static/description/assets/misc/compass.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
purchase_product_configurator/static/description/assets/misc/corporate.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
purchase_product_configurator/static/description/assets/misc/customer-support.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
purchase_product_configurator/static/description/assets/misc/cybrosys-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
purchase_product_configurator/static/description/assets/misc/features.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

BIN
purchase_product_configurator/static/description/assets/misc/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
purchase_product_configurator/static/description/assets/misc/pictures.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
purchase_product_configurator/static/description/assets/misc/pie-chart.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
purchase_product_configurator/static/description/assets/misc/right-arrow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

BIN
purchase_product_configurator/static/description/assets/misc/star.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
purchase_product_configurator/static/description/assets/misc/support.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
purchase_product_configurator/static/description/assets/misc/whatsapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
purchase_product_configurator/static/description/assets/modules/1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
purchase_product_configurator/static/description/assets/modules/2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
purchase_product_configurator/static/description/assets/modules/3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
purchase_product_configurator/static/description/assets/modules/4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
purchase_product_configurator/static/description/assets/modules/5.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
purchase_product_configurator/static/description/assets/modules/6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
purchase_product_configurator/static/description/assets/screenshots/1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
purchase_product_configurator/static/description/assets/screenshots/2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
purchase_product_configurator/static/description/assets/screenshots/3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
purchase_product_configurator/static/description/assets/screenshots/hero.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
purchase_product_configurator/static/description/banner.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
purchase_product_configurator/static/description/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

621
purchase_product_configurator/static/description/index.html

@ -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 &amp; 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 -->

84
purchase_product_configurator/static/src/js/basic_model.js

@ -0,0 +1,84 @@
odoo.define('purchase_product_configurator.BasicModel', function (require) {
'use strict';
var BasicModel = require('web.BasicModel');
const { patch, unpatch } = require('web.utils');
patch(BasicModel.prototype, 'purchase_product_configurator/static/src/js/basic_model.js', {
/**
* Patch the _applyChange method of the BasicModel to handle specific field changes.
*/
_applyChange (recordID, changes, options) {
var self = this;
var record = this.localData[recordID];
var field;
var defs = [];
options = options || {};
record._changes = record._changes || {};
if (!options.doNotSetDirty) {
record._isDirty = true;
}
var initialData = {};
this._visitChildren(record, function (elem) {
initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes'));
});
// apply changes to local data
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field && (field.type === 'one2many' || field.type === 'many2many')) {
if (fieldName == "product_custom_attribute_value_ids") {
continue
};
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options));
} else if (field && (field.type === 'many2one' || field.type === 'reference')) {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName], options));
} else {
record._changes[fieldName] = changes[fieldName];
}
}
if (options.notifyChange === false) {
return Promise.all(defs).then(function () {
return Promise.resolve(_.keys(changes));
});
}
return Promise.all(defs).then(function () {
var onChangeFields = []; // the fields that have changed and that have an on_change
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field && field.onChange) {
var isX2Many = field.type === 'one2many' || field.type === 'many2many';
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) {
onChangeFields.push(fieldName);
}
}
}
return new Promise(function (resolve, reject) {
if (onChangeFields.length) {
self._performOnChange(record, onChangeFields, { viewType: options.viewType })
.then(function (result) {
delete record._warning;
resolve(_.keys(changes).concat(Object.keys(result && result.value || {})));
}).guardedCatch(function () {
self._visitChildren(record, function (elem) {
_.extend(elem, initialData[elem.id]);
});
reject();
});
} else {
resolve(_.keys(changes));
}
}).then(function (fieldNames) {
return self._fetchSpecialData(record).then(function (fieldNames2) {
// Return the names of the fields that changed (onchange or
// associated special data change)
return _.union(fieldNames, fieldNames2);
});
});
});
},
})
})

437
purchase_product_configurator/static/src/js/product_configurator.js

@ -0,0 +1,437 @@
/** @odoo-module */
import ajax from 'web.ajax';
import Dialog from 'web.Dialog';
import OwlDialog from 'web.OwlDialog';
import ServicesMixin from 'web.ServicesMixin';
import VariantMixin from 'sale.VariantMixin';
export const OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
events: _.extend({}, Dialog.prototype.events, VariantMixin.events, {
'click a.js_add, a.js_remove': '_onAddOrRemoveOption',
'click button.js_add_cart_json': 'onClickAddCartJSON',
'change .in_cart input.js_quantity': '_onChangeQuantity',
'change .js_raw_price': '_computePriceTotal'
}),
/**
* Initializes the optional products modal
**/
init: function (parent, params) {
var self = this;
var options = _.extend({
size: 'large',
buttons: [{
text: params.okButtonText,
click: this._onConfirmButtonClick,
// the o_sale_product_configurator_edit class is used for tours.
classes: 'btn-primary o_sale_product_configurator_edit'
}, {
text: params.cancelButtonText,
click: this._onCancelButtonClick
}],
technical: !params.isWebsite,
}, params || {});
this._super(parent, options);
this.context = params.context;
this.rootProduct = params.rootProduct;
this.container = parent;
this.pricelistId = params.pricelistId;
this.previousModalHeight = params.previousModalHeight;
this.mode = params.mode;
this.dialogClass = 'oe_advanced_configurator_modal';
this._productImageField = 'image_128';
this._opened.then(function () {
if (self.previousModalHeight) {
self.$el.closest('.modal-content').css('min-height', self.previousModalHeight + 'px');
}
});
},
willStart: function () {
var self = this;
var uri = this._getUri("/sale_product_configurator/show_advanced_configurator");
var getModalContent = ajax.jsonRpc(uri, 'call', {
mode: self.mode,
product_id: self.rootProduct.product_id,
variant_values: self.rootProduct.variant_values,
product_custom_attribute_values: self.rootProduct.product_custom_attribute_values,
pricelist_id: self.pricelistId || false,
add_qty: self.rootProduct.quantity,
force_dialog: self.forceDialog,
context: _.extend({'quantity': self.rootProduct.quantity}, this.context),
})
.then(function (modalContent) {
if (modalContent) {
var $modalContent = $(modalContent);
$modalContent = self._postProcessContent($modalContent);
self.$content = $modalContent;
} else {
self.trigger('options_empty');
self.preventOpening = true;
}
});
var parentInit = self._super.apply(self, arguments);
return Promise.all([getModalContent, parentInit]);
},
/**
* This is overridden to append the modal to the provided container (see init("parent")).
* We need this to have the modal contained in the web shop product form.
* The additional products data will then be contained in the form and sent on submit.
*/
open: function (options) {
$('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened
var self = this;
this.appendTo($('<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;
},
});

307
purchase_product_configurator/static/src/js/purchase_product_field.js

@ -0,0 +1,307 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
import { PurchaseOrderLineProductField } from '@purchase_product_matrix/js/purchase_product_field';
import { OptionalProductsModal } from "@purchase_product_configurator/js/product_configurator";
import {
selectOrCreateProduct,
getSelectedVariantValues,
getNoVariantAttributeValues,
} from "sale.VariantMixin";
patch(PurchaseOrderLineProductField.prototype, 'purchase_product_configurator', {
setup() {
this._super(...arguments);
this.rpc = useService("rpc");
this.ui = useService("ui");
},
async _onProductTemplateUpdate() {
const result = await this.orm.call(
'product.template',
'get_single_product_variant',
[this.props.record.data.product_template_id[0]],
{
context: this.context,
}
);
if(result && result.product_id) {
if (this.props.record.data.product_id != result.product_id.id) {
await this.props.record.update({
product_id: [result.product_id, result.product_name],
});
if (result.has_optional_products) {
this._openProductConfigurator('options');
} else {
this._onProductUpdate();
}
}
} else {
if (!result.mode || result.mode === 'configurator') {
this._openProductConfigurator('add');
} else {
// only triggered when sale_product_matrix is installed.
this._openGridConfigurator(result.mode);
}
}
},
async _onProductUpdate() { },
_editProductConfiguration() {
this._super(...arguments);
if (this.props.record.data.is_configurable_product) {
this._openProductConfigurator('edit');
}
},
get isConfigurableTemplate() {
return this._super(...arguments) || this.props.record.data.is_configurable_product;
},
async _openProductConfigurator(mode) {
if (mode === 'edit' && this.props.record.data.product_config_mode == 'matrix') {
this._openGridConfigurator('edit');
} else {
this._super(...arguments);
}
},
async _openProductConfigurator(mode) {
const PurchaseOrderRecord = this.props.record.model.root;
const pricelistId = PurchaseOrderRecord.data.pricelist_id ? PurchaseOrderRecord.data.pricelist_id[0] : false;
const productTemplateId = this.props.record.data.product_template_id[0];
const $modal = $(
await this.rpc(
"/purchase_product_configurator/configure",
{
product_template_id: productTemplateId,
quantity: this.props.record.data.product_qty || 1,
pricelist_id: pricelistId,
product_template_attribute_value_ids: this.props.record.data.product_template_attribute_value_ids.records.map(
record => record.data.id
),
product_no_variant_attribute_value_ids: this.props.record.data.product_no_variant_attribute_value_ids.records.map(
record => record.data.id
),
context: this.context,
},
)
);
const productSelector = `input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked`;
// TODO VFE drop this selectOrCreate and make it so that
// get_single_product_variant returns first variant as well.
// and use specified product on edition mode.
const productId = await selectOrCreateProduct.call(
this,
$modal,
parseInt($modal.find(productSelector).first().val(), 10),
productTemplateId,
false
);
$modal.find(productSelector).val(productId);
const variantValues = getSelectedVariantValues($modal);
const noVariantAttributeValues = getNoVariantAttributeValues($modal);
const customAttributeValues = this.props.record.data.product_custom_attribute_value_ids.records.map(
record => {
// NOTE: this dumb formatting is necessary to avoid
// modifying the shared code between frontend & backend for now.
return {
custom_value: record.data.custom_value,
custom_product_template_attribute_value_id: {
res_id: record.data.custom_product_template_attribute_value_id[0],
},
};
}
);
this.rootProduct = {
product_id: productId,
product_template_id: productTemplateId,
quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1),
variant_values: variantValues,
product_custom_attribute_values: customAttributeValues,
no_variant_attribute_values: noVariantAttributeValues,
};
const optionalProductsModal = new OptionalProductsModal(null, {
rootProduct: this.rootProduct,
pricelistId: pricelistId,
okButtonText: this.env._t("Confirm"),
cancelButtonText: this.env._t("Back"),
title: this.env._t("Configure"),
context: this.context,
mode: mode,
});
let modalEl;
optionalProductsModal.opened(() => {
modalEl = optionalProductsModal.el;
this.ui.activateElement(modalEl);
});
optionalProductsModal.on("closed", null, async () => {
// Wait for the event that caused the close to bubble
await new Promise(resolve => setTimeout(resolve, 0));
this.ui.deactivateElement(modalEl);
});
optionalProductsModal.open();
let confirmed = false;
optionalProductsModal.on("confirm", null, async () => {
confirmed = true;
const [
mainProduct,
...optionalProducts
] = await optionalProductsModal.getAndCreateSelectedProducts();
await this.props.record.update(await this._convertConfiguratorDataToUpdateData(mainProduct))
this._onProductUpdate();
const optionalProductLinesCreationContext = this._convertConfiguratorDataToLinesCreationContext(optionalProducts);
for (let optionalProductLineCreationContext of optionalProductLinesCreationContext) {
const line = await PurchaseOrderRecord.data.order_line.addNew({
position: 'bottom',
context: optionalProductLineCreationContext,
mode: 'readonly', // whatever but not edit !
});
// FIXME: update sets the field dirty otherwise on the next edit and click out it gets deleted
line.data.product_qty = optionalProductLineCreationContext.default_product_qty;
};
for (let line of PurchaseOrderRecord.data.order_line.records) {
for (let optionalProductLineCreationContext of optionalProductLinesCreationContext) {
if (line.data.product_id[0] == optionalProductLineCreationContext.default_product_id) {
line.data.product_qty = optionalProductLineCreationContext.default_product_qty;
}
}
}
PurchaseOrderRecord.data.order_line.unselectRecord();
this.props.record.data.product_qty = mainProduct.quantity;
});
optionalProductsModal.on("closed", null, () => {
if (confirmed) {
return;
}
if (mode != 'edit') {
this.props.record.update({
product_template_id: false,
product_id: false,
product_qty: 1.0,
// TODO reset custom/novariant values (and remove onchange logic?)
});
}
});
},
async _convertConfiguratorDataToUpdateData(mainProduct) {
const nameGet = await this.orm.nameGet(
'product.product',
[mainProduct.product_id],
{ context: this.context }
);
let result = {
product_id: nameGet[0],
product_qty: mainProduct.quantity,
};
var customAttributeValues = mainProduct.product_custom_attribute_values;
var customValuesCommands = [{ operation: "DELETE_ALL" }];
if (customAttributeValues && customAttributeValues.length !== 0) {
_.each(customAttributeValues, function (customValue) {
customValuesCommands.push({
operation: "CREATE",
context: [
{
default_custom_product_template_attribute_value_id:
customValue.custom_product_template_attribute_value_id,
default_custom_value: customValue.custom_value,
},
],
});
});
}
result.product_custom_attribute_value_ids = {
operation: "MULTI",
commands: customValuesCommands,
};
var noVariantAttributeValues = mainProduct.no_variant_attribute_values;
var noVariantCommands = [{ operation: "DELETE_ALL" }];
if (noVariantAttributeValues && noVariantAttributeValues.length !== 0) {
var resIds = _.map(noVariantAttributeValues, function (noVariantValue) {
return { id: parseInt(noVariantValue.value) };
});
noVariantCommands.push({
operation: "ADD_M2M",
ids: resIds,
});
}
result.product_no_variant_attribute_value_ids = {
operation: "MULTI",
commands: noVariantCommands,
};
return result;
},
/**
* Will map the optional products data to sale.order.line
*/
_convertConfiguratorDataToLinesCreationContext: function (optionalProductsData) {
return optionalProductsData.map(productData => {
return {
default_product_id: productData.product_id,
default_product_template_id: productData.product_template_id,
default_product_qty: parseFloat(productData.quantity),
default_product_no_variant_attribute_value_ids: productData.no_variant_attribute_values.map(
noVariantAttributeData => {
return [4, parseInt(noVariantAttributeData.value)];
}
),
default_product_custom_attribute_value_ids: productData.product_custom_attribute_values.map(
customAttributeData => {
return [
0,
0,
{
custom_product_template_attribute_value_id:
customAttributeData.custom_product_template_attribute_value_id,
custom_value: customAttributeData.custom_value,
},
];
}
)
};
});
},
async _openGridConfigurator(mode) {
const PurchaseOrderRecord = this.props.record.model.root;
// fetch matrix information from server;
await PurchaseOrderRecord.update({
grid_product_tmpl_id: this.props.record.data.product_template_id,
});
let updatedLineAttributes = [];
if (mode === 'edit') {
// provide attributes of edited line to automatically focus on matching cell in the matrix
for (let ptnvav of this.props.record.data.product_no_variant_attribute_value_ids.records) {
updatedLineAttributes.push(ptnvav.data.id);
}
for (let ptav of this.props.record.data.product_template_attribute_value_ids.records) {
updatedLineAttributes.push(ptav.data.id);
}
updatedLineAttributes.sort((a, b) => { return a - b; });
}
this._openMatrixConfigurator(
PurchaseOrderRecord.data.grid,
this.props.record.data.product_template_id[0],
updatedLineAttributes,
);
if (mode !== 'edit') {
// remove new line used to open the matrix
PurchaseOrderRecord.data.order_line.removeRecord(this.props.record);
}
},
});

221
purchase_product_configurator/views/optional_product_template.xml

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

21
purchase_product_configurator/views/product_template_views.xml

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

18
purchase_product_configurator/views/purchase_order_views.xml

@ -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>
Loading…
Cancel
Save