From d8b5fce657426f5c6277180f414552e8f6bb594e Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 22:55:46 +0100 Subject: [PATCH] [ADD] : Dynamic Price for POS --- pos_dynamic_price/README.md | 34 ++ pos_dynamic_price/__init__.py | 18 + pos_dynamic_price/__openerp__.py | 40 ++ pos_dynamic_price/static/src/js/db.js | 156 +++++ pos_dynamic_price/static/src/js/helper.js | 27 + pos_dynamic_price/static/src/js/main.js | 23 + pos_dynamic_price/static/src/js/models.js | 546 ++++++++++++++++++ pos_dynamic_price/static/src/js/screens.js | 41 ++ .../view/pos_dynamic_price_template.xml | 20 + 9 files changed, 905 insertions(+) create mode 100644 pos_dynamic_price/README.md create mode 100644 pos_dynamic_price/__init__.py create mode 100644 pos_dynamic_price/__openerp__.py create mode 100644 pos_dynamic_price/static/src/js/db.js create mode 100644 pos_dynamic_price/static/src/js/helper.js create mode 100644 pos_dynamic_price/static/src/js/main.js create mode 100644 pos_dynamic_price/static/src/js/models.js create mode 100644 pos_dynamic_price/static/src/js/screens.js create mode 100644 pos_dynamic_price/view/pos_dynamic_price_template.xml diff --git a/pos_dynamic_price/README.md b/pos_dynamic_price/README.md new file mode 100644 index 00000000..16d4c188 --- /dev/null +++ b/pos_dynamic_price/README.md @@ -0,0 +1,34 @@ +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. +###ODOO POS Pricelist - Public Price & Discounted Price in Receipt +###V8.0 pos gives wrong price when using min qty in pos pricelist + +### 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 : + +- 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. + +- 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. \ No newline at end of file diff --git a/pos_dynamic_price/__init__.py b/pos_dynamic_price/__init__.py new file mode 100644 index 00000000..0760a2d3 --- /dev/null +++ b/pos_dynamic_price/__init__.py @@ -0,0 +1,18 @@ +# -#- coding: utf-8 -#- +############################################################################## +# Point Of Sale - Dynamic Price for POS Odoo +# Copyright (C) 2014 Taktik (http://www.taktik.be) +# @author Adil Houmadi +# +# 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 . +# +############################################################################## \ No newline at end of file diff --git a/pos_dynamic_price/__openerp__.py b/pos_dynamic_price/__openerp__.py new file mode 100644 index 00000000..d39dd219 --- /dev/null +++ b/pos_dynamic_price/__openerp__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +############################################################################## +# Point Of Sale - Dynamic Price for POS Odoo +# Copyright (C) 2014 Taktik (http://www.taktik.be) +# @author Adil Houmadi +# +# 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 . +# +############################################################################## +{ + 'name': 'POS Dynamic Price', + 'version': '1.0.0', + 'category': 'Point Of Sale', + 'sequence': 1, + 'author': 'Adil Houmadi @Taktik', + 'summary': 'Dyanmic Price Point of sale', + 'description': """ +New features for the Point Of Sale: +============================================= + Dynamic price on the point of sale + """, + 'depends': [ + "point_of_sale", + ], + 'data': [ + "view/pos_dynamic_price_template.xml", + ], + 'installable': True, + 'application': False, + 'auto_install': False, +} \ No newline at end of file diff --git a/pos_dynamic_price/static/src/js/db.js b/pos_dynamic_price/static/src/js/db.js new file mode 100644 index 00000000..da160bf8 --- /dev/null +++ b/pos_dynamic_price/static/src/js/db.js @@ -0,0 +1,156 @@ +/****************************************************************************** +* Point Of Sale - Dynamic Price for POS Odoo +* Copyright (C) 2014 Taktik (http://www.taktik.be) +* @author Adil Houmadi +* +* 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 . +* +******************************************************************************/ +function pdp_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_catrgory_by_id = {}; + this.product_catrgory_children = {}; + this.product_catrgory_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]; + } + for (var i = 0, len = fiscal_position_taxes.length; i < len; i++) { + var fiscal_position_tax = fiscal_position_taxes[i]; + 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]; + } + for (var i = 0, len = pricelist_partnerinfos.length; i < len; i++) { + var partner_info = pricelist_partnerinfos[i]; + this.pricelist_partnerinfo_by_id[partner_info.id] = partner_info; + } + }, + add_supplierinfo: function (supplierinfos) { + if (!(supplierinfos instanceof Array)) { + supplierinfos = [supplierinfos]; + } + for (var i = 0, len = supplierinfos.length; i < len; i++) { + var supplier_info = supplierinfos[i]; + 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]; + } + for (var i = 0, len = pricelists.length; i < len; i++) { + var pricelist = pricelists[i]; + this.pricelist_by_id[pricelist.id] = pricelist; + } + }, + add_pricelist_versions: function (versions) { + if (!(versions instanceof Array)) { + versions = [versions]; + } + for (var i = 0, len = versions.length; i < len; i++) { + var version = versions[i]; + this.pricelist_version_by_id[version.id] = version; + } + }, + add_pricelist_items: function (items) { + if (!(items instanceof Array)) { + items = [items]; + } + for (var i = 0, len = items.length; i < len; i++) { + var item = items[i]; + 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]; + } + for (var i = 0, len = price_types.length; i < len; i++) { + var ptype = price_types[i]; + this.product_price_type_by_id[ptype.id] = ptype; + } + }, + add_product_categories: function (categories) { + var self = this; + if (!(categories instanceof Array)) { + categories = [categories]; + } + for (var i = 0, len = categories.length; i < len; i++) { + var category = categories[i]; + this.product_catrgory_by_id[category.id] = category; + this.product_catrgory_children[category.id] = category.child_id + } + function make_ancestors(cat_id, ancestors) { + self.product_catrgory_ancestors[cat_id] = ancestors; + ancestors = ancestors.slice(0); + ancestors.push(cat_id); + var children = self.product_catrgory_children[cat_id] || []; + for (var i = 0, len = children.length; i < len; i++) { + make_ancestors(children[i], ancestors); + } + } + if (categories.length) { + var cat = categories[0]; + make_ancestors(cat.id, cat.parent_id === false ? [] : [cat.parent_id]) + } + }, + _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; + } + }) +} \ No newline at end of file diff --git a/pos_dynamic_price/static/src/js/helper.js b/pos_dynamic_price/static/src/js/helper.js new file mode 100644 index 00000000..b5a22eb2 --- /dev/null +++ b/pos_dynamic_price/static/src/js/helper.js @@ -0,0 +1,27 @@ +/****************************************************************************** +* Point Of Sale - Dynamic Price for POS Odoo +* Copyright (C) 2014 Taktik (http://www.taktik.be) +* @author Adil Houmadi +* +* 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 . +* +******************************************************************************/ +Object.size = function (obj) { + "use strict"; + var size = 0, key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + size += 1; + } + } + return size; +}; \ No newline at end of file diff --git a/pos_dynamic_price/static/src/js/main.js b/pos_dynamic_price/static/src/js/main.js new file mode 100644 index 00000000..dcf31eed --- /dev/null +++ b/pos_dynamic_price/static/src/js/main.js @@ -0,0 +1,23 @@ +/****************************************************************************** +* Point Of Sale - Dynamic Price for POS Odoo +* Copyright (C) 2014 Taktik (http://www.taktik.be) +* @author Adil Houmadi +* +* 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 . +* +******************************************************************************/ +openerp.pos_dynamic_price = function (instance) { + var module = instance.point_of_sale; + pdp_db(instance, module); + pdp_models(instance, module); + pdp_screens(instance, module); +}; \ No newline at end of file diff --git a/pos_dynamic_price/static/src/js/models.js b/pos_dynamic_price/static/src/js/models.js new file mode 100644 index 00000000..7f56a8d0 --- /dev/null +++ b/pos_dynamic_price/static/src/js/models.js @@ -0,0 +1,546 @@ +/****************************************************************************** +* Point Of Sale - Dynamic Price for POS Odoo +* Copyright (C) 2014 Taktik (http://www.taktik.be) +* @author Adil Houmadi +* +* 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 . +* +******************************************************************************/ +function pdp_models(instance, module) { + + var _t = instance.web._t; + var round_pr = instance.web.round_precision + + /** + * @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); + 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 + } + }); + + /** + * Extend the order + */ + module.Order = module.Order.extend({ + initialize: function (attributes) { + this._super('initialize', attributes); + }, + /** + * override this method to merge lines + * TODO : find a better way to do it + * @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); + 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 = self.posmodel.db; + var price = this.compute_price_all(db, product, partner, qty); + if (price !== false && price !== 0.0) { + this.price = price; + } + } + }, + /** + * @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.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 postions in consideration + * get all price + * @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 partner = this.order.get_client(); + var taxes_ids = product.taxes_id; + 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_ids = []; + 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_ids.length; j < jlen; j++) { + var p_tax = taxes_ids[j]; + if (fp_tax && p_tax && fp_tax.tax_src_id[0] === p_tax) { + product_taxes_ids.push(fp_tax.tax_dest_id[0]); + } + } + } + if (product_taxes_ids.length === 0) { + product_taxes_ids = taxes_ids; + } + var taxes = self.pos.taxes; + var taxtotal = 0; + var taxdetail = {}; + _.each(product_taxes_ids, function (el) { + var tax = _.detect(taxes, function (t) { + return t.id === el; + }); + 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); + taxtotal += tmp; + totalTax += tmp; + taxdetail[tax.id] = tmp; + } + }); + return { + "priceWithTax": totalTax, + "priceWithoutTax": totalNoTax, + "tax": taxtotal, + "taxDetails": taxdetail + }; + }, + + /** + * 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)); + }, + /** + * 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) { + if (this.get_product().id !== orderline.get_product().id) { + return false; + } else if (!this.get_unit()) { + return false; + } else if (this.get_product_type() !== orderline.get_product_type()) { + return false; + } else return this.get_discount() <= 0; + }, + /** + * 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) { + qty += orderlines[i].quantity; + } + } + return qty; + }, + /** + * 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) { + debugger; + 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_catrgory_ancestors[product.categ_id[0]]); + } + + // find items + var items = [], i; + for (var 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 (var 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_catrgory_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 + } + }); + + /** + * show error based on pop up + * @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 (Object.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 (Object.size(res_product_pricelist) == 1) { + var pricelist_index = parseInt(Object.keys(res_product_pricelist)[0]); + + // after the pricelist we can load all pricelists, versions and items + 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) { + self.db.add_price_types(price_types); + } + } + ); + } + + var res_partner_model = pos_model.find_model('res.partner'); + if (Object.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'); + } + + } + +} \ No newline at end of file diff --git a/pos_dynamic_price/static/src/js/screens.js b/pos_dynamic_price/static/src/js/screens.js new file mode 100644 index 00000000..e2d088d0 --- /dev/null +++ b/pos_dynamic_price/static/src/js/screens.js @@ -0,0 +1,41 @@ +/****************************************************************************** +* Point Of Sale - Dynamic Price for POS Odoo +* Copyright (C) 2014 Taktik (http://www.taktik.be) +* @author Adil Houmadi +* +* 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 . +* +******************************************************************************/ +function pdp_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; + for (var i = 0, len = orderLines.length; i < len; i++) { + var line = orderLines[i]; + var partner = currentOrder.get_client(); + var product = line.product; + var db = self.posmodel.db; + var quantity = line.quantity; + var price = line.compute_price_all(db, product, partner, quantity); + if (price !== false && price !== 0.0) { + line.price = price; + } + line.trigger('change', line); + } + } + } + }); +} diff --git a/pos_dynamic_price/view/pos_dynamic_price_template.xml b/pos_dynamic_price/view/pos_dynamic_price_template.xml new file mode 100644 index 00000000..5f9f7b8c --- /dev/null +++ b/pos_dynamic_price/view/pos_dynamic_price_template.xml @@ -0,0 +1,20 @@ + + + + +