Browse Source

Merge pull request #6 from taktik/8.0-pos_dynamic_price

[ADD][8.0] : POS - manage pricelist in Front End POS
pull/13/head
Sylvain LE GAL 9 years ago
parent
commit
7d14e55229
  1. 45
      pos_pricelist/README.rst
  2. 18
      pos_pricelist/__init__.py
  3. 43
      pos_pricelist/__openerp__.py
  4. 95
      pos_pricelist/demo/pos_pricelist_demo.yml
  5. 46
      pos_pricelist/static/src/css/style.css
  6. 155
      pos_pricelist/static/src/js/db.js
  7. 24
      pos_pricelist/static/src/js/main.js
  8. 631
      pos_pricelist/static/src/js/models.js
  9. 32
      pos_pricelist/static/src/js/screens.js
  10. 164
      pos_pricelist/static/src/js/tests.js
  11. 41
      pos_pricelist/static/src/js/widgets.js
  12. 28
      pos_pricelist/test/test.py
  13. 29
      pos_pricelist/views/pos_pricelist_template.xml

45
pos_pricelist/README.rst

@ -0,0 +1,45 @@
Dynamic Price for Odoo Point of Sale
------------------------------------
*************
Motivation :
*************
Many issues report this feature. This why I took decision to start this module
Reported issues :
`odoo 8 POS price list discount has no effect. <https://github.com/odoo/odoo/issues/3579>`_
`ODOO POS Pricelist - Public Price & Discounted Price in Receipt. <https://github.com/odoo/odoo/issues/1758>`_
`V8.0 pos gives wrong price when using min qty in pos pricelist <https://github.com/odoo/odoo/issues/2297>`_
********************
Goal of the module :
********************
The goal of this module is to bring the pricelist computation engine to the POS.
This module loads all the necessary data into the POS in order to have a coherent behaviour (offline/online/backend).
**********************
Implemented features :
**********************
1. Attached pricelist on partner will take effect on the POS, which means that if we attach a pricelist to a partner.
The POS will recognize it and will compute the price according to the rule defined.
2. Fiscal Position of each partner will also be present so taxes will be correctly computed
(conforming to the fiscal position).
- Implemented Rules are :
1. (-1) : Rule based on other pricelist
2. (-2) : Rule based on supplierinfo
3. (default) : Any price type which is set on the product form
******************
Missing features :
******************
- As you may know, product template is not fully implemented in the POS, so I decided to drop it from this module.

18
pos_pricelist/__init__.py

@ -0,0 +1,18 @@
# -#- coding: utf-8 -#-
##############################################################################
# Point Of Sale - Pricelist for POS Odoo
# Copyright (C) 2014 Taktik (http://www.taktik.be)
# @author Adil Houmadi <ah@taktik.be>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

43
pos_pricelist/__openerp__.py

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
##############################################################################
# Point Of Sale - Pricelist for POS Odoo
# Copyright (C) 2014 Taktik (http://www.taktik.be)
# @author Adil Houmadi <ah@taktik.be>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'POS Pricelist',
'version': '1.0.0',
'category': 'Point Of Sale',
'sequence': 1,
'author': 'Adil Houmadi @Taktik',
'summary': 'Pricelist for Point of sale',
'description': """
New feature for the Point Of Sale:
=============================================
Add support for pricelist on the point of sale
""",
'depends': [
"point_of_sale",
],
'data': [
"views/pos_pricelist_template.xml",
],
'demo': [
'demo/pos_pricelist_demo.yml',
],
'installable': True,
'application': False,
'auto_install': False,
}

95
pos_pricelist/demo/pos_pricelist_demo.yml

