[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:
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import backend
|
||||
from . import main
|
||||
from . import variant
|
||||
|
||||
@@ -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)
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user