[ADD/MOV] website_sale_product_configurator: new module and files move

Purpose
=======
Since we now have the product_configurator in its own module, we can add a bridge module for the website_sale
to override the necessary controller routes in a cleaner way.

This commit just adds the bridge module and move files and code around, there should not be any functional change.
This commit is contained in:
Aurélien Warnon
2019-03-27 15:12:51 +00:00
parent 3cb46ec74c
commit 55ed70f405
27 changed files with 472 additions and 350 deletions
@@ -3,3 +3,4 @@
from . import backend
from . import main
from . import variant
+5 -113
View File
@@ -12,7 +12,6 @@ from odoo.addons.payment.controllers.portal import PaymentProcessing
from odoo.addons.website.controllers.main import QueryURL
from odoo.exceptions import ValidationError
from odoo.addons.website.controllers.main import Website
from odoo.addons.sale.controllers.product_configurator import ProductConfiguratorController
from odoo.addons.website_form.controllers.main import WebsiteForm
from odoo.osv import expression
@@ -131,7 +130,7 @@ class Website(Website):
return views
class WebsiteSale(ProductConfiguratorController):
class WebsiteSale(http.Controller):
def _get_compute_currency_and_context(self, product=None):
pricelist_context, pricelist = self._get_pricelist_context()
@@ -292,6 +291,9 @@ class WebsiteSale(ProductConfiguratorController):
if not product.can_access_from_current_website():
raise NotFound()
return request.render("website_sale.product", self._prepare_product_values(product, category, search, **kwargs))
def _prepare_product_values(self, product, category, search, **kwargs):
add_qty = int(kwargs.get('add_qty', 1))
product_context = dict(request.env.context, quantity=add_qty,
@@ -319,7 +321,7 @@ class WebsiteSale(ProductConfiguratorController):
product_context['pricelist'] = pricelist.id
product = product.with_context(product_context)
values = {
return {
'search': search,
'category': category,
'pricelist': pricelist,
@@ -332,11 +334,7 @@ class WebsiteSale(ProductConfiguratorController):
'main_object': product,
'product': product,
'add_qty': add_qty,
'optional_product_ids': [p.with_context({'active_id': p.id}) for p in product.optional_product_ids],
# get_attribute_exclusions deprecated, use product method
'get_attribute_exclusions': self._get_attribute_exclusions,
}
return request.render("website_sale.product", values)
@http.route(['/shop/change_pricelist/<model("product.pricelist"):pl_id>'], type='http', auth="public", website=True, sitemap=False)
def pricelist_change(self, pl_id, **post):
@@ -1121,109 +1119,3 @@ class WebsiteSale(ProductConfiguratorController):
lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False)
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
"""This route is called when submitting the optional product modal."""
if lang:
request.website = request.website.with_context(lang=lang)
order = request.website.sale_get_order(force_create=True)
if order.state != 'draft':
request.session['sale_order_id'] = None
order = request.website.sale_get_order(force_create=True)
optional_product_ids = []
for k, v in kw.items():
if "optional-product-" in k and int(kw.get(k.replace("product", "add"))):
optional_product_ids.append(int(v))
custom_values = []
if kw.get('custom_values'):
custom_values = json.loads(kw.get('custom_values'))
value = {}
if add_qty or set_qty:
value = order._cart_update(
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
optional_product_ids=optional_product_ids,
product_custom_attribute_values=self._get_product_custom_value(
int(product_id),
custom_values,
'product_custom_attribute_values'
),
no_variant_attribute_values=self._get_product_custom_value(
int(product_id),
custom_values,
'no_variant_attribute_values'
)
)
# options have all time the same quantity
for option_id in optional_product_ids:
order._cart_update(
product_id=option_id,
set_qty=value.get('quantity'),
linked_line_id=value.get('line_id'),
product_custom_attribute_values=self._get_product_custom_value(
option_id,
custom_values,
'product_custom_attribute_values'
),
no_variant_attribute_values=self._get_product_custom_value(
option_id,
custom_values,
'no_variant_attribute_values'
)
)
return str(order.cart_quantity)
def _get_product_custom_value(self, product_id, custom_values, field):
if custom_values:
for custom_value in custom_values:
if custom_value['product_id'] == product_id:
return custom_value[field]
return None
# ------------------------------------------------------
# Product Configurator
# ------------------------------------------------------
@http.route(auth="public")
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(WebsiteSale, self).create_product_variant(product_template_id, product_template_attribute_value_ids, **kwargs)
@http.route(['/product_configurator/show_optional_products_website'], type='json', auth="public", methods=['POST'], website=True)
def show_optional_products_website(self, product_id, variant_values, **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')
return self.show_optional_products(product_id, variant_values, request.website.get_current_pricelist(), **kw)
@http.route(['/product_configurator/optional_product_items_website'], type='json', auth="public", methods=['POST'], website=True)
def optional_product_items_website(self, product_id, **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')
return self.optional_product_items(product_id, request.website.get_current_pricelist(), **kw)
@http.route(['/product_configurator/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')
res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel',
values={
'product': request.env['product.template'].browse(res['product_template_id']),
'product_variant': request.env['product.product'].browse(res['product_id']),
})
res['carousel'] = carousel_view
return res
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.addons.sale.controllers.variant import VariantController
class WebsiteSaleVariantController(VariantController):
@http.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')
res = self.get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist(), **kw)
carousel_view = request.env['ir.ui.view'].render_template('website_sale.shop_product_carousel',
values={
'product': request.env['product.template'].browse(res['product_template_id']),
'product_variant': request.env['product.product'].browse(res['product_id']),
})
res['carousel'] = carousel_view
return res
@http.route(auth="public")
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)
-8
View File
@@ -5,11 +5,6 @@
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
</record>
<record id="sale.product_product_1_product_template" model="product.template">
<field name="website_sequence">100</field>
<field name="website_published" eval="True"/>
</record>
<record id="product.product_product_24" model="product.product">
<field name="website_published" eval="True"/>
</record>
@@ -663,9 +658,6 @@
<field name="attribute_line_ids" eval="[(6,0,[ref('product_template_attribute_line_1')])]"/>
<field name="public_categ_ids" eval="[(6,0,[ref('website_sale.services')])]"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template'), ref('product_product_1_product_template')])]"/>
</record>
<!-- Handle automatically created product.template.attribute.value -->
<function model="ir.model.data" name="_update_xmlids">
+1 -2
View File
@@ -240,6 +240,7 @@ class ProductTemplate(models.Model):
if not self._is_add_to_cart_possible(parent_combination):
return False
# TODO FIXME awa: not optimal since rebased on p. configurator v2
gen = self._get_possible_combinations(parent_combination)
first_possible_combination = next(gen)
if next(gen, False) is not False:
@@ -247,8 +248,6 @@ class ProductTemplate(models.Model):
return False
if self._has_is_custom_values():
return False
if self.optional_product_ids.filtered(lambda p: p._is_add_to_cart_possible(first_possible_combination)):
return False
return True
@api.multi
+1 -10
View File
@@ -95,17 +95,8 @@ class SaleOrder(models.Model):
if line_id:
return lines
linked_line_id = kwargs.get('linked_line_id', False)
optional_product_ids = set(kwargs.get('optional_product_ids', []))
lines = lines.filtered(lambda line: line.linked_line_id.id == linked_line_id)
if optional_product_ids:
# only match the lines with the same chosen optional products on the existing lines
lines = lines.filtered(lambda line: optional_product_ids == set(line.mapped('option_line_ids.product_id.id')))
else:
lines = lines.filtered(lambda line: not line.option_line_ids)
return lines
return self.env['sale.order.line']
@api.multi
def _website_product_id_change(self, order_id, product_id, qty=0):
@@ -0,0 +1,23 @@
odoo.define('website_sale_.ProductConfiguratorMixin', function (require) {
'use strict';
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
/**
* Website behavior is slightly different from backend so we append
* "_website" to URLs to lead to a different route
*
* @private
* @param {string} uri The uri to adapt
*/
ProductConfiguratorMixin._getUri = function (uri) {
if (this.isWebsite){
return uri + '_website';
} else {
return uri;
}
};
return ProductConfiguratorMixin;
});
@@ -143,6 +143,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(ProductConfigurat
'change select[name="country_id"]': '_onChangeCountry',
'change #shipping_use_same': '_onChangeShippingUseSame',
'click .toggle_summary': '_onToggleSummary',
'click #add_to_cart, #products_grid .product_price .a-submit': 'async _onClickAdd',
'click input.js_product_change': 'onChangeVariant',
// dirty fix: prevent options modal events to be triggered and bubbled
'change oe_optional_products_modal [data-attribute_exclusions]': 'onChangeVariant',
@@ -421,6 +422,83 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(ProductConfigurat
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onClickAdd: function (ev) {
ev.preventDefault();
return this._handleAdd($(ev.currentTarget).closest('form'));
},
/**
* Initializes the optional products modal
* and add handlers to the modal events (confirm, back, ...)
*
* @private
* @param {$.Element} $form the related webshop form
*/
_handleAdd: function ($form) {
var self = this;
this.$form = $form;
var productSelector = [
'input[type="hidden"][name="product_id"]',
'input[type="radio"][name="product_id"]:checked'
];
var productReady = this.selectOrCreateProduct(
$form,
parseInt($form.find(productSelector.join(', ')).first().val(), 10),
$form.find('.product_template_id').val(),
false
);
productReady.then(function (productId){
$form.find(productSelector.join(', ')).val(productId);
self.rootProduct = {
product_id: productId,
quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1),
product_custom_attribute_values: self.getCustomVariantValues($form.find('.js_product')),
variant_values: self.getSelectedVariantValues($form.find('.js_product')),
no_variant_attribute_values: self.getNoVariantAttributeValues($form.find('.js_product'))
};
return self._onProductReady();
});
return productReady;
},
_onProductReady: function () {
this._submitForm();
},
/**
* Add custom variant values and attribute values that do not generate variants
* in the form data and trigger submit.
*
* @private
*/
_submitForm: function () {
var $productCustomVariantValues = $('<input>', {
name: 'product_custom_attribute_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.product_custom_attribute_values)
});
this.$form.append($productCustomVariantValues);
var $productNoVariantAttributeValues = $('<input>', {
name: 'no_variant_attribute_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.no_variant_attribute_values)
});
this.$form.append($productNoVariantAttributeValues);
this.$form.trigger('submit', [true]);
},
/**
* @private
* @param {MouseEvent} ev
@@ -1,193 +0,0 @@
odoo.define('website_sale_options.website_sale', function (require) {
'use strict';
var core = require('web.core');
var publicWidget = require('web.public.widget');
var OptionalProductsModal = require('sale.OptionalProductsModal');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
require('website_sale.website_sale');
var _t = core._t;
publicWidget.registry.WebsiteSaleOptions = publicWidget.Widget.extend(ProductConfiguratorMixin, {
selector: '.oe_website_sale',
events: _.extend({}, ProductConfiguratorMixin.events || {}, {
'click #add_to_cart, #products_grid .product_price .a-submit': 'async _onClickAdd',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this.isWebsite = true;
delete this.events['change [data-attribute_exclusions]'];
delete this.events['click input.js_product_change'];
delete this.events['change input.js_quantity'];
delete this.events['click button.js_add_cart_json'];
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Prevents the default submit to allow showing product options
* in a modal (if any)
*
* @private
* @param {MouseEvent} ev
*/
_onClickAdd: function (ev) {
ev.preventDefault();
return this._handleAdd($(ev.currentTarget).closest('form'));
},
/**
* Initializes the optional products modal
* and add handlers to the modal events (confirm, back, ...)
*
* @private
* @param {$.Element} $form the related webshop form
*/
_handleAdd: function ($form) {
var self = this;
this.$form = $form;
this.isWebsite = true;
var productSelector = [
'input[type="hidden"][name="product_id"]',
'input[type="radio"][name="product_id"]:checked'
];
var productReady = this.selectOrCreateProduct(
$form,
parseInt($form.find(productSelector.join(', ')).first().val(), 10),
$form.find('.product_template_id').val(),
false
);
productReady.then(function (productId){
$form.find(productSelector.join(', ')).val(productId);
self.rootProduct = {
product_id: productId,
quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1),
product_custom_attribute_values: self.getCustomVariantValues($form.find('.js_product')),
variant_values: self.getSelectedVariantValues($form.find('.js_product')),
no_variant_attribute_values: self.getNoVariantAttributeValues($form.find('.js_product'))
};
self.optionalProductsModal = new OptionalProductsModal($form, {
rootProduct: self.rootProduct,
isWebsite: true,
okButtonText: _t('Proceed to Checkout'),
cancelButtonText: _t('Continue Shopping'),
title: _t('Add to cart'),
context: self._getContext(),
}).open();
self.optionalProductsModal.on('options_empty', null, self._onModalOptionsEmpty.bind(self));
self.optionalProductsModal.on('update_quantity', null, self._onOptionsUpdateQuantity.bind(self));
self.optionalProductsModal.on('confirm', null, self._onModalConfirm.bind(self));
self.optionalProductsModal.on('back', null, self._onModalBack.bind(self));
return self.optionalProductsModal.opened();
});
return productReady;
},
/**
* No optional products found for this product
* Add custom variant values and attribute values that do not generate variants
* in the form data and trigger submit
*
* @private
*/
_onModalOptionsEmpty: function () {
var $productCustomVariantValues = $('<input>', {
name: 'product_custom_attribute_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.product_custom_attribute_values)
});
this.$form.append($productCustomVariantValues);
var $productNoVariantAttributeValues = $('<input>', {
name: 'no_variant_attribute_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.no_variant_attribute_values)
});
this.$form.append($productNoVariantAttributeValues);
this.$form.trigger('submit', [true]);
},
/**
* Update web shop base form quantity
* when quantity is updated in the optional products window
*
* @private
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
this.$form
.find('input[name="add_qty"]')
.first()
.val(quantity)
.trigger('change');
},
/**
* Submit form and stay on the same page
*
* @private
*/
_onModalBack: function () {
this._onModalSubmit(false);
},
/**
* Submit form and go to shop
*
* @private
*/
_onModalConfirm: function () {
this._onModalSubmit(true);
},
/**
* Submits the form with additional parameters
* - lang
* - product_custom_attribute_values: The products custom variant values
*
* @private
* @param {Boolean} goToShop Triggers a page refresh to the url "shop/cart"
*/
_onModalSubmit: function (goToShop){
var customValues = JSON.stringify(
this.optionalProductsModal.getSelectedProducts()
);
this.$form.ajaxSubmit({
url: '/shop/cart/update_option',
data: {
lang: this._getContext().lang,
custom_values: customValues
},
success: function (quantity) {
if (goToShop) {
var path = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart");
window.location.pathname = path;
}
var $quantity = $(".my_cart_quantity");
$quantity.parent().parent().removeClass("d-none", !quantity);
$quantity.html(quantity).hide().fadeIn(600);
}
});
},
});
return publicWidget.registry.WebsiteSaleOptions;
});
@@ -30,7 +30,7 @@
<field name="module_sale_product_configurator"/>
</div>
<div class="o_setting_right_pane">
<label for="module_sale_product_configurator"/>
<label for="module_sale_product_configurator" string="Optional Products"/>
<div class="text-muted">
Configure your attributes with exclusions and select optional products
</div>
+1 -3
View File
@@ -17,11 +17,10 @@
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale_frontend.scss"/>
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/sale_portal.scss"/>
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/product_configurator.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale_options.scss"/>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/product_configurator_mixin.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_utils.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_payment.js"></script>
@@ -35,7 +34,6 @@
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_deleted_archived_variants.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_list_view_b2c.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_no_variant_attribute.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_options.js"></script>
</xpath>
</template>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': "Website Sale Product Configurator",
'summary': """
Bridge module for website_sale / sale_product_configurator""",
'description': """
Bridge module to make the website e-commerce compatible with the product configurator
""",
'category': 'Technical Settings',
'version': '0.1',
'depends': ['website_sale', 'sale_product_configurator'],
'auto_install': True,
'data': [
'views/assets.xml',
],
'demo': [
'data/demo.xml',
],
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main
@@ -0,0 +1,99 @@
# -*- 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
from odoo.addons.sale_product_configurator.controllers.main import ProductConfiguratorController
from odoo.addons.website_sale.controllers.main import WebsiteSale
class WebsiteSaleProductConfiguratorController(ProductConfiguratorController):
@http.route(['/sale_product_configurator/show_optional_products_website'], type='json', auth="public", methods=['POST'], website=True)
def show_optional_products_website(self, product_id, variant_values, **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')
return self.show_optional_products(product_id, variant_values, request.website.get_current_pricelist(), **kw)
@http.route(['/sale_product_configurator/optional_product_items_website'], type='json', auth="public", methods=['POST'], website=True)
def optional_product_items_website(self, product_id, **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')
return self.optional_product_items(product_id, request.website.get_current_pricelist(), **kw)
class WebsiteSale(WebsiteSale):
def _prepare_product_values(self, product, category, search, **kwargs):
values = super(WebsiteSale, self)._prepare_product_values(product, category, search, **kwargs)
values['optional_product_ids'] = [p.with_context({'active_id': p.id}) for p in product.optional_product_ids]
return values
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False)
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
"""This route is called when submitting the optional product modal."""
if lang:
request.website = request.website.with_context(lang=lang)
order = request.website.sale_get_order(force_create=True)
if order.state != 'draft':
request.session['sale_order_id'] = None
order = request.website.sale_get_order(force_create=True)
optional_product_ids = []
for k, v in kw.items():
if "optional-product-" in k and int(kw.get(k.replace("product", "add"))):
optional_product_ids.append(int(v))
custom_values = []
if kw.get('custom_values'):
custom_values = json.loads(kw.get('custom_values'))
value = {}
if add_qty or set_qty:
value = order._cart_update(
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
optional_product_ids=optional_product_ids,
product_custom_attribute_values=self._get_product_custom_value(
int(product_id),
custom_values,
'product_custom_attribute_values'
),
no_variant_attribute_values=self._get_product_custom_value(
int(product_id),
custom_values,
'no_variant_attribute_values'
)
)
# options have all time the same quantity
for option_id in optional_product_ids:
order._cart_update(
product_id=option_id,
set_qty=value.get('quantity'),
linked_line_id=value.get('line_id'),
product_custom_attribute_values=self._get_product_custom_value(
option_id,
custom_values,
'product_custom_attribute_values'
),
no_variant_attribute_values=self._get_product_custom_value(
option_id,
custom_values,
'no_variant_attribute_values'
)
)
return str(order.cart_quantity)
def _get_product_custom_value(self, product_id, custom_values, field):
if custom_values:
for custom_value in custom_values:
if custom_value['product_id'] == product_id:
return custom_value[field]
return None
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="sale_product_configurator.product_product_1_product_template" model="product.template">
<field name="website_sequence">100</field>
<field name="website_published" eval="True"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template'), ref('website_sale.product_product_1_product_template')])]"/>
</record>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import product_template
from . import sale_order
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ProductTemplate(models.Model):
_inherit = ['product.template']
@api.multi
def _is_quick_add_to_cart_possible(self, parent_combination=None):
quick_add_possible = super(ProductTemplate, self)._is_quick_add_to_cart_possible(parent_combination)
if not quick_add_possible:
return quick_add_possible
if self.optional_product_ids.filtered(lambda p: p._is_add_to_cart_possible(self._get_first_possible_combination())):
return False
return True
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class SaleOrder(models.Model):
_inherit = "sale.order"
@api.multi
def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
lines = super(SaleOrder, self)._cart_find_product_line(product_id, line_id, **kwargs)
if lines:
return lines
linked_line_id = kwargs.get('linked_line_id', False)
optional_product_ids = set(kwargs.get('optional_product_ids', []))
lines = lines.filtered(lambda line: line.linked_line_id.id == linked_line_id)
if optional_product_ids:
# only match the lines with the same chosen optional products on the existing lines
lines = lines.filtered(lambda line: optional_product_ids == set(line.mapped('option_line_ids.product_id.id')))
else:
lines = lines.filtered(lambda line: not line.option_line_ids)
return lines
@@ -0,0 +1,36 @@
odoo.define('website_sale_product_configurator.OptionalProductsModal', function (require) {
"use strict";
var OptionalProductsModal = require('sale_product_configurator.OptionalProductsModal');
OptionalProductsModal.include({
/**
* If the "isWebsite" param is true, will also disable the following events:
* - change [data-attribute_exclusions]
* - click button.js_add_cart_json
*
* This has to be done because those events are already registered at the "website_sale"
* component level.
* This modal is part of the form that has these events registered and we
* want to avoid duplicates.
*
* @override
* @param {$.Element} parent The parent container
* @param {Object} params
* @param {boolean} params.isWebsite If we're on a web shop page, we need some
* custom behavior
*/
init: function (parent, params) {
this._super.apply(this, arguments);
this.isWebsite = params.isWebsite;
if (this.isWebsite) {
delete this.events['change [data-attribute_exclusions]'];
delete this.events['click button.js_add_cart_json'];
}
this.dialogClass = 'oe_optional_products_modal' + (params.isWebsite ? ' oe_website_sale' : '');
}
});
});
@@ -0,0 +1,84 @@
odoo.define('website_sale_options.website_sale', function (require) {
'use strict';
var core = require('web.core');
var publicWidget = require('web.public.widget');
var OptionalProductsModal = require('sale_product_configurator.OptionalProductsModal');
require('website_sale.website_sale');
var _t = core._t;
publicWidget.registry.WebsiteSale.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_onProductReady: function () {
this.optionalProductsModal = new OptionalProductsModal(this.$form, {
rootProduct: this.rootProduct,
isWebsite: true,
okButtonText: _t('Proceed to Checkout'),
cancelButtonText: _t('Continue Shopping'),
title: _t('Add to cart'),
context: this._getContext(),
}).open();
this.optionalProductsModal.on('options_empty', null, this._submitForm.bind(this));
this.optionalProductsModal.on('update_quantity', null, this._onOptionsUpdateQuantity.bind(this));
this.optionalProductsModal.on('confirm', null, this._onModalSubmit.bind(this, true));
this.optionalProductsModal.on('back', null, this._onModalSubmit.bind(this, false));
return this.optionalProductsModal.opened();
},
/**
* Update web shop base form quantity
* when quantity is updated in the optional products window
*
* @private
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
this.$form
.find('input[name="add_qty"]')
.first()
.val(quantity)
.trigger('change');
},
/**
* Submits the form with additional parameters
* - lang
* - product_custom_attribute_values: The products custom variant values
*
* @private
* @param {Boolean} goToShop Triggers a page refresh to the url "shop/cart"
*/
_onModalSubmit: function (goToShop){
var customValues = JSON.stringify(
this.optionalProductsModal.getSelectedProducts()
);
this.$form.ajaxSubmit({
url: '/shop/cart/update_option',
data: {
lang: this._getContext().lang,
custom_values: customValues
},
success: function (quantity) {
if (goToShop) {
var path = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart");
window.location.pathname = path;
}
var $quantity = $(".my_cart_quantity");
$quantity.parent().parent().removeClass("d-none", !quantity);
$quantity.html(quantity).hide().fadeIn(600);
}
});
},
});
return publicWidget.registry.WebsiteSaleOptions;
});
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="website_sale_product_configurator assets frontend">
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/product_configurator.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_sale_product_configurator/static/src/scss/website_sale_options.scss"/>
</xpath>
<xpath expr="script[contains(@src, '/website_sale/static/src/js/website_sale.js')]" position="before">
<script type="text/javascript" src="/sale_product_configurator/static/src/js/product_configurator_modal.js"></script>
<script type="text/javascript" src="/website_sale_product_configurator/static/src/js/product_configurator_modal.js"></script>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/website_sale_product_configurator/static/src/js/website_sale_options.js"></script>
</xpath>
</template>
</odoo>
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
from . import variant
@@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.http import request
class WebsiteSale(WebsiteSale):
def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw):
"""deprecated, use product method"""
combination = request.env['product.template.attribute.value'].browse(combination)
return request.env['product.template'].browse(int(product_template_id))._get_combination_info(combination, product_id, add_qty, pricelist)
@http.route()
def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw):
kw['context'] = {'website_sale_stock_get_quantity': True}
return super(WebsiteSale, self).get_combination_info_website(product_template_id, product_id, combination, add_qty, **kw)
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.addons.website_sale.controllers.variant import WebsiteSaleVariantController
class WebsiteSaleStockVariantController(WebsiteSaleVariantController):
@http.route()
def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw):
kw['context'] = {'website_sale_stock_get_quantity': True}
return super(WebsiteSaleStockVariantController, self).get_combination_info_website(product_template_id, product_id, combination, add_qty, **kw)
@@ -57,7 +57,7 @@
</xpath>
</template>
<template id="website_sale_stock_modal" inherit_id="sale.product_configurator_configure_optional_products" name="Stocks Modal">
<template id="website_sale_stock_modal" inherit_id="sale_product_configurator.configure_optional_products" name="Stocks Modal">
<xpath expr="//input[@type='text'][hasclass('quantity')]" position="attributes">
<attribute name='t-att-data-max'>max(product.sudo().virtual_available - product.cart_qty, 1) if handle_stock and product.type == "product" and product.inventory_availability in ["always", "threshold"] else None</attribute>
</xpath>