@ -0,0 +1,95 @@
-
This product will have two rule (min_qty:3 => 10%, min_qty:5 => 30%)
-
!record {model: product.product, id: pos_product_product_1}:
default_code: ABC123
name: POS Product 1
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 50.0
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
available_in_pos: True
-
This product will have one rule (min_qty:2 => 10%)
-
!record {model: product.product, id: pos_product_product_2}:
default_code: ABC124
name: POS Product 2
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 100.0
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
available_in_pos: True
-
This product will have a rule that (based on supplier discount)
-
!record {model: product.product, id: pos_product_product_3}:
default_code: ABC125
name: POS Product 3
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 50.0
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
available_in_pos: True
seller_ids:
- delay: 1
name: base.res_partner_3
pricelist_ids:
- min_quantity : 2.0
price : 80
-
This product belgon to computer category (5% dicount)
-
!record {model: product.product, id: pos_product_product_4}:
default_code: ABC125
name: POS Product 4
type: product
categ_id: product.product_category_4
list_price: 100.0
standard_price: 50.0
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
available_in_pos: True
-
Prepare pricelist items
-
!record {model: product.pricelist.version, id: product.ver0}:
items_id:
- name: 10% Discount on POS Product 1 (Qty 3)
sequence: 2
product_id: pos_product_product_1
base: 1
price_discount: -0.10
min_quantity: 3
- name: 30% Discount on POS Product 1 (Qty 5)
sequence: 1
product_id: pos_product_product_1
min_quantity: 5
base: 1
price_discount: -0.30
- name: 10% Discount (POS Product 2)
sequence: 1
product_id: pos_product_product_2
base: 2
price_discount: -0.10
min_quantity: 2
- name: 20% Discount given by supplier
sequence: 1
min_quantity: 2
product_id: pos_product_product_3
base: -2
- name: 5% Discount on all Computer related products (Qty 2)
sequence: 1
min_quantity: 2
base: 1
categ_id: product.product_category_4
price_discount: -0.05

46
pos_pricelist/static/src/css/style.css

@ -0,0 +1,46 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1060;
display: none;
max-width: 276px;
padding: 1px;
text-align: left;
white-space: normal;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
}
.popover-title {
padding: 8px 14px;
margin: 0;
font-size: 14px;
font-weight: normal;
line-height: 18px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}

155
pos_pricelist/static/src/js/db.js

@ -0,0 +1,155 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_db(instance, module) {
module.PosDB = module.PosDB.extend({
init: function (options) {
options = options || {};
this._super(options);
this.default_pricelist_id = 0;
this.pricelist_by_id = {};
this.pricelist_version_by_id = {};
this.pricelist_item_by_id = {};
this.pricelist_item_sorted = [];
this.product_category_by_id = {};
this.product_category_children = {};
this.product_category_ancestors = {};
this.product_price_type_by_id = {};
this.supplierinfo_by_id = {};
this.pricelist_partnerinfo_by_id = {};
this.fiscal_position_tax_by_id = {};
},
add_fiscal_position_taxes: function (fiscal_position_taxes) {
if (!(fiscal_position_taxes instanceof Array)) {
fiscal_position_taxes = [fiscal_position_taxes];
}
var fiscal_position_tax;
while (fiscal_position_tax = fiscal_position_taxes.pop()) {
this.fiscal_position_tax_by_id[fiscal_position_tax.id] = fiscal_position_tax;
}
},
add_pricelist_partnerinfo: function (pricelist_partnerinfos) {
if (!(pricelist_partnerinfos instanceof Array)) {
pricelist_partnerinfos = [pricelist_partnerinfos];
}
var partner_info;
while (partner_info = pricelist_partnerinfos.pop()) {
this.pricelist_partnerinfo_by_id[partner_info.id] = partner_info;
}
},
add_supplierinfo: function (supplierinfos) {
if (!(supplierinfos instanceof Array)) {
supplierinfos = [supplierinfos];
}
var supplier_info;
while (supplier_info = supplierinfos.pop()) {
this.supplierinfo_by_id[supplier_info.id] = supplier_info;
}
},
add_default_pricelist: function (res_id) {
if (res_id && res_id.length) {
this.default_pricelist_id = res_id[0].res_id;
}
},
add_pricelists: function (pricelists) {
if (!(pricelists instanceof Array)) {
pricelists = [pricelists];
}
var pricelist;
while (pricelist = pricelists.pop()) {
this.pricelist_by_id[pricelist.id] = pricelist;
}
},
add_pricelist_versions: function (versions) {
if (!(versions instanceof Array)) {
versions = [versions];
}
var version;
while (version = versions.pop()) {
this.pricelist_version_by_id[version.id] = version;
}
},
add_pricelist_items: function (items) {
if (!(items instanceof Array)) {
items = [items];
}
var item;
while (item = items.pop()) {
this.pricelist_item_by_id[item.id] = item;
}
this.pricelist_item_sorted = this._items_sorted();
},
add_price_types: function (price_types) {
if (!(price_types instanceof Array)) {
price_types = [price_types];
}
var ptype;
while (ptype = price_types.pop()) {
this.product_price_type_by_id[ptype.id] = ptype;
}
},
add_product_categories: function (categories) {
if (!(categories instanceof Array)) {
categories = [categories];
}
var category;
while (category = categories.pop()) {
this.product_category_by_id[category.id] = category;
this.product_category_children[category.id] = category.child_id
}
this._make_ancestors();
},
_make_ancestors: function () {
var category, ancestors;
for (var id in this.product_category_by_id) {
category = this.product_category_by_id[id];
ancestors = [];
while (category.parent_id) {
ancestors.push(category.parent_id[0]);
category = category.parent_id ? this.product_category_by_id[category.parent_id[0]] : false;
}
this.product_category_ancestors[parseInt(id)] = ancestors;
}
},
_items_sorted: function () {
var items = this.pricelist_item_by_id;
var list = [];
for (var key in items) {
list.push(items[key]);
}
list.sort(function (a, b) {
if (a.sequence < b.sequence) return -1;
if (a.sequence > b.sequence) return 1;
if (a.min_quantity > b.min_quantity) return -1;
if (a.min_quantity < b.min_quantity) return 1;
return 0;
});
return list;
},
find_taxes_by_fiscal_position_id: function (fiscal_position_id) {
var taxes = [];
for (var id in this.fiscal_position_tax_by_id) {
var tax = this.fiscal_position_tax_by_id[id];
if (tax && tax.position_id && tax.position_id[0] == fiscal_position_id) {
taxes.push(tax);
}
}
return taxes;
}
})
}

