[IMP] website_sale: allow custom PPR and PPG

Part of https://github.com/odoo/odoo/pull/31147
task-1925327
This commit is contained in:
qsm-odoo
2019-05-28 15:05:42 +00:00
parent 5521bcd81c
commit d1240ee5d9
6 changed files with 159 additions and 27 deletions
+31 -23
View File
@@ -17,65 +17,62 @@ from odoo.osv import expression
_logger = logging.getLogger(__name__)
PPG = 20 # Products Per Page
PPR = 4 # Products Per Row
class TableCompute(object):
def __init__(self):
self.table = {}
def _check_place(self, posx, posy, sizex, sizey):
def _check_place(self, posx, posy, sizex, sizey, ppr):
res = True
for y in range(sizey):
for x in range(sizex):
if posx + x >= PPR:
if posx + x >= ppr:
res = False
break
row = self.table.setdefault(posy + y, {})
if row.setdefault(posx + x) is not None:
res = False
break
for x in range(PPR):
for x in range(ppr):
self.table[posy + y].setdefault(x, None)
return res
def process(self, products, ppg=PPG):
def process(self, products, ppg=20, ppr=4):
# Compute products positions on the grid
minpos = 0
index = 0
maxy = 0
x = 0
for p in products:
x = min(max(p.website_size_x, 1), PPR)
y = min(max(p.website_size_y, 1), PPR)
x = min(max(p.website_size_x, 1), ppr)
y = min(max(p.website_size_y, 1), ppr)
if index >= ppg:
x = y = 1
pos = minpos
while not self._check_place(pos % PPR, pos // PPR, x, y):
while not self._check_place(pos % ppr, pos // ppr, x, y, ppr):
pos += 1
# if 21st products (index 20) and the last line is full (PPR products in it), break
# (pos + 1.0) / PPR is the line where the product would be inserted
# if 21st products (index 20) and the last line is full (ppr products in it), break
# (pos + 1.0) / ppr is the line where the product would be inserted
# maxy is the number of existing lines
# + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block
# and to force python to not round the division operation
if index >= ppg and ((pos + 1.0) // PPR) > maxy:
if index >= ppg and ((pos + 1.0) // ppr) > maxy:
break
if x == 1 and y == 1: # simple heuristic for CPU optimization
minpos = pos // PPR
minpos = pos // ppr
for y2 in range(y):
for x2 in range(x):
self.table[(pos // PPR) + y2][(pos % PPR) + x2] = False
self.table[pos // PPR][pos % PPR] = {
self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False
self.table[pos // ppr][pos % ppr] = {
'product': p, 'x': x, 'y': y,
'class': " ".join(x.html_class for x in p.website_style_ids if x.html_class)
}
if index <= ppg:
maxy = max(maxy, y + (pos // PPR))
maxy = max(maxy, y + (pos // ppr))
index += 1
# Format table according to HTML needs
@@ -210,11 +207,13 @@ class WebsiteSale(http.Controller):
if ppg:
try:
ppg = int(ppg)
post['ppg'] = ppg
except ValueError:
ppg = PPG
post["ppg"] = ppg
else:
ppg = PPG
ppg = False
if not ppg:
ppg = request.env['website'].get_current_website().shop_ppg or 20
ppr = request.env['website'].get_current_website().shop_ppr or 4
attrib_list = request.httprequest.args.getlist('attrib')
attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v]
@@ -279,8 +278,9 @@ class WebsiteSale(http.Controller):
'add_qty': add_qty,
'products': products,
'search_count': product_count, # common for all searchbox
'bins': TableCompute().process(products, ppg),
'rows': PPR,
'bins': TableCompute().process(products, ppg, ppr),
'ppg': ppg,
'ppr': ppr,
'categories': categs,
'attributes': attributes,
'compute_currency': compute_currency,
@@ -1075,6 +1075,14 @@ class WebsiteSale(http.Controller):
product = request.env['product.template'].browse(id)
return product.write({'website_size_x': x, 'website_size_y': y})
@http.route(['/shop/change_ppg'], type='json', auth='user')
def change_ppg(self, ppg):
request.env['website'].get_current_website().shop_ppg = ppg
@http.route(['/shop/change_ppr'], type='json', auth='user')
def change_ppr(self, ppr):
request.env['website'].get_current_website().shop_ppr = ppr
def order_lines_2_google_api(self, order_lines):
""" Transforms a list of order lines into a dict for google analytics """
ret = []
+5 -2
View File
@@ -25,8 +25,8 @@ class Website(models.Model):
except ValueError:
return None
salesteam_id = fields.Many2one('crm.team',
string='Sales Team',
salesteam_id = fields.Many2one('crm.team',
string='Sales Team',
default=_get_default_website_team)
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
string='Price list available for this Ecommerce/Website')
@@ -40,6 +40,9 @@ class Website(models.Model):
cart_recovery_mail_template_id = fields.Many2one('mail.template', string='Cart Recovery Email', default=_default_recovery_mail_template, domain="[('model', '=', 'sale.order')]")
cart_abandoned_delay = fields.Float("Abandoned Delay", default=1.0)
shop_ppg = fields.Integer(default=20, string="Number of products in the grid on the shop")
shop_ppr = fields.Integer(default=4, string="Number of grid columns on the shop")
@api.one
def _compute_pricelist_ids(self):
""" Return the pricelists that can be used directly or indirectly on
@@ -87,6 +87,94 @@ function reload() {
}
}
options.registry.WebsiteSaleGridLayout = options.Class.extend({
xmlDependencies: ['/website_sale/static/src/xml/website_sale.editor.xml'],
/**
* @override
*/
start: function () {
this.ppg = this.$target.closest('[data-ppg]').data('ppg');
this.ppr = this.$target.closest('[data-ppr]').data('ppr');
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Options
//--------------------------------------------------------------------------
/**
* @see this.selectClass for params
*/
choosePpg: function (previewMode, value, $opt) {
var self = this;
new Dialog(this, {
title: _t("Choose number of products"),
$content: $(qweb.render('website_sale.dialog.choosePPG', {widget: this})),
buttons: [
{text: _t("Save"), classes: 'btn-primary', click: function () {
var $input = this.$('input');
var def = self._setPPG($input.val());
if (!def) {
$input.addClass('is-invalid');
return;
}
return def.then(this.close.bind(this));
}},
{text: _t("Discard"), close: true},
],
}).open();
},
/**
* @see this.selectClass for params
*/
setPpr: function (previewMode, value, $opt) {
this._rpc({
route: '/shop/change_ppr',
params: {
'ppr': value,
},
}).then(reload);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_setActive: function () {
var self = this;
this._super.apply(this, arguments);
this.$el.find('[data-set-ppr]')
.addBack('[data-set-ppr]')
.removeClass('active')
.filter(function () {
var nbColumns = $(this).data('setPpr');
return nbColumns === self.ppr;
})
.addClass('active');
},
/**
* @private
* @param {integer} ppg
* @returns {Promise|false}
*/
_setPPG: function (ppg) {
ppg = parseInt(ppg);
if (!ppg || ppg < 1) {
return false;
}
return this._rpc({
route: '/shop/change_ppg',
params: {
'ppg': ppg,
},
}).then(reload);
},
});
options.registry.WebsiteSaleProductsItem = options.Class.extend({
events: _.extend({}, options.Class.prototype.events || {}, {
'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter',
@@ -101,6 +189,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
start: function () {
var self = this;
this.ppr = this.$target.closest('[data-ppr]').data('ppr');
this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id'));
var defs = [this._super.apply(this, arguments)];
@@ -166,6 +255,9 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
$size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')')
.addClass('selected');
// Adapt size array preview to fit ppr
$size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide();
return this._super.apply(this, arguments);
},
@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="website_sale.dialog.choosePPG">
<div class="form-group row">
<label for="ppg_input" class="col-lg-auto col-form-label">
Choose the number of products to display
</label>
<div class="col">
<input type="number" id="ppg_input" class="form-control" t-att-value="widget.ppg"/>
</div>
</div>
</div>
<!-- Products Search Bar settings dialog -->
<div t-name="website_sale.dialog.productsSearchBar">
<div class="form-group">
+18
View File
@@ -40,6 +40,24 @@
<template id="snippet_options" inherit_id="website.snippet_options" name="e-commerce snippet options">
<xpath expr="." position="inside">
<div data-js="WebsiteSaleGridLayout"
data-selector="#products_grid > table"
data-no-check="true">
<a href="#" class="dropdown-item" role="menuitem" data-no-preview="true" data-choose-ppg="1">
<i class="fa fa-cart-plus"/>Choose number of products
</a>
<div class="dropdown-submenu" role="menuitem" aria-haspopup="true">
<a tabindex="-1" href="#" class="dropdown-item" role="menuitem">
<i class="fa fa-columns"/>Number of Columns
</a>
<div class="dropdown-menu" role="menu" data-no-preview="true">
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="2">2</a>
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="3">3</a>
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="4">4</a>
</div>
</div>
</div>
<div data-js="WebsiteSaleProductsItem"
data-selector="#products_grid .oe_product"
data-no-check="true">
+2 -2
View File
@@ -218,10 +218,10 @@
<t t-set='editor_msg'>Drag building blocks here to customize the header for "<t t-esc='category.name'/>" category.</t>
<div class="mb16" id="category_header" t-att-data-editor-message="editor_msg" t-field="category.website_description"/>
</t>
<table t-if="bins" class="table table-borderless my-2">
<table t-if="bins" class="table table-borderless my-2" t-att-data-ppg="ppg" t-att-data-ppr="ppr">
<colgroup t-ignore="true">
<!-- Force the number of columns (useful when only one row of (x < ppr) products) -->
<col t-foreach="rows" t-as="p"/>
<col t-foreach="ppr" t-as="p"/>
</colgroup>
<tbody>
<tr t-foreach="bins" t-as="tr_product">