From 83193566305929539e37699c89a589eceefc6d01 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 11 Jun 2014 12:50:41 +0200 Subject: [PATCH 01/42] [IMP] website_sale: allow optional products in shop; In shop, it opens the modal jus after having clicked on the 'Add to cart'. The modal contains the optional products. --- addons/product/product.py | 8 +-- addons/website_sale/controllers/main.py | 36 ++++++++-- addons/website_sale/models/product.py | 1 + addons/website_sale/models/sale_order.py | 22 ++++++ .../static/src/js/website_sale.js | 20 ++++-- addons/website_sale/views/templates.xml | 71 +++++++++++++++++-- addons/website_sale/views/views.xml | 1 + 7 files changed, 135 insertions(+), 24 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index db86047c10a..34627f3d647 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -802,10 +802,8 @@ class product_product(osv.osv): uom.id, product.list_price, context['uom']) else: res[product.id] = product.list_price - price_extra = 0.0 - for variant_id in product.attribute_value_ids: - price_extra += variant_id.price_extra - res[product.id] = (res[product.id] or 0.0) + price_extra + res[product.id] = res[product.id] + product.price_extra + return res def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None): @@ -864,6 +862,8 @@ class product_product(osv.osv): def _get_price_extra(self, cr, uid, ids, name, args, context=None): result = dict.fromkeys(ids, False) for product in self.browse(cr, uid, ids, context=context): + ctx = context.copy() + ctx.update(active_id=product.product_tmpl_id.id) price_extra = 0.0 for variant_id in product.attribute_value_ids: for price_id in variant_id.price_ids: diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 23225186dc8..4cb5e6f99a0 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -212,6 +212,17 @@ class website_sale(http.Controller): variants = [[p.id, map(int, p.attribute_value_ids), p.price] for p in product.product_variant_ids] + + optional_products = {} + for o in product.optional_product_ids: + if not optional_products.get(o.product_tmpl_id.id): + optional_products[o.product_tmpl_id.id] = { + 'product_tmpl_id': o.product_tmpl_id, + 'product_ids': [] + } + if o not in optional_products[o.product_tmpl_id.id]: + optional_products[o.product_tmpl_id.id]['product_ids'].append(o) + values = { 'search': search, 'category': category, @@ -223,6 +234,7 @@ class website_sale(http.Controller): 'main_object': product, 'product': product, 'variants': variants, + 'optional_products': optional_products } return request.website.render("website_sale.product", values) @@ -240,21 +252,31 @@ class website_sale(http.Controller): @http.route(['/shop/cart'], type='http', auth="public", website=True) def cart(self, **post): + cr, uid, context, pool = request.cr, request.uid, request.context, request.registry order = request.website.sale_get_order() + values = { 'order': order, - 'suggested_products': [], + 'suggested_products': [] } if order: - if not request.context.get('pricelist'): - request.context['pricelist'] = order.pricelist_id.id - values['suggested_products'] = order._cart_accessories(context=request.context) + if not context.get('pricelist'): + context['pricelist'] = order.pricelist_id.id + values['suggested_products'] = order._cart_accessories(context=context) + return request.website.render("website_sale.cart", values) @http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True) def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): - cr, uid, context = request.cr, request.uid, request.context - request.website.sale_get_order(force_create=1)._cart_update(product_id=int(product_id), add_qty=add_qty, set_qty=set_qty) + cr, uid, context, pool = request.cr, request.uid, request.context, request.registry + order = request.website.sale_get_order(force_create=1) + + print kw + for key,val in kw.items(): + if 'option-' in key: + order._cart_update(product_id=int(key.split("-")[1]), add_qty=1) + + order._cart_update(product_id=int(product_id), add_qty=add_qty, set_qty=set_qty) return request.redirect("/shop/cart") @http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True) @@ -445,7 +467,7 @@ class website_sale(http.Controller): @http.route(['/shop/checkout'], type='http', auth="public", website=True) def checkout(self, **post): - cr, uid, context, registry = request.cr, request.uid, request.context, request.registry + cr, uid, context, pool = request.cr, request.uid, request.context, request.registry order = request.website.sale_get_order(force_create=1, context=context) diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 116822f2984..bedf0d1be0b 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -132,6 +132,7 @@ class product_template(osv.Model): 'website_sequence': fields.integer('Sequence', help="Determine the display order in the Website E-commerce"), 'website_url': fields.function(_website_url, string="Website url", type="char"), 'public_categ_ids': fields.many2many('product.public.category', string='Public Category', help="Those categories are used to group similar products for e-commerce."), + 'optional_product_ids': fields.many2many('product.product', string='Optional Products'), } def _defaults_website_sequence(self, cr, uid, *l, **kwargs): diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index d9855034018..759884ab1fd 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -116,6 +116,28 @@ class sale_order(osv.Model): product_ids = random.sample(s, min(len(s),3)) return self.pool['product.product'].browse(cr, uid, product_ids, context=context) + def _cart_optional_products(self, cr, uid, ids, context=None): + for order in self.browse(cr, uid, ids, context=context): + products = {} + for l in order.website_order_line: + opt = {} + for o in l.product_id.optional_product_ids: + if not opt.get(o.product_tmpl_id.id): + opt[o.product_tmpl_id.id] = { + 'product_tmpl_id': o.product_tmpl_id, + 'product_ids': [] + } + if o not in opt[o.product_tmpl_id.id]: + opt[o.product_tmpl_id.id]['product_ids'].append(o) + + if opt: + products[l.id] = { + 'order_line_id': l, + 'optional_product_tmpl': opt + } + + return products + class website(orm.Model): _inherit = 'website' diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 62c1f829bde..1325807713d 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -50,6 +50,13 @@ $(document).ready(function () { $(this).closest("form").submit(); }); + // modal to select optional product in my cart + $("#modal_optional_products label").mousedown(function(event) { + $(event.currentTarget).parents('li.optional_product_tmpl:first').find("input[type=checkbox]").each(function () { + if($(this).parent()[0] != event.currentTarget) $(this).removeAttr('checked'); + }); + }); + // change price when they are variants var $price = $(".oe_price .oe_currency_value"); $('form.js_add_cart_json label').on('mouseup', function (ev) { @@ -70,12 +77,12 @@ $(document).ready(function () { var $form_var = $('form.js_add_cart_variants'); var variant_ids = $form_var.data("attribute_value_ids"); - $form_var.on('change', 'input, select', function (ev) { + $form_var.on('change', 'input.js_variant_change, select.js_variant_change', function (ev) { var values = []; $form_var.find("label").removeClass("text-muted css_not_available"); $form_var.find(".a-submit").removeProp("disabled"); - $form_var.find('input:checked, select').each(function () { + $form_var.find('input.js_variant_change:checked, select').each(function () { values.push(+$(this).val()); }); var available = false; @@ -89,10 +96,10 @@ $(document).ready(function () { } } - $form_var.find("input:radio, select").each(function () { + $form_var.find("input.js_variant_change:radio, select.js_variant_change").each(function () { var id = +$(this).val(); var values = [id]; - $form_var.find(">ul>li:not(:has(input[value='" + id + "'])) input:checked, select").each(function () { + $form_var.find(">ul>li:not(:has(input.js_variant_change[value='" + id + "'])) input.js_variant_change:checked, select").each(function () { values.push(+$(this).val()); }); for (var k in variant_ids) { @@ -112,10 +119,9 @@ $(document).ready(function () { $(".oe_price_h4").addClass("hidden"); $(".oe_not_available").removeClass("hidden"); $form_var.find('input[name="product_id"]').val(0); - $form_var.find(".a-submit").prop("disabled", "disabled"); + $form_var.find(".js_check_product").prop("disabled", "disabled"); } }); - $form_var.find("input:first").trigger('change'); - + $form_var.find("input.js_variant_change:first").trigger('change'); }); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 470ca8383f4..9b51d819b86 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -387,7 +387,11 @@ - Add to Cart + Add to Cart + Add to Cart + + +
@@ -402,10 +406,60 @@
+
+ + @@ -685,7 +742,9 @@
- Process Checkout + + Process Checkout +
diff --git a/addons/website_sale/views/views.xml b/addons/website_sale/views/views.xml index 86e369f11e7..c6c30d0a162 100644 --- a/addons/website_sale/views/views.xml +++ b/addons/website_sale/views/views.xml @@ -26,6 +26,7 @@ + From f62673c2663d08855c592f8091f5709326800252 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 12 Jun 2014 16:58:44 +0200 Subject: [PATCH 02/42] [IMP] website_sale: Optional product with variant suggestion --- addons/website_sale/controllers/main.py | 36 +-- addons/website_sale/models/product.py | 2 +- .../static/src/css/website_sale.css | 8 +- .../static/src/css/website_sale.sass | 8 +- .../website_sale/static/src/img/redcross.png | Bin 0 -> 669 bytes .../static/src/js/website_sale.js | 96 +++--- addons/website_sale/views/templates.xml | 281 +++++++++++------- 7 files changed, 264 insertions(+), 167 deletions(-) create mode 100644 addons/website_sale/static/src/img/redcross.png diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 4cb5e6f99a0..9f0005908ad 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -191,6 +191,7 @@ class website_sale(http.Controller): def product(self, product, category='', search='', **kwargs): cr, uid, context, pool = request.cr, request.uid, request.context, request.registry category_obj = pool['product.public.category'] + template_obj = pool['product.template'] context.update(active_id=product.id) @@ -208,20 +209,13 @@ class website_sale(http.Controller): if not context.get('pricelist'): context['pricelist'] = int(self.get_pricelist()) - product = request.registry.get('product.template').browse(request.cr, request.uid, int(product), context=context) + product = template_obj.browse(request.cr, request.uid, int(product), context=context) - variants = [[p.id, map(int, p.attribute_value_ids), p.price] for p in product.product_variant_ids] - - - optional_products = {} - for o in product.optional_product_ids: - if not optional_products.get(o.product_tmpl_id.id): - optional_products[o.product_tmpl_id.id] = { - 'product_tmpl_id': o.product_tmpl_id, - 'product_ids': [] - } - if o not in optional_products[o.product_tmpl_id.id]: - optional_products[o.product_tmpl_id.id]['product_ids'].append(o) + optional_product_ids = [] + for p in product.optional_product_ids: + ctx = context.copy() + ctx.update(active_id=p.id) + optional_product_ids.append(template_obj.browse(cr, uid, p.id, context=ctx)) values = { 'search': search, @@ -233,8 +227,7 @@ class website_sale(http.Controller): 'category_list': category_list, 'main_object': product, 'product': product, - 'variants': variants, - 'optional_products': optional_products + 'optional_product_ids': optional_product_ids } return request.website.render("website_sale.product", values) @@ -270,19 +263,16 @@ class website_sale(http.Controller): def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): cr, uid, context, pool = request.cr, request.uid, request.context, request.registry order = request.website.sale_get_order(force_create=1) - - print kw - for key,val in kw.items(): - if 'option-' in key: - order._cart_update(product_id=int(key.split("-")[1]), add_qty=1) - - order._cart_update(product_id=int(product_id), add_qty=add_qty, set_qty=set_qty) + if add_qty or set_qty: + order._cart_update(product_id=int(product_id), add_qty=int(add_qty), set_qty=int(set_qty)) return request.redirect("/shop/cart") @http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True) - def cart_update_json(self, product_id, line_id, add_qty=None, set_qty=None): + def cart_update_json(self, product_id, line_id, add_qty=None, set_qty=None, display=True): order = request.website.sale_get_order(force_create=1) quantity = order._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty) + if not display: + return None return { 'quantity': quantity, 'cart_quantity': order.cart_quantity, diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index bedf0d1be0b..51359c4851a 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -132,7 +132,7 @@ class product_template(osv.Model): 'website_sequence': fields.integer('Sequence', help="Determine the display order in the Website E-commerce"), 'website_url': fields.function(_website_url, string="Website url", type="char"), 'public_categ_ids': fields.many2many('product.public.category', string='Public Category', help="Those categories are used to group similar products for e-commerce."), - 'optional_product_ids': fields.many2many('product.product', string='Optional Products'), + 'optional_product_ids': fields.many2many('product.template','product_optional_rel','src_id','dest_id',string='Optional Products'), } def _defaults_website_sequence(self, cr, uid, *l, **kwargs): diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css index 73d374357ab..41db67c0ffe 100644 --- a/addons/website_sale/static/src/css/website_sale.css +++ b/addons/website_sale/static/src/css/website_sale.css @@ -302,10 +302,12 @@ color: #cccccc; } .js_add_cart_variants label.css_not_available { - opacity: 0.3; + opacity: 0.6; } -.js_add_cart_variants label.css_not_available input { - opacity: 0; +.js_add_cart_variants label.css_attribute_color.css_not_available { + opacity: 1; + background-image: url("/website_sale/static/src/img/redcross.png"); + background-size: cover; } .product_detail_img { diff --git a/addons/website_sale/static/src/css/website_sale.sass b/addons/website_sale/static/src/css/website_sale.sass index 40308925552..05bcff522da 100644 --- a/addons/website_sale/static/src/css/website_sale.sass +++ b/addons/website_sale/static/src/css/website_sale.sass @@ -261,9 +261,11 @@ option.css_not_available color: #ccc label.css_not_available - opacity: 0.3 - input - opacity: 0 + opacity: 0.6 + label.css_attribute_color.css_not_available + opacity: 1 + background-image: url('/website_sale/static/src/img/redcross.png') + background-size: cover .product_detail_img margin-left: auto diff --git a/addons/website_sale/static/src/img/redcross.png b/addons/website_sale/static/src/img/redcross.png new file mode 100644 index 0000000000000000000000000000000000000000..92a87401e8927e93bdea7ab743ac01c5e9ee05fc GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0vp^DImIi(arPdkDp8PrkM2HFOPKdJouyf zcJ`Fl694BrTP;>ecq7YWy+|eT&HO9Y3ssWdG?zGf9;;vrl5J&pZ0QjyBGhx;Vbwx` z#65gLtgSqcZ3Rk2oO-@Hh6H*To@-dKP#}2@bD*Q*aSQH4eM$|5axJ2+EXr&xysjL| zd@Zc5Jj!A!(yju^aw^=eB9Hk_+T8v{1c zIGsnu0ujvI+EPCkP7!xm`Fe>>M^b!Pq`=d2M~f6htEx{oguJSo&c4z@UWq%XvdN2Y z$>E-bJRZw6W2z?f6~Ee|pmkT#Q$wRUkcD+=m>Z+Wl!F>gO+gZ@0Reo@GOVhGEr9`I z&LXT!4Os#M@B=G2oNHlZf;69T&EFtdqV0WoC|0PCc0idc=;y^47#PT=|n8Lq+>HaCLJ+?oA P>5akD)z4*}Q$iB}VO;z) literal 0 HcmV?d00001 diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 1325807713d..fce5f72b861 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -38,7 +38,8 @@ $(document).ready(function () { ev.preventDefault(); var $link = $(ev.currentTarget); var $input = $link.parent().parent().find("input"); - $input.val(($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val(),10)); + var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val(),10); + $input.val(quantity > 0 ? quantity : 0); $input.change(); return false; }); @@ -50,18 +51,10 @@ $(document).ready(function () { $(this).closest("form").submit(); }); - // modal to select optional product in my cart - $("#modal_optional_products label").mousedown(function(event) { - $(event.currentTarget).parents('li.optional_product_tmpl:first').find("input[type=checkbox]").each(function () { - if($(this).parent()[0] != event.currentTarget) $(this).removeAttr('checked'); - }); - }); - // change price when they are variants - var $price = $(".oe_price .oe_currency_value"); $('form.js_add_cart_json label').on('mouseup', function (ev) { - ev.preventDefault(); - var $label = $(ev.currentTarget); + var $label = $(this); + var $price = $label.parents("form:first").find(".oe_price .oe_currency_value"); if (!$price.data("price")) { $price.data("price", parseFloat($price.text())); } @@ -75,53 +68,82 @@ $(document).ready(function () { $('.css_attribute_color:has(input:checked)').addClass("active"); }); - var $form_var = $('form.js_add_cart_variants'); - var variant_ids = $form_var.data("attribute_value_ids"); - $form_var.on('change', 'input.js_variant_change, select.js_variant_change', function (ev) { + $('input.js_variant_change, select.js_variant_change').change(function (ev) { + var $ul = $(this).parents('ul.js_add_cart_variants:first'); + var $parent = $ul.parents('.js_product:first'); + var $porduct_id = $parent.find('input.product_id, input.optional_product_id').first(); + var $price = $parent.find(".oe_price .oe_currency_value:first"); + var variant_ids = $ul.data("attribute_value_ids"); var values = []; - $form_var.find("label").removeClass("text-muted css_not_available"); - $form_var.find(".a-submit").removeProp("disabled"); - - $form_var.find('input.js_variant_change:checked, select').each(function () { + $parent.find('input.js_variant_change:checked, select.js_variant_change').each(function () { values.push(+$(this).val()); }); - var available = false; + + $parent.find("label").removeClass("text-muted css_not_available"); + + var product_id = false; for (var k in variant_ids) { if (_.isEqual(variant_ids[k][1], values)) { - var dec = variant_ids[k][2] % 1; - $('input[name="product_id"]').val(variant_ids[k][0]); - $price.html(variant_ids[k][2] + (dec < 0.01 ? ".00" : (dec < 1 ? "0" : "") )); - available = true; + var dec = ((variant_ids[k][2] % 1) * 100) | 0; + $price.html(variant_ids[k][2] + (dec ? '' : '.0') + (dec%10 ? '' : '0')); + product_id = variant_ids[k][0]; break; } } - $form_var.find("input.js_variant_change:radio, select.js_variant_change").each(function () { - var id = +$(this).val(); + $parent.find("input.js_variant_change:radio, select.js_variant_change").each(function () { + var $input = $(this); + var id = +$input.val(); var values = [id]; - $form_var.find(">ul>li:not(:has(input.js_variant_change[value='" + id + "'])) input.js_variant_change:checked, select").each(function () { + + $parent.find("ul:not(:has(input.js_variant_change[value='" + id + "'])) input.js_variant_change:checked, select").each(function () { values.push(+$(this).val()); }); + for (var k in variant_ids) { if (!_.difference(values, variant_ids[k][1]).length) { return; } } - $(this).parents("label:not(.css_attribute_color):first").addClass("text-muted"); - $(this).parents("label.css_attribute_color:first").addClass("css_not_available"); - $(this).find("option[value='" + id + "']").addClass("css_not_available"); + $input.parents("label:first").addClass("css_not_available"); + $input.find("option[value='" + id + "']").addClass("css_not_available"); }); - if (available) { - $(".oe_price_h4").removeClass("hidden"); - $(".oe_not_available").addClass("hidden"); + if (product_id) { + $parent.removeClass("oe_not_available"); + $porduct_id.val(product_id); + $parent.find(".js_check_product").removeAttr("disabled"); } else { - $(".oe_price_h4").addClass("hidden"); - $(".oe_not_available").removeClass("hidden"); - $form_var.find('input[name="product_id"]').val(0); - $form_var.find(".js_check_product").prop("disabled", "disabled"); + $parent.addClass("oe_not_available"); + $porduct_id.val(0); + $parent.find(".js_check_product").attr("disabled", "disabled"); } }); - $form_var.find("input.js_variant_change:first").trigger('change'); + $('ul.js_add_cart_variants').each(function () { + $('input.js_variant_change, select.js_variant_change', this).first().trigger('change'); + }); + + $('#product_detail form[action^="/shop/cart/update"] .a-submit').off("click").click(function (event) { + event.preventDefault(); + var $link = $(this); + var $form = $link.parents("form:first"); + var defs = []; + $link.attr('disabled', 'disabled'); + $('.js_product', $form).each(function () { + var product_id = parseInt($('input.optional_product_id', this).val(),10); + var quantity = parseInt($('input.js_quantity', this).val(),10); + if (product_id && quantity) { + defs.push(openerp.jsonRpc("/shop/cart/update_json", 'call', { + 'line_id': null, + 'product_id': product_id, + 'add_qty': quantity, + 'display': false})); + } + }); + $.when.apply($.when, defs).then(function () { + $form.submit(); + }); + return false; + }); }); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 9b51d819b86..6e32fc934f2 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -387,11 +387,14 @@ - Add to Cart - Add to Cart - - + + Add to Cart + Add to Cart + + + +
@@ -411,45 +414,113 @@
+ + + + -