24
pos_pricelist/static/src/js/main.js

@ -0,0 +1,24 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
openerp.pos_pricelist = function (instance) {
var module = instance.point_of_sale;
pos_pricelist_db(instance, module);
pos_pricelist_models(instance, module);
pos_pricelist_screens(instance, module);
pos_pricelist_widgets(instance, module);
};

631
pos_pricelist/static/src/js/models.js

@ -0,0 +1,631 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_models(instance, module) {
var _t = instance.web._t;
var round_pr = instance.web.round_precision;
var round_di = instance.web.round_decimals;
/**
* @param funcName
* @returns {*}
* @private
*/
Backbone.Model.prototype._super = function (funcName) {
return this.constructor.__super__[funcName].apply(this, _.rest(arguments));
};
/**
* Extend the POS model
*/
module.PosModel = module.PosModel.extend({
initialize: function (session, attributes) {
this._super('initialize', session, attributes);
this.pricelist_engine = new module.PricelistEngine({'pos': this, 'db': this.db, 'pos_widget': this.pos_widget});
arrange_elements(this);
},
/**
* find model based on name
* @param model_name
* @returns {{}}
*/
find_model: function (model_name) {
var self = this;
var lookup = {};
for (var i = 0, len = self.models.length; i < len; i++) {
if (self.models[i].model === model_name) {
lookup[i] = self.models[i]
}
}
return lookup
},
on_removed_order: function (removed_order, index, reason) {
this._super('on_removed_order', removed_order, index, reason);
if ((reason === 'abandon' || removed_order.temporary) && this.get('orders').size() > 0) {
var current_order = (this.get('orders').at(index) || this.get('orders').last());
var partner = current_order.get_client() ? current_order.get_client() : false;
this.pricelist_engine.update_products_ui(partner);
}
}
});
/**
* Extend the order
*/
module.Order = module.Order.extend({
/**
* override this method to merge lines
* TODO : Need some refactoring in the standard POS to Do it better
* TODO : from line 73 till 85, we need to move this to another method
* @param product
* @param options
*/
addProduct: function (product, options) {
options = options || {};
var attr = JSON.parse(JSON.stringify(product));
attr.pos = this.pos;
attr.order = this;
var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
var self = this;
var found = false;
if (options.quantity !== undefined) {
line.set_quantity(options.quantity);
}
if (options.price !== undefined) {
line.set_unit_price(options.price);
}
if (options.discount !== undefined) {
line.set_discount(options.discount);
}
var orderlines = [];
if (self.get('orderLines').models !== undefined) {
orderlines = self.get('orderLines').models;
}
for (var i = 0; i < orderlines.length; i++) {
var _line = orderlines[i];
if (_line && _line.can_be_merged_with(line) && options.merge !== false) {
_line.merge(line);
found = true;
break;
}
}
if (!found) {
this.get('orderLines').add(line);
}
this.selectLine(this.getLastOrderline());
}
});
/**
* Extend the Order line
*/
module.Orderline = module.Orderline.extend({
initialize: function (attr, options) {
this._super('initialize', attr, options);
this.manuel_price = false;
if (options.product !== undefined) {
var qty = this.compute_qty(options.order, options.product);
var partner = options.order.get_client();
var product = options.product;
var db = this.pos.db;
var price = this.pos.pricelist_engine.compute_price_all(db, product, partner, qty);
if (price !== false && price !== 0.0) {
this.price = price;
}
}
},
/**
* @param state
*/
set_manuel_price: function (state) {
this.manuel_price = state;
},
/**
* @param quantity
*/
set_quantity: function (quantity) {
this._super('set_quantity', quantity);
var partner = this.order.get_client();
var product = this.product;
var db = this.pos.db;
var price = this.pos.pricelist_engine.compute_price_all(db, product, partner, quantity);
if (price !== false && price !== 0.0) {
this.price = price;
}
this.trigger('change', this);
},
/**
* override this method to take fiscal positions in consideration
* get all price
* TODO : find a better way to do it : need some refactoring in the pos standard
* @returns {{priceWithTax: *, priceWithoutTax: *, tax: number, taxDetails: {}}}
*/
get_all_prices: function () {
var self = this;
var currency_rounding = this.pos.currency.rounding;
var base = this.get_base_price();
var totalTax = base;
var totalNoTax = base;
var product = this.get_product();
var taxes = this.get_applicable_taxes();
var taxtotal = 0;
var taxdetail = {};
// Add by pos_pricelist
var partner = this.order.get_client();
var fiscal_position_taxes = [];
if (partner && partner.property_account_position) {
fiscal_position_taxes = self.pos.db.find_taxes_by_fiscal_position_id(partner.property_account_position[0]);
}
var product_taxes = [];
for (var i = 0, ilen = fiscal_position_taxes.length; i < ilen; i++) {
var fp_tax = fiscal_position_taxes[i];
for (var j = 0, jlen = taxes.length; j < jlen; j++) {
var p_tax = taxes[j];
if (fp_tax && p_tax && fp_tax.tax_src_id[0] === p_tax.id) {
var dest_tax = _.detect(this.pos.taxes, function (t) {
return t.id === fp_tax.tax_dest_id[0];
});
product_taxes.push(dest_tax);
}
}
}
if (product_taxes.length === 0) {
for (var i = 0, ilen = product.taxes_id; i < ilen; i++) {
var _id = product.taxes_id[i];
var p_tax = _.detect(this.pos.taxes, function (t) {
return t.id === _id;
});
product_taxes.push(p_tax);
}
}
_.each(product_taxes, function (tax) {
if (tax.price_include) {
var tmp;
if (tax.type === "percent") {
tmp = base - round_pr(base / (1 + tax.amount), currency_rounding);
} else if (tax.type === "fixed") {
tmp = round_pr(tax.amount * self.get_quantity(), currency_rounding);
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp, currency_rounding);
taxtotal += tmp;
totalNoTax -= tmp;
taxdetail[tax.id] = tmp;
} else {
var tmp;
if (tax.type === "percent") {
tmp = tax.amount * base;
} else if (tax.type === "fixed") {
tmp = tax.amount * self.get_quantity();
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp, currency_rounding);
if (tax.include_base_amount) {
base += tmp;
}
taxtotal += tmp;
totalTax += tmp;
taxdetail[tax.id] = tmp;
}
});
return {
"priceWithTax": totalTax,
"priceWithoutTax": totalNoTax,
"tax": taxtotal,
"taxDetails": taxdetail
};
},
/**
* Override this method to avoid a return false if the price is different
* Check super method : (this.price !== orderline.price) is not necessary in our case
* @param orderline
* @returns {boolean}
*/
can_be_merged_with: function (orderline) {
var result = this._super('can_be_merged_with', orderline);
if (!result) {
if (!this.manuel_price) {
return (this.get_product().id === orderline.get_product().id);
} else {
return false;
}
}
return true;
},
/**
* Override to set price
* @param orderline
*/
merge: function (orderline) {
this._super('merge', orderline);
this.set_unit_price(orderline.price);
},
/**
* @param order
* @param product
* @returns {number}
*/
compute_qty: function (order, product) {
var qty = 1;
var orderlines = [];
if (order.get('orderLines').models !== undefined) {
orderlines = order.get('orderLines').models;
}
for (var i = 0; i < orderlines.length; i++) {
if (orderlines[i].product.id === product.id && !orderlines[i].manuel_price) {
qty += orderlines[i].quantity;
}
}
return qty;
},
});
/**
* Pricelist Engine to compute price
*/
module.PricelistEngine = instance.web.Class.extend({
init: function(options){
options = options || {};
this.pos = options.pos;
this.db = options.db;
this.pos_widget = options.pos_widget;
},
/**
* compute price for all price list
* @param db
* @param product
* @param partner
* @param qty
* @returns {*}
*/
compute_price_all: function (db, product, partner, qty) {
var price_list_id = false;
if (partner && partner.property_product_pricelist) {
price_list_id = partner.property_product_pricelist[0];
} else {
price_list_id = db.default_pricelist_id;
}
return this.compute_price(db, product, partner, qty, parseInt(price_list_id));
},
/**
* loop find a valid version for the price list id given in param
* @param db
* @param pricelist_id
* @returns {boolean}
*/
find_valid_pricelist_version: function (db, pricelist_id) {
var date = new Date();
var version = false;
var pricelist = db.pricelist_by_id[pricelist_id];
for (var i = 0, len = pricelist.version_id.length; i < len; i++) {
var v = db.pricelist_version_by_id[pricelist.version_id[i]];
if (((v.date_start == false) || (new Date(v.date_start) <= date)) &&
((v.date_end == false) || (new Date(v.date_end) >= date))) {
version = v;
break;
}
}
return version;
},
/**
* compute the price for the given product
* @param database
* @param product
* @param partner
* @param qty
* @param pricelist_id
* @returns {boolean}
*/
compute_price: function (database, product, partner, qty, pricelist_id) {
var self = this;
var db = database;
// get a valid version
var version = this.find_valid_pricelist_version(db, pricelist_id);
if (version == false) {
var message = _t('Pricelist Error');
var comment = _t('At least one pricelist has no active version ! Please create or activate one.');
show_error(this, message, comment);
return false;
}
// get categories
var categ_ids = [];
if (product.categ_id) {
categ_ids.push(product.categ_id[0]);
categ_ids = categ_ids.concat(db.product_category_ancestors[product.categ_id[0]]);
}
// find items
var items = [], i, len;
for (i = 0, len = db.pricelist_item_sorted.length; i < len; i++) {
var item = db.pricelist_item_sorted[i];
if ((item.product_id === false || item.product_id[0] === product.id) &&
(item.categ_id === false || categ_ids.indexOf(item.categ_id[0]) !== -1) &&
(item.price_version_id[0] === version.id)) {
items.push(item);
}
}
var results = {};
results[product.id] = 0.0;
var price_types = {};
var price = false;
// loop through items
for (i = 0, len = items.length; i < len; i++) {
var rule = items[i];
if (rule.min_quantity && qty < rule.min_quantity) {
continue;
}
if (rule.product_id && rule.product_id[0] && product.id != rule.product_id[0]) {
continue;
}
if (rule.categ_id) {
var cat_id = product.categ_id[0];
while (cat_id) {
if (cat_id == rule.categ_id[0]) {
break;
}
cat_id = db.product_category_by_id[cat_id].parent_id[0];
}
if (!(cat_id)) {
continue;
}
}
// Based on field
switch (rule.base) {
case -1:
if (rule.base_pricelist_id) {
price = self.compute_price(db, product, false, qty, rule.base_pricelist_id[0]);
}
break;
case -2:
var seller = false;
for (var index in product.seller_ids) {
var seller_id = product.seller_ids[index];
var _tmp_seller = db.supplierinfo_by_id[seller_id];
if ((!partner) || (_tmp_seller.name.length && _tmp_seller.name[0] != partner.name))
continue;
seller = _tmp_seller
}
if (!seller && product.seller_ids) {
seller = db.supplierinfo_by_id[product.seller_ids[0]];
}
if (seller) {
for (var _id in seller.pricelist_ids) {
var info_id = seller.pricelist_ids[_id];
var line = db.pricelist_partnerinfo_by_id[info_id];
if (line.min_quantity <= qty) {
price = line.price
}
}
}
break;
default:
if (!price_types.hasOwnProperty(rule.base)) {
price_types[rule.base] = db.product_price_type_by_id[rule.base];
}
var price_type = price_types[rule.base];
if (db.product_by_id[product.id].hasOwnProperty(price_type.field)) {
price = db.product_by_id[product.id][price_type.field];
}
}
if (price !== false) {
var price_limit = price;
price = price * (1.0 + (rule['price_discount'] ? rule['price_discount'] : 0.0))
if (rule['price_round']) {
price = parseFloat(price.toFixed(Math.ceil(Math.log(1.0 / rule['price_round']) / Math.log(10))));
}
price += (rule['price_surcharge'] ? rule['price_surcharge'] : 0.0);
if (rule['price_min_margin']) {
price = Math.max(price, price_limit + rule['price_min_margin'])
}
if (rule['price_max_margin']) {
price = Math.min(price, price_limit + rule['price_min_margin'])
}
}
break;
}
return price
},
/**
* @param partner
*/
update_products_ui: function (partner) {
var db = this.db;
if(!this.pos_widget.product_screen) return;
var product_list_ui = this.pos_widget.product_screen.$('.product-list span.product');
for (var i = 0, len = product_list_ui.length; i < len; i++) {
var product_ui = product_list_ui[i];
var product_id = $(product_ui).data('product-id');
var product = db.get_product_by_id(product_id);
var price = this.compute_price_all(db, product, partner, 1);
if (price !== false && price !== 0.0) {
price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']);
price = this.pos_widget.format_currency(price);
$(product_ui).find('.price-tag').html(price);
}
}
},
/**
*
* @param partner
* @param orderLines
*/
update_ticket: function (partner, orderLines) {
var db = this.db;
for (var i = 0, len = orderLines.length; i < len; i++) {
var line = orderLines[i];
var product = line.product;
var quantity = line.quantity;
var price = this.compute_price_all(db, product, partner, quantity);
if (price !== false && price !== 0.0) {
line.price = price;
}
line.trigger('change', line);
}
}
});
/**
* show error
* @param context
* @param message
* @param comment
*/
function show_error(context, message, comment) {
context.pos.pos_widget.screen_selector.show_popup('error', {
'message': message,
'comment': comment
});
}
/**
* patch models to load some entities
* @param pos_model
*/
function arrange_elements(pos_model) {
var product_model = pos_model.find_model('product.product');
if (_.size(product_model) == 1) {
var product_index = parseInt(Object.keys(product_model)[0]);
pos_model.models[product_index].fields.push('categ_id', 'seller_ids');
}
var res_product_pricelist = pos_model.find_model('product.pricelist');
if (_.size(res_product_pricelist) == 1) {
var pricelist_index = parseInt(Object.keys(res_product_pricelist)[0]);
pos_model.models.splice(++pricelist_index, 0,
{
model: 'account.fiscal.position.tax',
fields: ['display_name', 'position_id', 'tax_src_id', 'tax_dest_id'],
domain: null,
loaded: function (self, fiscal_position_taxes) {
self.db.add_fiscal_position_taxes(fiscal_position_taxes);
}
},
{
model: 'pricelist.partnerinfo',
fields: ['display_name', 'min_quantity', 'name', 'price', 'suppinfo_id'],
domain: null,
loaded: function (self, pricelist_partnerinfos) {
self.db.add_pricelist_partnerinfo(pricelist_partnerinfos);
}
},
{
model: 'product.supplierinfo',
fields: ['delay', 'name', 'min_qty', 'pricelist_ids', 'product_code', 'product_name', 'sequence',
'qty', 'product_tmpl_id'],
domain: null,
loaded: function (self, supplierinfos) {
self.db.add_supplierinfo(supplierinfos);
}
},
{
model: 'product.category',
fields: ['name', 'display_name', 'parent_id', 'child_id'],
domain: null,
loaded: function (self, categories) {
self.db.add_product_categories(categories);
}
},
{
model: 'ir.model.data',
fields: ['res_id'],
domain: function () {
return [
['module', '=', 'product'],
['name', '=', 'property_product_pricelist']
]
},
loaded: function (self, res) {
self.db.add_default_pricelist(res);
}
},
{
model: 'product.pricelist',
fields: ['display_name', 'name', 'version_id', 'currency_id'],
domain: function () {
return [
['type', '=', 'sale']
]
},
loaded: function (self, pricelists) {
self.db.add_pricelists(pricelists);
}
},
{
model: 'product.pricelist.version',
fields: ['name', 'pricelist_id', 'date_start', 'date_end', 'items'],
domain: null,
loaded: function (self, versions) {
self.db.add_pricelist_versions(versions);
}
},
{
model: 'product.pricelist.item',
fields: ['name', 'base', 'base_pricelist_id', 'categ_id', 'min_quantity',
'price_discount', 'price_max_margin', 'price_min_margin', 'price_round', 'price_surcharge',
'price_version_id', 'product_id', 'product_tmpl_id', 'sequence'
],
domain: null,
loaded: function (self, items) {
self.db.add_pricelist_items(items);
}
},
{
model: 'product.price.type',
fields: ['name', 'field', 'currency_id'],
domain: null,
loaded: function (self, price_types) {
// we need to add price type field to product.product model if not the case
var product_model = posmodel.find_model('product.product');
for(var i = 0, len = price_types.length; i < len; i++) {
var p_type = price_types[i].field;
if (_.size(product_model) == 1) {
var product_index = parseInt(Object.keys(product_model)[0]);
if(posmodel.models[product_index].fields.indexOf(p_type) === -1) {
posmodel.models[product_index].fields.push(p_type);
}
}
}
self.db.add_price_types(price_types);
}
}
);
}
var res_partner_model = pos_model.find_model('res.partner');
if (_.size(res_partner_model) == 1) {
var res_partner_index = parseInt(Object.keys(res_partner_model)[0]);
pos_model.models[res_partner_index].fields.push('property_account_position', 'property_product_pricelist');
}
}
}

