From d8b5fce657426f5c6277180f414552e8f6bb594e Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 22:55:46 +0100 Subject: [PATCH 01/12] [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 @@ + + + + + From d71e9f2f8ced6dca3529775f473e6711777403d2 Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 23:20:56 +0100 Subject: [PATCH 02/12] flake8 is not happy :) --- pos_dynamic_price/__init__.py | 3 ++- pos_dynamic_price/__openerp__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pos_dynamic_price/__init__.py b/pos_dynamic_price/__init__.py index 0760a2d3..d740d47b 100644 --- a/pos_dynamic_price/__init__.py +++ b/pos_dynamic_price/__init__.py @@ -15,4 +15,5 @@ # 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 index d39dd219..755ce6f9 100644 --- a/pos_dynamic_price/__openerp__.py +++ b/pos_dynamic_price/__openerp__.py @@ -37,4 +37,4 @@ New features for the Point Of Sale: 'installable': True, 'application': False, 'auto_install': False, -} \ No newline at end of file +} From 7040fe9eea05553495b171e44bb9354fdb5b839b Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 23:32:08 +0100 Subject: [PATCH 03/12] [FIX] flask8 --- pos_dynamic_price/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pos_dynamic_price/__init__.py b/pos_dynamic_price/__init__.py index d740d47b..80169306 100644 --- a/pos_dynamic_price/__init__.py +++ b/pos_dynamic_price/__init__.py @@ -16,4 +16,3 @@ # along with this program. If not, see . # ############################################################################## - From af03649502c838249f0ba8ebef49de6a687d9eab Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 23:32:40 +0100 Subject: [PATCH 04/12] Revert "[FIX] flask8" This reverts commit 7040fe9eea05553495b171e44bb9354fdb5b839b. --- pos_dynamic_price/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pos_dynamic_price/__init__.py b/pos_dynamic_price/__init__.py index 80169306..d740d47b 100644 --- a/pos_dynamic_price/__init__.py +++ b/pos_dynamic_price/__init__.py @@ -16,3 +16,4 @@ # along with this program. If not, see . # ############################################################################## + From 978c7412fb490d5e343b0bcb8ec7d104cf02265f Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 30 Nov 2014 23:33:36 +0100 Subject: [PATCH 05/12] [FIX] Flake8 --- pos_dynamic_price/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pos_dynamic_price/__init__.py b/pos_dynamic_price/__init__.py index d740d47b..80169306 100644 --- a/pos_dynamic_price/__init__.py +++ b/pos_dynamic_price/__init__.py @@ -16,4 +16,3 @@ # along with this program. If not, see . # ############################################################################## - From 91fc78d2abdc862ea688fe5c4d5da2779a784a44 Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Wed, 17 Dec 2014 23:49:21 +0100 Subject: [PATCH 06/12] [CHG] : Rename README to README.rst (https://github.com/OCA/pos/pull/6#issuecomment-66383621) [CHG] : Rename module to pos_pricelist instead of the old one [FIX] : Fix typo mentioned on this comments : - https://github.com/OCA/pos/pull/6#discussion_r21574900 - https://github.com/OCA/pos/pull/6#commitcomment-9012800 [IMP] : Recover a missed feature while setting a price for an orderline https://github.com/OCA/pos/pull/6#discussion_r21575474 [IMP] : Switch to while loop while iterating over an array [REM] : Avoid bad practice on Object Class : https://github.com/OCA/pos/pull/6#commitcomment-9012727 --- pos_dynamic_price/static/src/js/helper.js | 27 ------- .../view/pos_dynamic_price_template.xml | 20 ----- .../README.md => pos_pricelist/README.rst | 6 +- .../__init__.py | 2 +- .../__openerp__.py | 10 +-- .../static/src/js/db.js | 75 ++++++++++--------- .../static/src/js/main.js | 13 ++-- .../static/src/js/models.js | 52 +++++++------ .../static/src/js/screens.js | 4 +- pos_pricelist/static/src/js/widgets.js | 33 ++++++++ .../views/pos_pricelist_template.xml | 29 +++++++ 11 files changed, 147 insertions(+), 124 deletions(-) delete mode 100644 pos_dynamic_price/static/src/js/helper.js delete mode 100644 pos_dynamic_price/view/pos_dynamic_price_template.xml rename pos_dynamic_price/README.md => pos_pricelist/README.rst (92%) rename {pos_dynamic_price => pos_pricelist}/__init__.py (95%) rename {pos_dynamic_price => pos_pricelist}/__openerp__.py (86%) rename {pos_dynamic_price => pos_pricelist}/static/src/js/db.js (71%) rename {pos_dynamic_price => pos_pricelist}/static/src/js/main.js (78%) rename {pos_dynamic_price => pos_pricelist}/static/src/js/models.js (94%) rename {pos_dynamic_price => pos_pricelist}/static/src/js/screens.js (95%) create mode 100644 pos_pricelist/static/src/js/widgets.js create mode 100644 pos_pricelist/views/pos_pricelist_template.xml diff --git a/pos_dynamic_price/static/src/js/helper.js b/pos_dynamic_price/static/src/js/helper.js deleted file mode 100644 index b5a22eb2..00000000 --- a/pos_dynamic_price/static/src/js/helper.js +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** -* 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/view/pos_dynamic_price_template.xml b/pos_dynamic_price/view/pos_dynamic_price_template.xml deleted file mode 100644 index 5f9f7b8c..00000000 --- a/pos_dynamic_price/view/pos_dynamic_price_template.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/pos_dynamic_price/README.md b/pos_pricelist/README.rst similarity index 92% rename from pos_dynamic_price/README.md rename to pos_pricelist/README.rst index 16d4c188..ea046571 100644 --- a/pos_dynamic_price/README.md +++ b/pos_pricelist/README.rst @@ -21,7 +21,8 @@ This module loads all the necessary data into the POS in order to have a coheren - 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). +- 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
@@ -30,5 +31,4 @@ The POS will recognize it and will compute the price according to the rule defin ### 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 +- 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_pricelist/__init__.py similarity index 95% rename from pos_dynamic_price/__init__.py rename to pos_pricelist/__init__.py index 80169306..6d755589 100644 --- a/pos_dynamic_price/__init__.py +++ b/pos_pricelist/__init__.py @@ -1,6 +1,6 @@ # -#- coding: utf-8 -#- ############################################################################## -# Point Of Sale - Dynamic Price for POS Odoo +# Point Of Sale - Pricelist for POS Odoo # Copyright (C) 2014 Taktik (http://www.taktik.be) # @author Adil Houmadi # diff --git a/pos_dynamic_price/__openerp__.py b/pos_pricelist/__openerp__.py similarity index 86% rename from pos_dynamic_price/__openerp__.py rename to pos_pricelist/__openerp__.py index 755ce6f9..d254085b 100644 --- a/pos_dynamic_price/__openerp__.py +++ b/pos_pricelist/__openerp__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# Point Of Sale - Dynamic Price for POS Odoo +# Point Of Sale - Pricelist for POS Odoo # Copyright (C) 2014 Taktik (http://www.taktik.be) # @author Adil Houmadi # @@ -17,22 +17,22 @@ # ############################################################################## { - 'name': 'POS Dynamic Price', + 'name': 'POS Pricelist', 'version': '1.0.0', 'category': 'Point Of Sale', 'sequence': 1, 'author': 'Adil Houmadi @Taktik', - 'summary': 'Dyanmic Price Point of sale', + 'summary': 'Pricelist for Point of sale', 'description': """ New features for the Point Of Sale: ============================================= - Dynamic price on the point of sale + Add support for pricelist on the point of sale """, 'depends': [ "point_of_sale", ], 'data': [ - "view/pos_dynamic_price_template.xml", + "views/pos_pricelist_template.xml", ], 'installable': True, 'application': False, diff --git a/pos_dynamic_price/static/src/js/db.js b/pos_pricelist/static/src/js/db.js similarity index 71% rename from pos_dynamic_price/static/src/js/db.js rename to pos_pricelist/static/src/js/db.js index da160bf8..9e5e2715 100644 --- a/pos_dynamic_price/static/src/js/db.js +++ b/pos_pricelist/static/src/js/db.js @@ -1,5 +1,5 @@ /****************************************************************************** -* Point Of Sale - Dynamic Price for POS Odoo +* Point Of Sale - Pricelist for POS Odoo * Copyright (C) 2014 Taktik (http://www.taktik.be) * @author Adil Houmadi * @@ -15,7 +15,9 @@ * along with this program. If not, see . * ******************************************************************************/ -function pdp_db(instance, module) { +function pos_pricelist_db(instance, module) { + + console.log('Loading ...'); module.PosDB = module.PosDB.extend({ init: function (options) { @@ -26,9 +28,9 @@ function pdp_db(instance, module) { 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_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 = {}; @@ -38,8 +40,8 @@ function pdp_db(instance, module) { 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]; + 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; } }, @@ -47,8 +49,8 @@ function pdp_db(instance, module) { 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]; + var partner_info; + while (partner_info = pricelist_partnerinfos.pop()) { this.pricelist_partnerinfo_by_id[partner_info.id] = partner_info; } }, @@ -56,8 +58,8 @@ function pdp_db(instance, module) { if (!(supplierinfos instanceof Array)) { supplierinfos = [supplierinfos]; } - for (var i = 0, len = supplierinfos.length; i < len; i++) { - var supplier_info = supplierinfos[i]; + var supplier_info; + while (supplier_info = supplierinfos.pop()) { this.supplierinfo_by_id[supplier_info.id] = supplier_info; } }, @@ -70,8 +72,8 @@ function pdp_db(instance, module) { if (!(pricelists instanceof Array)) { pricelists = [pricelists]; } - for (var i = 0, len = pricelists.length; i < len; i++) { - var pricelist = pricelists[i]; + var pricelist; + while (pricelist = pricelists.pop()) { this.pricelist_by_id[pricelist.id] = pricelist; } }, @@ -79,8 +81,8 @@ function pdp_db(instance, module) { if (!(versions instanceof Array)) { versions = [versions]; } - for (var i = 0, len = versions.length; i < len; i++) { - var version = versions[i]; + var version; + while (version = versions.pop()) { this.pricelist_version_by_id[version.id] = version; } }, @@ -88,8 +90,8 @@ function pdp_db(instance, module) { if (!(items instanceof Array)) { items = [items]; } - for (var i = 0, len = items.length; i < len; i++) { - var item = items[i]; + var item; + while (item = items.pop()) { this.pricelist_item_by_id[item.id] = item; } this.pricelist_item_sorted = this._items_sorted(); @@ -98,33 +100,32 @@ function pdp_db(instance, module) { 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]; + var ptype; + while (ptype = price_types.pop()) { 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); - } + var category; + while (category = categories.pop()) { + this.product_category_by_id[category.id] = category; + this.product_category_children[category.id] = category.child_id } - if (categories.length) { - var cat = categories[0]; - make_ancestors(cat.id, cat.parent_id === false ? [] : [cat.parent_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 () { @@ -153,4 +154,4 @@ function pdp_db(instance, module) { return taxes; } }) -} \ No newline at end of file +} diff --git a/pos_dynamic_price/static/src/js/main.js b/pos_pricelist/static/src/js/main.js similarity index 78% rename from pos_dynamic_price/static/src/js/main.js rename to pos_pricelist/static/src/js/main.js index dcf31eed..143d36e0 100644 --- a/pos_dynamic_price/static/src/js/main.js +++ b/pos_pricelist/static/src/js/main.js @@ -1,5 +1,5 @@ /****************************************************************************** -* Point Of Sale - Dynamic Price for POS Odoo +* Point Of Sale - Pricelist for POS Odoo * Copyright (C) 2014 Taktik (http://www.taktik.be) * @author Adil Houmadi * @@ -15,9 +15,10 @@ * along with this program. If not, see . * ******************************************************************************/ -openerp.pos_dynamic_price = function (instance) { +openerp.pos_pricelist = 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 + pos_pricelist_db(instance, module); + pos_pricelist_models(instance, module); + pos_pricelist_screens(instance, module); + pos_pricelist_widgets(instance, module); +}; diff --git a/pos_dynamic_price/static/src/js/models.js b/pos_pricelist/static/src/js/models.js similarity index 94% rename from pos_dynamic_price/static/src/js/models.js rename to pos_pricelist/static/src/js/models.js index 7f56a8d0..e87b5e73 100644 --- a/pos_dynamic_price/static/src/js/models.js +++ b/pos_pricelist/static/src/js/models.js @@ -1,5 +1,5 @@ /****************************************************************************** -* Point Of Sale - Dynamic Price for POS Odoo +* Point Of Sale - Pricelist for POS Odoo * Copyright (C) 2014 Taktik (http://www.taktik.be) * @author Adil Houmadi * @@ -15,7 +15,7 @@ * along with this program. If not, see . * ******************************************************************************/ -function pdp_models(instance, module) { +function pos_pricelist_models(instance, module) { var _t = instance.web._t; var round_pr = instance.web.round_precision @@ -58,9 +58,6 @@ function pdp_models(instance, module) { * 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 @@ -111,6 +108,7 @@ function pdp_models(instance, module) { 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(); @@ -122,6 +120,12 @@ function pdp_models(instance, module) { } } }, + /** + * @param state + */ + set_manuel_price: function (state) { + this.manuel_price = state; + }, /** * @param quantity */ @@ -235,13 +239,15 @@ function pdp_models(instance, module) { * @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; + 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 @@ -263,7 +269,7 @@ function pdp_models(instance, module) { orderlines = order.get('orderLines').models; } for (var i = 0; i < orderlines.length; i++) { - if (orderlines[i].product.id === product.id) { + if (orderlines[i].product.id === product.id && !orderlines[i].manuel_price) { qty += orderlines[i].quantity; } } @@ -299,7 +305,7 @@ function pdp_models(instance, module) { * @returns {boolean} */ compute_price: function (database, product, partner, qty, pricelist_id) { - debugger; + var self = this; var db = database; @@ -316,12 +322,12 @@ function pdp_models(instance, module) { 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]]); + categ_ids = categ_ids.concat(db.product_category_ancestors[product.categ_id[0]]); } // find items - var items = [], i; - for (var i = 0, len = db.pricelist_item_sorted.length; i < len; i++) { + 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) && @@ -336,7 +342,7 @@ function pdp_models(instance, module) { var price = false; // loop through items - for (var i = 0, len = items.length; i < len; i++) { + for (i = 0, len = items.length; i < len; i++) { var rule = items[i]; if (rule.min_quantity && qty < rule.min_quantity) { @@ -351,7 +357,7 @@ function pdp_models(instance, module) { if (cat_id == rule.categ_id[0]) { break; } - cat_id = db.product_catrgory_by_id[cat_id].parent_id[0]; + cat_id = db.product_category_by_id[cat_id].parent_id[0]; } if (!(cat_id)) { continue; @@ -435,13 +441,13 @@ function pdp_models(instance, module) { function arrange_elements(pos_model) { var product_model = pos_model.find_model('product.product'); - if (Object.size(product_model) == 1) { + 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 (Object.size(res_product_pricelist) == 1) { + if (_.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 @@ -536,11 +542,11 @@ function pdp_models(instance, module) { } var res_partner_model = pos_model.find_model('res.partner'); - if (Object.size(res_partner_model) == 1) { + 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'); } } -} \ No newline at end of file +} diff --git a/pos_dynamic_price/static/src/js/screens.js b/pos_pricelist/static/src/js/screens.js similarity index 95% rename from pos_dynamic_price/static/src/js/screens.js rename to pos_pricelist/static/src/js/screens.js index e2d088d0..b2d674ec 100644 --- a/pos_dynamic_price/static/src/js/screens.js +++ b/pos_pricelist/static/src/js/screens.js @@ -1,5 +1,5 @@ /****************************************************************************** -* Point Of Sale - Dynamic Price for POS Odoo +* Point Of Sale - Pricelist for POS Odoo * Copyright (C) 2014 Taktik (http://www.taktik.be) * @author Adil Houmadi * @@ -15,7 +15,7 @@ * along with this program. If not, see . * ******************************************************************************/ -function pdp_screens(instance, module) { +function pos_pricelist_screens(instance, module) { module.ClientListScreenWidget = module.ClientListScreenWidget.extend({ save_changes: function () { diff --git a/pos_pricelist/static/src/js/widgets.js b/pos_pricelist/static/src/js/widgets.js new file mode 100644 index 00000000..14390ce3 --- /dev/null +++ b/pos_pricelist/static/src/js/widgets.js @@ -0,0 +1,33 @@ +/****************************************************************************** + * Point Of Sale - Pricelist 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 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); + } + } + } + }); +} + diff --git a/pos_pricelist/views/pos_pricelist_template.xml b/pos_pricelist/views/pos_pricelist_template.xml new file mode 100644 index 00000000..eaa794a8 --- /dev/null +++ b/pos_pricelist/views/pos_pricelist_template.xml @@ -0,0 +1,29 @@ + + + + + + + + From 2092500fea8f582911dbda5def6444424239d3b3 Mon Sep 17 00:00:00 2001 From: Adil Houmadi Date: Sun, 21 Dec 2014 00:19:48 +0100 Subject: [PATCH 07/12] [ADD] Some functional testing --- pos_pricelist/__openerp__.py | 5 +- pos_pricelist/demo/pos_pricelist_demo.yml | 95 ++++++++++ pos_pricelist/static/src/css/style.css | 46 +++++ pos_pricelist/static/src/js/db.js | 2 - pos_pricelist/static/src/js/models.js | 67 ++++--- pos_pricelist/static/src/js/tests.js | 164 ++++++++++++++++++ pos_pricelist/test/test.py | 27 +++ .../views/pos_pricelist_template.xml | 6 +- 8 files changed, 382 insertions(+), 30 deletions(-) create mode 100644 pos_pricelist/demo/pos_pricelist_demo.yml create mode 100644 pos_pricelist/static/src/css/style.css create mode 100644 pos_pricelist/static/src/js/tests.js create mode 100644 pos_pricelist/test/test.py diff --git a/pos_pricelist/__openerp__.py b/pos_pricelist/__openerp__.py index d254085b..01d14e64 100644 --- a/pos_pricelist/__openerp__.py +++ b/pos_pricelist/__openerp__.py @@ -24,7 +24,7 @@ 'author': 'Adil Houmadi @Taktik', 'summary': 'Pricelist for Point of sale', 'description': """ -New features for the Point Of Sale: +New feature for the Point Of Sale: ============================================= Add support for pricelist on the point of sale """, @@ -34,6 +34,9 @@ New features for the Point Of Sale: 'data': [ "views/pos_pricelist_template.xml", ], + 'demo': [ + 'demo/pos_pricelist_demo.yml', + ], 'installable': True, 'application': False, 'auto_install': False, diff --git a/pos_pricelist/demo/pos_pricelist_demo.yml b/pos_pricelist/demo/pos_pricelist_demo.yml new file mode 100644 index 00000000..4c2e9888 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/pos_pricelist/static/src/css/style.css b/pos_pricelist/static/src/css/style.css new file mode 100644 index 00000000..5a652bb9 --- /dev/null +++ b/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 +* +* 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 . +* +******************************************************************************/ +.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; +} \ No newline at end of file diff --git a/pos_pricelist/static/src/js/db.js b/pos_pricelist/static/src/js/db.js index 9e5e2715..d00f6ef9 100644 --- a/pos_pricelist/static/src/js/db.js +++ b/pos_pricelist/static/src/js/db.js @@ -17,8 +17,6 @@ ******************************************************************************/ function pos_pricelist_db(instance, module) { - console.log('Loading ...'); - module.PosDB = module.PosDB.extend({ init: function (options) { options = options || {}; diff --git a/pos_pricelist/static/src/js/models.js b/pos_pricelist/static/src/js/models.js index e87b5e73..bf87dae9 100644 --- a/pos_pricelist/static/src/js/models.js +++ b/pos_pricelist/static/src/js/models.js @@ -18,7 +18,7 @@ function pos_pricelist_models(instance, module) { var _t = instance.web._t; - var round_pr = instance.web.round_precision + var round_pr = instance.web.round_precision; /** * @param funcName @@ -60,7 +60,8 @@ function pos_pricelist_models(instance, module) { module.Order = module.Order.extend({ /** * override this method to merge lines - * TODO : find a better way to do it + * 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 */ @@ -101,7 +102,6 @@ function pos_pricelist_models(instance, module) { this.selectLine(this.getLastOrderline()); } }); - /** * Extend the Order line */ @@ -143,6 +143,7 @@ function pos_pricelist_models(instance, module) { /** * override this method to take fiscal postions 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 () { @@ -153,32 +154,39 @@ function pos_pricelist_models(instance, module) { 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 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 = []; + 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_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]); + 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_ids.length === 0) { - product_taxes_ids = taxes_ids; + 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); + } } - 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; - }); + _.each(product_taxes, function (tax) { if (tax.price_include) { var tmp; if (tax.type === "percent") { @@ -202,6 +210,9 @@ function pos_pricelist_models(instance, module) { 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; @@ -214,7 +225,6 @@ function pos_pricelist_models(instance, module) { "taxDetails": taxdetail }; }, - /** * compute price for all price list * @param db @@ -240,8 +250,8 @@ function pos_pricelist_models(instance, module) { */ can_be_merged_with: function (orderline) { var result = this._super('can_be_merged_with', orderline); - if(!result) { - if(!this.manuel_price) { + if (!result) { + if (!this.manuel_price) { return (this.get_product().id === orderline.get_product().id); } else { return false; @@ -422,7 +432,7 @@ function pos_pricelist_models(instance, module) { }); /** - * show error based on pop up + * show error * @param context * @param message * @param comment @@ -449,8 +459,6 @@ function pos_pricelist_models(instance, module) { 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]); - - // after the pricelist we can load all pricelists, versions and items pos_model.models.splice(++pricelist_index, 0, { model: 'account.fiscal.position.tax', @@ -535,6 +543,17 @@ function pos_pricelist_models(instance, module) { 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); } } diff --git a/pos_pricelist/static/src/js/tests.js b/pos_pricelist/static/src/js/tests.js new file mode 100644 index 00000000..ed7b232c --- /dev/null +++ b/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 + * + * 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 () { + '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
" + + "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
" + + "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")' + } + ] + }); + +})(); diff --git a/pos_pricelist/test/test.py b/pos_pricelist/test/test.py new file mode 100644 index 00000000..cec1c319 --- /dev/null +++ b/pos_pricelist/test/test.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# Point Of Sale - Pricelist 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 . +# +############################################################################## +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") diff --git a/pos_pricelist/views/pos_pricelist_template.xml b/pos_pricelist/views/pos_pricelist_template.xml index eaa794a8..e954f1d8 100644 --- a/pos_pricelist/views/pos_pricelist_template.xml +++ b/pos_pricelist/views/pos_pricelist_template.xml @@ -19,9 +19,9 @@ -