[MOV] website_sale: move variants logic from sale to website_sale
Currently, `website_sale` uses the same logic as `sale` to show the product variant configuration. This commit aims to separate this logic and let each module handle its own product variant configuration. task-3056806 Part-of: odoo/odoo#106511
This commit is contained in:
@@ -3,4 +3,3 @@
|
||||
|
||||
from . import onboarding
|
||||
from . import portal
|
||||
from . import variant
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class VariantController(http.Controller):
|
||||
@http.route(['/sale/get_combination_info'], type='json', auth="user", methods=['POST'])
|
||||
def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw):
|
||||
combination = request.env['product.template.attribute.value'].browse(combination)
|
||||
pricelist = self._get_pricelist(pricelist_id)
|
||||
cids = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id))
|
||||
allowed_company_ids = [int(cid) for cid in cids.split(',')]
|
||||
ProductTemplate = request.env['product.template'].with_context(allowed_company_ids=allowed_company_ids)
|
||||
if 'context' in kw:
|
||||
ProductTemplate = ProductTemplate.with_context(**kw.get('context'))
|
||||
product_template = ProductTemplate.browse(int(product_template_id))
|
||||
res = product_template._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist)
|
||||
if 'parent_combination' in kw:
|
||||
parent_combination = request.env['product.template.attribute.value'].browse(kw.get('parent_combination'))
|
||||
if not combination.exists() and product_id:
|
||||
product = request.env['product.product'].browse(int(product_id))
|
||||
if product.exists():
|
||||
combination = product.product_template_attribute_value_ids
|
||||
res.update({
|
||||
'is_combination_possible': product_template._is_combination_possible(combination=combination, parent_combination=parent_combination),
|
||||
'parent_exclusions': product_template._get_parent_attribute_exclusions(parent_combination=parent_combination)
|
||||
})
|
||||
return res
|
||||
|
||||
@http.route(['/sale/create_product_variant'], type='json', auth="user", methods=['POST'])
|
||||
def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs):
|
||||
return request.env['product.template'].browse(int(product_template_id)).create_product_variant(json.loads(product_template_attribute_value_ids))
|
||||
|
||||
def _get_pricelist(self, pricelist_id, pricelist_fallback=False):
|
||||
return request.env['product.pricelist'].browse(int(pricelist_id or 0))
|
||||
@@ -17,7 +17,6 @@ It also enables the "optional products" feature.
|
||||
'data': [
|
||||
'views/product_template_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/sale_demo.xml',
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
'views/website_sale_delivery_templates.xml',
|
||||
'views/website_sale_menus.xml',
|
||||
'views/website_sale_delivery_views.xml',
|
||||
'views/variant_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/demo.xml',
|
||||
@@ -58,8 +59,7 @@
|
||||
'website_sale/static/src/scss/website_sale_delivery.scss',
|
||||
'website/static/lib/multirange/multirange_custom.scss',
|
||||
'sale/static/src/scss/sale_portal.scss',
|
||||
'sale/static/src/scss/product_configurator.scss',
|
||||
'sale/static/src/js/variant_mixin.js',
|
||||
'website_sale/static/src/js/sale_variant_mixin.js',
|
||||
'website_sale/static/src/js/variant_mixin.js',
|
||||
'website_sale/static/src/js/website_sale.js',
|
||||
'website_sale/static/src/xml/website_sale.xml',
|
||||
@@ -80,6 +80,7 @@
|
||||
'website_sale/static/src/xml/website_sale_reorder_modal.xml',
|
||||
'website_sale/static/src/js/website_sale_reorder.js',
|
||||
'website_sale/static/src/js/website_sale_delivery.js',
|
||||
'website_sale/static/src/scss/product_configurator.scss',
|
||||
],
|
||||
'web._assets_primary_variables': [
|
||||
'website_sale/static/src/scss/primary_variables.scss',
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from odoo.addons.sale.controllers.variant import VariantController
|
||||
import json
|
||||
|
||||
from odoo.http import request, route, Controller
|
||||
|
||||
|
||||
class WebsiteSaleVariantController(VariantController):
|
||||
@http.route(['/sale/get_combination_info_website'], type='json', auth="public", methods=['POST'], website=True)
|
||||
class WebsiteSaleVariantController(Controller):
|
||||
|
||||
@route('/sale/get_combination_info_website', type='json', auth="public", methods=['POST'], website=True)
|
||||
def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw):
|
||||
"""Special route to use website logic in get_combination_info override.
|
||||
This route is called in JS by appending _website to the base route.
|
||||
"""
|
||||
kw.pop('pricelist_id')
|
||||
combination = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
|
||||
"""Old product configurator logic, only used by frontend configurator, will be deprecated soon"""
|
||||
combination = request.env['product.template.attribute.value'].browse(combination)
|
||||
pricelist = request.website.get_current_pricelist()
|
||||
cids = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id))
|
||||
allowed_company_ids = [int(cid) for cid in cids.split(',')]
|
||||
ProductTemplate = request.env['product.template'].with_context(allowed_company_ids=allowed_company_ids)
|
||||
if 'context' in kw:
|
||||
ProductTemplate = ProductTemplate.with_context(**kw.get('context'))
|
||||
product_template = ProductTemplate.browse(int(product_template_id))
|
||||
combination_info = product_template._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist)
|
||||
if 'parent_combination' in kw:
|
||||
parent_combination = request.env['product.template.attribute.value'].browse(kw.get('parent_combination'))
|
||||
if not combination.exists() and product_id:
|
||||
product = request.env['product.product'].browse(int(product_id))
|
||||
if product.exists():
|
||||
combination = product.product_template_attribute_value_ids
|
||||
combination_info.update({
|
||||
'is_combination_possible': product_template._is_combination_possible(combination=combination, parent_combination=parent_combination),
|
||||
'parent_exclusions': product_template._get_parent_attribute_exclusions(parent_combination=parent_combination)
|
||||
})
|
||||
|
||||
if request.website.google_analytics_key:
|
||||
combination['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination)
|
||||
combination_info['product_tracking_info'] = request.env['product.template'].get_google_analytics_data(combination_info)
|
||||
|
||||
if request.website.product_page_image_width != 'none' and not request.env.context.get('website_sale_no_images', False):
|
||||
carousel_view = request.env['ir.ui.view']._render_template('website_sale.shop_product_images', values={
|
||||
'product': request.env['product.template'].browse(combination['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(combination['product_id']),
|
||||
combination_info['carousel'] = request.env['ir.ui.view']._render_template('website_sale.shop_product_images', values={
|
||||
'product': request.env['product.template'].browse(combination_info['product_template_id']),
|
||||
'product_variant': request.env['product.product'].browse(combination_info['product_id']),
|
||||
'website': request.env['website'].get_current_website(),
|
||||
})
|
||||
combination['carousel'] = carousel_view
|
||||
return combination
|
||||
return combination_info
|
||||
|
||||
@http.route(auth="public")
|
||||
@route('/sale/create_product_variant', type='json', auth="public", methods=['POST'])
|
||||
def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs):
|
||||
"""Override because on the website the public user must access it."""
|
||||
return super(WebsiteSaleVariantController, self).create_product_variant(product_template_id, product_template_attribute_value_ids, **kwargs)
|
||||
"""Old product configurator logic, only used by frontend configurator, will be deprecated soon"""
|
||||
return request.env['product.template'].browse(int(product_template_id)).create_product_variant(json.loads(product_template_attribute_value_ids))
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/** @odoo-module alias=sale.VariantMixin **/
|
||||
/** @odoo-module alias=website_sale.SaleVariantMixin **/
|
||||
|
||||
import concurrency from "web.concurrency";
|
||||
import core from "web.core";
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module alias=website_sale.VariantMixin **/
|
||||
|
||||
import VariantMixin from "sale.VariantMixin";
|
||||
import VariantMixin from "website_sale.SaleVariantMixin";
|
||||
|
||||
/**
|
||||
* Website behavior is slightly different from backend so we append
|
||||
|
||||
@@ -958,7 +958,7 @@
|
||||
<input type="hidden" class="product_template_id" name="product_template_id" t-att-value="product.id" />
|
||||
<input t-if="product.public_categ_ids.ids" type="hidden" class="product_category_id" name="product_category_id" t-att-value="product.public_categ_ids.ids[0]" />
|
||||
<input t-if="product.product_tag_ids.ids" type="hidden" class="product_tag_id" name="product_tag_id" t-att-value="product.product_tag_ids.ids[0]" />
|
||||
<t t-if="combination" t-call="sale.variants">
|
||||
<t t-if="combination" t-call="website_sale.variants">
|
||||
<t t-set="ul_class" t-valuef="flex-column" />
|
||||
<t t-set="parent_combination" t-value="None" />
|
||||
</t>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="variants">
|
||||
<t t-set="attribute_exclusions" t-value="product._get_attribute_exclusions(parent_combination)"/>
|
||||
<ul t-attf-class="list-unstyled js_add_cart_variants mb-0 #{ul_class}" t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)">
|
||||
<t t-foreach="product.valid_product_template_attribute_line_ids" t-as="ptal">
|
||||
<!-- Attributes selection is hidden if there is only one value available and it's not a custom value -->
|
||||
<li t-att-data-attribute_id="ptal.attribute_id.id"
|
||||
t-att-data-attribute_name="ptal.attribute_id.name"
|
||||
t-att-data-attribute_display_type="ptal.attribute_id.display_type"
|
||||
t-attf-class="variant_attribute #{'d-none' if len(ptal.product_template_value_ids._only_active()) == 1 and not ptal.product_template_value_ids._only_active()[0].is_custom else ''}">
|
||||
|
||||
<!-- Used to customize layout if the only available attribute value is custom -->
|
||||
<t t-set="single" t-value="len(ptal.product_template_value_ids._only_active()) == 1"/>
|
||||
<t t-set="single_and_custom" t-value="single and ptal.product_template_value_ids._only_active()[0].is_custom" />
|
||||
<strong t-field="ptal.attribute_id.name" class="attribute_name"/>
|
||||
|
||||
<t t-if="ptal.attribute_id.display_type == 'select'">
|
||||
<select
|
||||
t-att-data-attribute_id="ptal.attribute_id.id"
|
||||
t-attf-class="form-select css_attribute_select o_wsale_product_attribute js_variant_change #{ptal.attribute_id.create_variant} #{'d-none' if single_and_custom else ''}"
|
||||
t-att-name="'ptal-%s' % ptal.id">
|
||||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav">
|
||||
<option t-att-value="ptav.id"
|
||||
t-att-data-value_id="ptav.id"
|
||||
t-att-data-value_name="ptav.name"
|
||||
t-att-data-attribute_name="ptav.attribute_id.name"
|
||||
t-att-data-is_custom="ptav.is_custom"
|
||||
t-att-selected="ptav in combination"
|
||||
t-att-data-is_single="single"
|
||||
t-att-data-is_single_and_custom="single_and_custom">
|
||||
<span t-field="ptav.name"/>
|
||||
<t t-call="website_sale.badge_extra_price"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<t t-if="ptal.attribute_id.display_type == 'radio'">
|
||||
<ul t-att-data-attribute_id="ptal.attribute_id.id" t-attf-class="list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}">
|
||||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav">
|
||||
<li class="list-inline-item mb-3 js_attribute_value" style="margin: 0;">
|
||||
<label class="col-form-label">
|
||||
<div class="form-check">
|
||||
<input type="radio"
|
||||
t-attf-class="form-check-input js_variant_change #{ptal.attribute_id.create_variant}"
|
||||
t-att-checked="ptav in combination"
|
||||
t-att-name="'ptal-%s' % ptal.id"
|
||||
t-att-value="ptav.id"
|
||||
t-att-data-value_id="ptav.id"
|
||||
t-att-data-value_name="ptav.name"
|
||||
t-att-data-attribute_name="ptav.attribute_id.name"
|
||||
t-att-data-is_custom="ptav.is_custom"
|
||||
t-att-data-is_single="single"
|
||||
t-att-data-is_single_and_custom="single_and_custom" />
|
||||
<div class="radio_input_value form-check-label">
|
||||
<span t-field="ptav.name"/>
|
||||
<t t-call="website_sale.badge_extra_price"/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-if="ptal.attribute_id.display_type == 'pills'">
|
||||
<ul t-att-data-attribute_id="ptal.attribute_id.id"
|
||||
t-attf-class="btn-group-toggle list-inline list-unstyled o_wsale_product_attribute #{'d-none' if single_and_custom else ''}"
|
||||
data-bs-toggle="buttons">
|
||||
<t t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav">
|
||||
<li t-attf-class="o_variant_pills btn btn-primary mb-1 list-inline-item js_attribute_value #{'active' if ptav in combination else ''}">
|
||||
<input type="radio"
|
||||
t-attf-class="js_variant_change #{ptal.attribute_id.create_variant}"
|
||||
t-att-checked="ptav in combination"
|
||||
t-att-name="'ptal-%s' % ptal.id"
|
||||
t-att-value="ptav.id"
|
||||
t-att-data-value_id="ptav.id"
|
||||
t-att-id="ptav.id"
|
||||
t-att-data-value_name="ptav.name"
|
||||
t-att-data-attribute_name="ptav.attribute_id.name"
|
||||
t-att-data-is_custom="ptav.is_custom"
|
||||
t-att-data-is_single_and_custom="single_and_custom"
|
||||
t-att-autocomplete="off"/>
|
||||
<div class="radio_input_value o_variant_pills_input_value">
|
||||
<span t-field="ptav.name"/>
|
||||
<t t-call="website_sale.badge_extra_price"/>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-if="ptal.attribute_id.display_type == 'color'">
|
||||
<ul t-att-data-attribute_id="ptal.attribute_id.id" t-attf-class="list-inline o_wsale_product_attribute #{'d-none' if single_and_custom else ''}">
|
||||
<li t-foreach="ptal.product_template_value_ids._only_active()" t-as="ptav" class="list-inline-item me-1">
|
||||
<label t-attf-style="background-color:#{ptav.html_color or ptav.product_attribute_value_id.name if not ptav.is_custom else ''}"
|
||||
t-attf-class="css_attribute_color #{'active' if ptav in combination else ''} #{'custom_value' if ptav.is_custom else ''} #{'transparent' if (not ptav.is_custom and not ptav.html_color) else ''}">
|
||||
<input type="radio"
|
||||
t-attf-class="js_variant_change #{ptal.attribute_id.create_variant}"
|
||||
t-att-checked="ptav in combination"
|
||||
t-att-name="'ptal-%s' % ptal.id"
|
||||
t-att-value="ptav.id"
|
||||
t-att-title="ptav.name"
|
||||
t-att-data-value_id="ptav.id"
|
||||
t-att-data-value_name="ptav.name"
|
||||
t-att-data-attribute_name="ptav.attribute_id.name"
|
||||
t-att-data-is_custom="ptav.is_custom"
|
||||
t-att-data-is_single="single"
|
||||
t-att-data-is_single_and_custom="single_and_custom"/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="badge_extra_price" name="Badge Extra Price">
|
||||
<t t-set="combination_info_variant" t-value="product._get_combination_info(ptav, pricelist=pricelist)"/>
|
||||
<span class="badge rounded-pill text-bg-light border" t-if="combination_info_variant['price_extra']">
|
||||
<!--
|
||||
price_extra is displayed as catalog price instead of
|
||||
price after pricelist because it is impossible to
|
||||
compute. Indeed, the pricelist rule might depend on the
|
||||
selected variant, so the price_extra will be different
|
||||
depending on the selected combination. The price of an
|
||||
attribute is therefore variable and it's not very
|
||||
accurate to display it.
|
||||
|
||||
To cover some generic cases, the price_extra also
|
||||
covers the price-included taxes in e-commerce flows.
|
||||
(See the override of `_get_combination_info`)
|
||||
-->
|
||||
<span class="sign_badge_price_extra" t-out="combination_info_variant['price_extra'] > 0 and '+' or '-'"/>
|
||||
<span t-out="abs(combination_info_variant['price_extra'])" class="variant_price_extra text-muted fst-italic" style="white-space: nowrap;"
|
||||
t-options='{
|
||||
"widget": "monetary",
|
||||
"display_currency": (pricelist or product.env.company).currency_id
|
||||
}'/>
|
||||
</span>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -4,7 +4,7 @@ import concurrency from "web.concurrency";
|
||||
import core from "web.core";
|
||||
import publicWidget from "web.public.widget";
|
||||
import {getCookie, setCookie} from "web.utils.cookies";
|
||||
import VariantMixin from "sale.VariantMixin";
|
||||
import VariantMixin from "website_sale.SaleVariantMixin";
|
||||
import website_sale_utils from "website_sale.utils";
|
||||
const cartHandlerMixin = website_sale_utils.cartHandlerMixin;
|
||||
|
||||
@@ -305,7 +305,7 @@ publicWidget.registry.ProductComparison = publicWidget.Widget.extend(cartHandler
|
||||
const $form = $(ev.currentTarget);
|
||||
const cellIndex = $(ev.currentTarget).closest('td')[0].cellIndex;
|
||||
this.getCartHandlerOptions(ev);
|
||||
// Override product image container for animation.
|
||||
// Override product image container for animation.
|
||||
this.$itemImgContainer = this.$('#o_comparelist_table tr').first().find('td').eq(cellIndex);
|
||||
const $inputProduct = $form.find('input[type="hidden"][name="product_id"]').first();
|
||||
const productId = parseInt($inputProduct.val());
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'depends': ['website_sale', 'sale_product_configurator'],
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'views/templates.xml',
|
||||
'views/website_sale_product_configurator_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
@@ -18,9 +19,9 @@
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
('before', 'website_sale/static/src/js/website_sale.js', 'sale_product_configurator/static/src/js/product_configurator_modal.js'),
|
||||
('before', 'website_sale/static/src/js/website_sale.js', 'website_sale_product_configurator/static/src/js/sale_product_configurator_modal.js'),
|
||||
('before', 'website_sale/static/src/js/website_sale.js', 'website_sale_product_configurator/static/src/js/product_configurator_modal.js'),
|
||||
'sale/static/src/scss/product_configurator.scss',
|
||||
'website_sale/static/src/scss/product_configurator.scss',
|
||||
'website_sale_product_configurator/static/src/scss/website_sale_options.scss',
|
||||
'website_sale_product_configurator/static/src/js/website_sale_options.js',
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import main
|
||||
from . import sale_product_configurator
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.sale_product_configurator.controllers.main import ProductConfiguratorController
|
||||
from odoo.addons.website_sale_product_configurator.controllers.sale_product_configurator import ProductConfiguratorController
|
||||
from odoo.addons.website_sale.controllers import main
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class ProductConfiguratorController(http.Controller):
|
||||
@http.route(['/sale_product_configurator/configure'], type='json', auth="user", methods=['POST'])
|
||||
def configure(self, product_template_id, pricelist_id, **kw):
|
||||
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
|
||||
).filtered(
|
||||
lambda ptav: ptav.product_tmpl_id == product_template
|
||||
) # Filter out ptavs not belonging to the given template
|
||||
# It happens when you change the template on an already configured line
|
||||
# receiving the configured attributes data from the previous template configuration.
|
||||
|
||||
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(
|
||||
"website_sale_product_configurator.configure",
|
||||
{
|
||||
'product': product_template,
|
||||
'pricelist': pricelist,
|
||||
'add_qty': add_qty,
|
||||
'product_combination': product_combination
|
||||
},
|
||||
)
|
||||
|
||||
@http.route(['/sale_product_configurator/show_advanced_configurator'], type='json', auth="user", methods=['POST'])
|
||||
def show_advanced_configurator(self, product_id, variant_values, pricelist_id, **kw):
|
||||
pricelist = self._get_pricelist(pricelist_id)
|
||||
return self._show_advanced_configurator(product_id, variant_values, pricelist, False, **kw)
|
||||
|
||||
@http.route(['/sale_product_configurator/optional_product_items'], type='json', auth="user", methods=['POST'])
|
||||
def optional_product_items(self, product_id, pricelist_id, **kw):
|
||||
pricelist = self._get_pricelist(pricelist_id)
|
||||
return self._optional_product_items(product_id, pricelist, **kw)
|
||||
|
||||
def _optional_product_items(self, product_id, pricelist, **kw):
|
||||
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'):
|
||||
# Add "no_variant" attribute values' exclusions
|
||||
# They are kept in the context since they are not linked to this product variant
|
||||
parent_combination |= product.env.context.get('no_variant_attribute_values')
|
||||
|
||||
return request.env['ir.ui.view']._render_template("website_sale_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):
|
||||
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("website_sale_product_configurator.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),
|
||||
'no_attribute': kw.get('no_attribute', False),
|
||||
'custom_attribute': kw.get('custom_attribute', False)
|
||||
})
|
||||
|
||||
def _get_pricelist(self, pricelist_id, pricelist_fallback=False):
|
||||
return request.env['product.pricelist'].browse(int(pricelist_id or 0))
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { OptionalProductsModal } from "@sale_product_configurator/js/product_configurator_modal";
|
||||
|
||||
import { OptionalProductsModal } from "@website_sale_product_configurator/js/sale_product_configurator_modal";
|
||||
|
||||
OptionalProductsModal.include({
|
||||
/**
|
||||
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
/** @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 'website_sale.SaleVariantMixin';
|
||||
|
||||
export const OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
|
||||
events: Object.assign({}, 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
|
||||
*
|
||||
* @override
|
||||
* @param {$.Element} parent The parent container
|
||||
* @param {Object} params
|
||||
* @param {integer} params.pricelistId
|
||||
* @param {string} params.okButtonText The text to apply on the "ok" button, typically
|
||||
* "Add" for the sale order and "Proceed to checkout" on the web shop
|
||||
* @param {string} params.cancelButtonText same as "params.okButtonText" but
|
||||
* for the cancel button
|
||||
* @param {integer} params.previousModalHeight used to configure a min height on the modal-content.
|
||||
* This parameter is provided by the product configurator to "cover" its modal by making
|
||||
* this one big enough. This way the user can't see multiple buttons (which can be confusing).
|
||||
* @param {Object} params.rootProduct The root product of the optional products window
|
||||
* @param {integer} params.rootProduct.product_id
|
||||
* @param {integer} params.rootProduct.quantity
|
||||
* @param {Array} params.rootProduct.variant_values
|
||||
* @param {Array} params.rootProduct.product_custom_attribute_values
|
||||
* @param {Array} params.rootProduct.no_variant_attribute_values
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
var self = this;
|
||||
|
||||
var options = Object.assign({
|
||||
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');
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
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,
|
||||
no_attribute: self.rootProduct.no_variant_attribute_values,
|
||||
custom_attribute: self.rootProduct.product_custom_attribute_values,
|
||||
context: Object.assign({'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.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
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.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
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
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').toArray();
|
||||
$products.forEach((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);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the list of selected products.
|
||||
* The root product is added on top of the list.
|
||||
*
|
||||
* @returns {Array} products
|
||||
* {integer} product_id
|
||||
* {integer} quantity
|
||||
* {Array} product_custom_variant_values
|
||||
* {Array} no_variant_attribute_values
|
||||
* @public
|
||||
*/
|
||||
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 = $item.find('.custom-attribute-info').data("attribute-value") || self.getCustomVariantValues($item);
|
||||
noVariantAttributeValues = $item.find('.no-attribute-info').data("attribute-value") || 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;
|
||||
},
|
||||
|
||||
// ------------------------------------------
|
||||
// Private
|
||||
// ------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds the product image and updates the product description
|
||||
* based on attribute values that are either "no variant" or "custom".
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Will add/remove the option, that includes:
|
||||
* - Moving it to the correct DOM section
|
||||
* and possibly under its parent product
|
||||
* - Hiding attribute values selection and showing the quantity
|
||||
* - Creating the product if it's in "dynamic" mode (see product_attribute.create_variant)
|
||||
* - Updating the description based on custom/no_create attribute values
|
||||
* - Removing optional products if parent product is removed
|
||||
* - Computing the total price
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_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();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @see _onAddOrRemoveOption
|
||||
* @param {$.Element} $modal
|
||||
* @param {$.Element} $parent
|
||||
* @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();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @see _onAddOrRemoveOption
|
||||
* @param {$.Element} $modal
|
||||
* @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);
|
||||
},
|
||||
|
||||
/**
|
||||
* If the removed product had optional products, remove them as well
|
||||
*
|
||||
* @private
|
||||
* @param {$.Element} $modal
|
||||
* @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();
|
||||
},
|
||||
/**
|
||||
* Update price total when the quantity of a product is changed
|
||||
*
|
||||
* @private
|
||||
* @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();
|
||||
},
|
||||
|
||||
/**
|
||||
* When a product is added or when the quantity is changed,
|
||||
* 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))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extension point for website_sale
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_triggerPriceUpdateOnChangeQuantity: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Returns a unique id for `$el`.
|
||||
*
|
||||
* @private
|
||||
* @param {Element} el
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getUniqueId: function (el) {
|
||||
if (!el.dataset.uniqueId) {
|
||||
el.dataset.uniqueId = parseInt(_.uniqueId(), 10);
|
||||
}
|
||||
return el.dataset.uniqueId;
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
import ajax from "web.ajax";
|
||||
import core from "web.core";
|
||||
import publicWidget from "web.public.widget";
|
||||
import { OptionalProductsModal } from "@sale_product_configurator/js/product_configurator_modal";
|
||||
import { OptionalProductsModal } from "@website_sale_product_configurator/js/sale_product_configurator_modal";
|
||||
import "website_sale.website_sale";
|
||||
import wsUtils from "website_sale.utils";
|
||||
|
||||
|
||||
+8
-8
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<template id="optional_products_modal" name="Optional Products">
|
||||
<main class="modal-body">
|
||||
<t t-call="sale_product_configurator.configure_optional_products" />
|
||||
<t t-call="website_sale_product_configurator.configure_optional_products" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<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-if="combination" t-call="website_sale.variants">
|
||||
<t t-set="parent_combination" t-value="None"/>
|
||||
</t>
|
||||
<h2>
|
||||
@@ -57,7 +57,7 @@
|
||||
}'/>
|
||||
</h2>
|
||||
<t t-if="product.visible_qty_configurator">
|
||||
<t t-call="sale_product_configurator.product_quantity_config" />
|
||||
<t t-call="website_sale_product_configurator.product_quantity_config" />
|
||||
</t>
|
||||
<p class="css_not_available_msg alert alert-warning">This combination does not exist.</p>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
<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-if="combination and not already_configured" t-call="website_sale.variants">
|
||||
<t t-set="ul_class" t-valuef="flex-column" />
|
||||
<t t-set="product" t-value="product.product_tmpl_id"/>
|
||||
</t>
|
||||
@@ -132,7 +132,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center td-qty">
|
||||
<t t-call='sale_product_configurator.product_quantity_config' />
|
||||
<t t-call='website_sale_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'}}"
|
||||
@@ -167,7 +167,7 @@
|
||||
</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="sale_product_configurator.optional_product_items">
|
||||
<t t-call="website_sale_product_configurator.optional_product_items">
|
||||
<t t-set="parent_combination" t-value="combination"/>
|
||||
</t>
|
||||
</t>
|
||||
@@ -196,12 +196,12 @@
|
||||
<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-call="website_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='sale_product_configurator.product_quantity_config' />
|
||||
<t t-call='website_sale_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'}}"
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="configure_optional_products_website" inherit_id="sale_product_configurator.configure_optional_products">
|
||||
<template id="configure_optional_products_website" inherit_id="website_sale_product_configurator.configure_optional_products">
|
||||
<xpath expr="//th[hasclass('td-qty')]/span[hasclass('label')]" position="attributes">
|
||||
<attribute name='t-if'>not request.is_frontend or (request.is_frontend and is_view_active('website_sale.product_quantity'))</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="product_quantity_config_website" inherit_id="sale_product_configurator.product_quantity_config" priority="18">
|
||||
<template id="product_quantity_config_website" inherit_id="website_sale_product_configurator.product_quantity_config" priority="18">
|
||||
<xpath expr="//div[hasclass('css_quantity')]" position="attributes">
|
||||
<attribute name='t-if'>not request.is_frontend or (request.is_frontend and is_view_active('website_sale.product_quantity'))</attribute>
|
||||
</xpath>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module alias=website_sale_stock.VariantMixin **/
|
||||
|
||||
import {Markup} from "web.utils";
|
||||
import VariantMixin from "sale.VariantMixin";
|
||||
import VariantMixin from "website_sale.SaleVariantMixin";
|
||||
import publicWidget from "web.public.widget";
|
||||
import core from "web.core";
|
||||
var QWeb = core.qweb;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="website_sale_stock_modal" inherit_id="sale_product_configurator.product_quantity_config" name="Stocks Modal">
|
||||
<template id="website_sale_stock_modal" inherit_id="website_sale_product_configurator.product_quantity_config" name="Stocks Modal">
|
||||
<xpath expr="//input[@type='text'][hasclass('quantity')]" position="attributes">
|
||||
<attribute name='t-att-data-max'>max(product.sudo()._get_max_available_qty(), 1) if handle_stock and product.type == "product" and not product.allow_out_of_stock_order else None</attribute>
|
||||
</xpath>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import publicWidget from "web.public.widget";
|
||||
import wSaleUtils from "website_sale.utils";
|
||||
import VariantMixin from "sale.VariantMixin";
|
||||
import VariantMixin from "website_sale.SaleVariantMixin";
|
||||
|
||||
// VariantMixin events are overridden on purpose here
|
||||
// to avoid registering them more than once since they are already registered
|
||||
|
||||
Reference in New Issue
Block a user