32
pos_pricelist/static/src/js/screens.js

@ -0,0 +1,32 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_screens(instance, module) {
module.ClientListScreenWidget = module.ClientListScreenWidget.extend({
save_changes: function () {
this._super();
if (this.has_client_changed()) {
var currentOrder = this.pos.get('selectedOrder');
var orderLines = currentOrder.get('orderLines').models;
var partner = currentOrder.get_client();
this.pos.pricelist_engine.update_products_ui(partner);
this.pos.pricelist_engine.update_ticket(partner, orderLines);
}
}
});
}

164
pos_pricelist/static/src/js/tests.js

@ -0,0 +1,164 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
(function () {
'use strict';
openerp.Tour.register({
id: 'pos_pricelist_order',
name: 'Complete a order trough the Front-End using POS Pricelist',
path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening',
mode: 'test',
steps: [
{
wait: 2000,
title: 'Wait for screen to be ready'
},
{
wait: 2000,
title: 'Load the Session',
waitNot: '.oe_loading:visible',
element: 'span:contains("Resume Session"),span:contains("Start Session")'
},
{
title: 'Loading Screen',
waitFor: '.loader'
},
{
wait: 2000,
title: 'The Point of Sale',
waitFor: '.pos'
},
{
title: "We will buy some Products!, let's add (POS Product 1)",
element: '.product-list .product-name:contains("POS Product 1")'
},
{
wait: 5000,
title: 'The order total has been updated to the correct value : 100€',
waitFor: '.order .total .value:contains("100.00 €")'
},
{
wait: 5000,
title: 'We will add one more unit!',
element: '.product-list .product-name:contains("POS Product 1")'
},
{
wait: 4000,
title: 'We will add another unit',
element: '.product-list .product-name:contains("POS Product 1")'
},
{
wait: 4000,
title: 'The order total should be updated : 270€ which means 90€/Unit (Rule 10% Discount from 3 Units)',
waitFor: '.order .total .value:contains("270.00 €")'
},
{
wait: 8000,
title: 'We will add another product',
element: '.product-list .product-name:contains("POS Product 2")'
},
{
wait: 4000,
title: 'We will add another unit for this product (POS Product 2)',
element: '.product-list .product-name:contains("POS Product 2")'
},
{
wait: 4000,
title: "Let's verify the total that we should pay, it's should be equal to : 450€, which means that <br>" +
"10% Discount if offered if we buy 2 units of (POS Product 2), Rule based on standard price",
waitFor: '.order .total .value:contains("450.00 €")'
},
{
wait: 10000,
title: "Now, we will add (POS Product 3), for this product if we buy more then 2 units <br>" +
"20% Discount is given by supplier to our customers",
element: '.product-list .product-name:contains("POS Product 3")'
},
{
wait: 10000,
title: 'We will add another unit for this product (POS Product 3)',
element: '.product-list .product-name:contains("POS Product 3")'
},
{
wait: 5000,
title: "Let's check the total (610€)",
waitFor: '.order .total .value:contains("610.00 €")'
},
{
wait: 5000,
title: "Now, we will add (POS Product 4), this product belong to (Comptuer) category in which " +
"we apply 5% if customer buy more then 2 products",
element: '.product-list .product-name:contains("POS Product 4")'
},
{
wait: 10000,
title: 'We will add another unit for this product (POS Product 4)',
element: '.product-list .product-name:contains("POS Product 4")'
},
{
wait: 5000,
title: "Let's check the total again (800€)",
waitFor: '.order .total .value:contains("800.00 €")'
},
{
wait: 5000,
title: "Let's pay the order",
element: ".paypad-button:contains('Bank')"
},
{
wait: 1000,
title: "Let's accept the payment",
onload: function () {
window._print = window.print;
window.print = function () {
console.log('Print!')
};
},
element: ".button .iconlabel:contains('Validate'):visible"
},
{
wait: 1000,
title: "Let's finish the order",
element: ".button:not(.disabled) .iconlabel:contains('Next'):visible"
},
{
wait: 1000,
onload: function () {
window.print = window._print;
window._print = undefined;
},
title: "Let's wait for the order posting",
waitFor: ".oe_status.js_synch .js_connected:visible"
},
{
wait: 1000,
title: "Let's close the Point of Sale",
element: ".header-button:contains('Close')"
},
{
title: "Let's confirm",
element: ".header-button.confirm:contains('Confirm')"
},
{
title: "Wait for the backend to ready itself",
element: 'span:contains("Resume Session"),span:contains("Start Session")'
}
]
});
})();

41
pos_pricelist/static/src/js/widgets.js

@ -0,0 +1,41 @@
/******************************************************************************
* Point Of Sale - Pricelist for POS Odoo
* Copyright (C) 2014 Taktik (http://www.taktik.be)
* @author Adil Houmadi <ah@taktik.be>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************************/
function pos_pricelist_widgets(instance, module) {
module.OrderWidget = module.OrderWidget.extend({
set_value: function (val) {
this._super(val);
var order = this.pos.get('selectedOrder');
if (this.editable && order.getSelectedLine()) {
var mode = this.numpad_state.get('mode');
if (mode === 'price') {
order.getSelectedLine().set_manuel_price(true);
}
}
}
});
module.OrderButtonWidget = module.OrderButtonWidget.extend({
selectOrder: function (event) {
this._super(event);
var partner = this.order.get_client() ? this.order.get_client() : false;
this.pos.pricelist_engine.update_products_ui(partner);
}
})
}

28
pos_pricelist/test/test.py

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
##############################################################################
# Point Of Sale - Pricelist for POS Odoo
# Copyright (C) 2014 Taktik (http://www.taktik.be)
# @author Adil Houmadi <ah@taktik.be>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import openerp.tests
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestPOS(openerp.tests.HttpCase):
def test_01_pos(self):
self.phantom_js("/", "openerp.Tour.run('pos_pricelist_order', 'test')",
"openerp.Tour.tours.pos_pricelist_order",
login="admin")

29
pos_pricelist/views/pos_pricelist_template.xml

@ -0,0 +1,29 @@
<openerp>
<data>
<template id="pos_pricelist_assets_backend"
name="pos_pricelist_assets_backend"
inherit_id="point_of_sale.assets_backend">
<xpath expr="." position="inside">
<script src="/pos_pricelist/static/src/js/db.js"
type="text/javascript"></script>
<script src="/pos_pricelist/static/src/js/models.js"
type="text/javascript"></script>
<script src="/pos_pricelist/static/src/js/widgets.js"
type="text/javascript"></script>
<script src="/pos_pricelist/static/src/js/screens.js"
type="text/javascript"></script>
<script src="/pos_pricelist/static/src/js/tests.js"
type="text/javascript"></script>
<script src="/pos_pricelist/static/src/js/main.js"
type="text/javascript"></script>
</xpath>
</template>
<template id="pos_pricelist_assets" name="pos_pricelist_css" inherit_id="point_of_sale.index">
<xpath expr="//script[@id='loading-script']" position="before">
<link rel="stylesheet" href="/pos_pricelist/static/src/css/style.css"/>
</xpath>
</template>
</data>
</openerp>
Loading…
Cancel
Save