diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 13be54850f6..dc773f1b3f6 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -21,6 +21,7 @@ import logging import time +import uuid from openerp import tools from openerp.osv import fields, osv @@ -81,6 +82,7 @@ class pos_config(osv.osv): 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), + 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, help="This sequence is automatically created by Odoo but you can change it "\ "to customize the reference numbers of your orders.", copy=False), @@ -146,6 +148,8 @@ class pos_config(osv.osv): session = record.session_ids[0] result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) return result + def _generate_uuid(self, cr, uid, context=None): + return str(uuid.uuid1()) def _default_sale_journal(self, cr, uid, context=None): company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id @@ -169,6 +173,7 @@ class pos_config(osv.osv): return company_id _defaults = { + 'uuid' : _generate_uuid, 'state' : POS_CONFIG_STATE[0][0], 'journal_id': _default_sale_journal, 'group_by' : True, diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 15a006d57aa..90955b84149 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -13,6 +13,10 @@ function openerp_pos_db(instance, module){ options = options || {}; this.name = options.name || this.name; this.limit = options.limit || this.limit; + + if (options.uuid) { + this.name = this.name + '_' + options.uuid; + } //cache the data in memory to avoid roundtrips to the localstorage this.cache = {}; @@ -39,6 +43,15 @@ function openerp_pos_db(instance, module){ this.packagings_by_product_tmpl_id = {}; this.packagings_by_ean13 = {}; }, + + /* + * sets an uuid to prevent conflict in locally stored data between multiple databases running + * in the same browser at the same origin (Doing this is not advised !) + */ + set_uuid: function(uuid){ + this.name = this.name + '_' + uuid; + }, + /* returns the category object from its id. If you pass a list of id as parameters, you get * a list of category objects. */ @@ -302,7 +315,7 @@ function openerp_pos_db(instance, module){ } var results = []; for(var i = 0; i < this.limit; i++){ - r = re.exec(this.partner_search_string); + var r = re.exec(this.partner_search_string); if(r){ var id = Number(r[1]); results.push(this.get_partner_by_id(id)); @@ -378,6 +391,8 @@ function openerp_pos_db(instance, module){ } return results; }, + + /* paid orders */ add_order: function(order){ var order_id = order.uid; var orders = this.load('orders',[]); @@ -417,5 +432,42 @@ function openerp_pos_db(instance, module){ } return undefined; }, + + /* working orders */ + save_unpaid_order: function(order){ + var order_id = order.uid; + var orders = this.load('unpaid_orders',[]); + var serialized = order.export_as_JSON(); + + for (var i = 0; i < orders.length; i++) { + if (orders[i].id === order_id){ + orders[i].data = serialized; + this.save('unpaid_orders',orders); + return order_id; + } + } + + orders.push({id: order_id, data: serialized}); + this.save('unpaid_orders',orders); + return order_id; + }, + remove_unpaid_order: function(order){ + var orders = this.load('unpaid_orders',[]); + orders = _.filter(orders, function(o){ + return o.id !== order.uid; + }); + this.save('unpaid_orders',orders); + }, + remove_all_unpaid_orders: function(){ + this.save('unpaid_orders',[]); + }, + get_unpaid_orders: function(){ + var saved = this.load('unpaid_orders',[]); + var orders = []; + for (var i = 0; i < saved.length; i++) { + orders.push(saved[i].data); + } + return orders; + }, }); } diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index b4d6037bda8..ffcc8d82112 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -374,10 +374,8 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal scale_read: function(){ var self = this; var ret = new $.Deferred(); - console.log('scale_read'); this.message('scale_read',{}) .then(function(weight){ - console.log(weight) ret.resolve(self.use_debug_weight ? self.debug_weight : weight); }, function(){ //failed to read weight ret.resolve(self.use_debug_weight ? self.debug_weight : {weight:0.0, unit:'Kg', info:'ok'}); diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index c737570cd1e..b04ab9392cc 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -199,11 +199,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; }, loaded: function(self,pos_sessions){ self.pos_session = pos_sessions[0]; - - var orders = self.db.get_orders(); - for (var i = 0; i < orders.length; i++) { - self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1); - } }, },{ model: 'pos.config', @@ -226,9 +221,17 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal 'price': self.config.barcode_price, }); + if (self.config.company_id[0] !== self.user.company_id[0]) { throw new Error(_t("Error: The Point of Sale User must belong to the same company as the Point of Sale. You are probably trying to load the point of sale as an administrator in a multi-company setup, with the administrator account set to the wrong company.")); } + + self.db.set_uuid(self.config.uuid); + + var orders = self.db.get_orders(); + for (var i = 0; i < orders.length; i++) { + self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1); + } }, },{ model: 'stock.location', @@ -472,11 +475,51 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal //creates a new empty order and sets it as the current order add_new_order: function(){ - var order = new module.Order({pos:this}); + var order = new module.Order({},{pos:this}); this.get('orders').add(order); this.set('selectedOrder', order); return order; }, + // load the locally saved unpaid orders for this session. + load_orders: function(){ + var jsons = this.db.get_unpaid_orders(); + var orders = []; + var not_loaded_count = 0; + + for (var i = 0; i < jsons.length; i++) { + var json = jsons[i]; + if (json.pos_session_id === this.pos_session.id) { + orders.push(new module.Order({},{ + pos: this, + json: json, + })); + } else { + not_loaded_count += 1; + } + } + + if (not_loaded_count) { + console.info('There are '+not_loaded_count+' locally saved unpaid orders belonging to another session'); + } + + orders = orders.sort(function(a,b){ + return a.sequence_number - b.sequence_number; + }); + + if (orders.length) { + this.get('orders').add(orders); + } + }, + + set_start_order: function(){ + var orders = this.get('orders').models; + + if (orders.length && !this.get('selectedOrder')) { + this.set('selectedOrder',orders[0]); + } else { + this.add_new_order(); + } + }, // return the current order get_order: function(){ @@ -662,13 +705,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } if(parsed_code.type === 'price'){ - selectedOrder.addProduct(product, {price:parsed_code.value}); + selectedOrder.add_product(product, {price:parsed_code.value}); }else if(parsed_code.type === 'weight'){ - selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false}); + selectedOrder.add_product(product, {quantity:parsed_code.value, merge:false}); }else if(parsed_code.type === 'discount'){ - selectedOrder.addProduct(product, {discount:parsed_code.value, merge:false}); + selectedOrder.add_product(product, {discount:parsed_code.value, merge:false}); }else{ - selectedOrder.addProduct(product); + selectedOrder.add_product(product); } return true; }, @@ -742,7 +785,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // rounded to zero set_quantity: function(quantity){ if(quantity === 'remove'){ - this.order.removeOrderline(this); + this.order.remove_orderline(this); return; }else{ var quant = parseFloat(quantity) || 0; @@ -987,7 +1030,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal module.PaymentlineCollection = Backbone.Collection.extend({ model: module.Paymentline, }); - // An order more or less represents the content of a client's shopping cart (the OrderLines) // plus the associated payment information (the Paymentlines) @@ -997,30 +1039,49 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal initialize: function(attributes,options){ Backbone.Model.prototype.initialize.apply(this, arguments); options = options || {}; - this.pos = attributes.pos; - if (options.json) { - this.init_from_JSON(options.json); - return this; - } - this.sequence_number = this.pos.pos_session.sequence_number++; - this.uid = this.generateUniqueId(); - this.set({ - creationDate: new Date(), - orderLines: new module.OrderlineCollection(), - paymentLines: new module.PaymentlineCollection(), - name: _t("Order ") + this.uid, - client: null, - }); + + this.init_locked = true; + this.pos = options.pos; this.selected_orderline = undefined; this.selected_paymentline = undefined; - this.screen_data = {}; // see ScreenSelector - this.temporary = attributes.temporary || false; - this.to_invoice = false; + this.screen_data = {}; // see ScreenSelector + this.temporary = options.temporary || false; + this.creation_date = new Date(); + this.to_invoice = false; + this.orderlines = new module.OrderlineCollection(); + this.paymentlines = new module.PaymentlineCollection(); + this.pos_session_id = this.pos.pos_session.id; + + this.set({ client: null }); + + if (options.json) { + this.init_from_JSON(options.json); + } else { + this.sequence_number = this.pos.pos_session.sequence_number++; + this.uid = this.generate_unique_id(); + this.name = _t("Order ") + this.uid; + } + + this.on('change', this.save_to_db, this); + this.orderlines.on('change', this.save_to_db, this); + this.paymentlines.on('change', this.save_to_db, this); + + this.init_locked = false; + this.save_to_db(); + return this; }, + save_to_db: function(){ + if (!this.init_locked) { + this.pos.db.save_unpaid_order(this); + } + }, init_from_JSON: function(json) { this.sequence_number = json.sequence_number; + this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number); + this.session_id = json.pos_session_id; this.uid = json.uid; + this.name = _t("Order ") + this.uid; if (json.partner_id) { var client = this.pos.db.get_partner_by_id(json.partner_id); if (!client) { @@ -1029,279 +1090,58 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } else { var client = null; } - this.set({ - creationDate: new Date(), - orderLines: new module.OrderlineCollection(), - paymentLines: new module.PaymentlineCollection(), - name: _t("Order ") + this.uid, - client: client, - }); - this.selected_orderline = undefined; - this.selected_paymentline = undefined; - this.screen_data = {}; + this.set_client(client); + this.temporary = false; // FIXME this.to_invoice = false; // FIXME var orderlines = json.lines; for (var i = 0; i < orderlines.length; i++) { var orderline = orderlines[i][2]; - this.addOrderline(new module.Orderline({}, {pos: this.pos, order: this, json: orderline})); + this.add_orderline(new module.Orderline({}, {pos: this.pos, order: this, json: orderline})); } var paymentlines = json.statement_ids; for (var i = 0; i < paymentlines.length; i++) { var paymentline = paymentlines[i][2]; - this.get('paymentLines').add(new module.Paymentline({},{pos: this.pos, json: paymentline})); + this.paymentlines.add(new module.Paymentline({},{pos: this.pos, json: paymentline})); } }, - is_empty: function(){ - return (this.get('orderLines').models.length === 0); + export_as_JSON: function() { + var orderLines, paymentLines; + orderLines = []; + this.orderlines.each(_.bind( function(item) { + return orderLines.push([0, 0, item.export_as_JSON()]); + }, this)); + paymentLines = []; + this.paymentlines.each(_.bind( function(item) { + return paymentLines.push([0, 0, item.export_as_JSON()]); + }, this)); + return { + name: this.get_name(), + amount_paid: this.get_total_paid(), + amount_total: this.get_total_with_tax(), + amount_tax: this.get_total_tax(), + amount_return: this.get_change(), + lines: orderLines, + statement_ids: paymentLines, + pos_session_id: this.pos_session_id, + partner_id: this.get_client() ? this.get_client().id : false, + user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id, + uid: this.uid, + sequence_number: this.sequence_number, + }; }, - - // Generates a public identification number for the order. - // The generated number must be unique and sequential. They are made 12 digit long - // to fit into EAN-13 barcodes, should it be needed - generateUniqueId: function() { - function zero_pad(num,size){ - var s = ""+num; - while (s.length < size) { - s = "0" + s; - } - return s; - } - return zero_pad(this.pos.pos_session.id,5) +'-'+ - zero_pad(this.pos.pos_session.login_number,3) +'-'+ - zero_pad(this.sequence_number,4); - }, - addOrderline: function(line){ - if(line.order){ - line.order.removeOrderline(line); - } - line.order = this; - this.get('orderLines').add(line); - this.selectLine(this.getLastOrderline()); - }, - 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}); - - 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); - } - - if(options.extras !== undefined){ - for (var prop in options.extras) { - line[prop] = options.extras[prop]; - } - } - - var last_orderline = this.getLastOrderline(); - if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){ - last_orderline.merge(line); - }else{ - this.get('orderLines').add(line); - } - this.selectLine(this.getLastOrderline()); - }, - removeOrderline: function( line ){ - this.get('orderLines').remove(line); - this.selectLine(this.getLastOrderline()); - }, - getOrderline: function(id){ - var orderlines = this.get('orderLines').models; - for(var i = 0; i < orderlines.length; i++){ - if(orderlines[i].id === id){ - return orderlines[i]; - } - } - return null; - }, - getLastOrderline: function(){ - return this.get('orderLines').at(this.get('orderLines').length -1); - }, - addPaymentline: function(cashregister) { - var paymentLines = this.get('paymentLines'); - var newPaymentline = new module.Paymentline({},{cashregister:cashregister, pos:this.pos}); - if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ - newPaymentline.set_amount( Math.max(this.getDueLeft(),0) ); - } - paymentLines.add(newPaymentline); - this.selectPaymentline(newPaymentline); - - }, - removePaymentline: function(line){ - if(this.selected_paymentline === line){ - this.selectPaymentline(undefined); - } - this.get('paymentLines').remove(line); - }, - getName: function() { - return this.get('name'); - }, - getSubtotal : function(){ - return (this.get('orderLines')).reduce((function(sum, orderLine){ - return sum + orderLine.get_display_price(); - }), 0); - }, - getTotalTaxIncluded: function() { - return (this.get('orderLines')).reduce((function(sum, orderLine) { - return sum + orderLine.get_price_with_tax(); - }), 0); - }, - getDiscountTotal: function() { - return (this.get('orderLines')).reduce((function(sum, orderLine) { - return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); - }), 0); - }, - getTotalTaxExcluded: function() { - return (this.get('orderLines')).reduce((function(sum, orderLine) { - return sum + orderLine.get_price_without_tax(); - }), 0); - }, - getTax: function() { - return (this.get('orderLines')).reduce((function(sum, orderLine) { - return sum + orderLine.get_tax(); - }), 0); - }, - getTaxDetails: function(){ - var details = {}; - var fulldetails = []; - var taxes_by_id = {}; - - for(var i = 0; i < this.pos.taxes.length; i++){ - taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i]; - } - - this.get('orderLines').each(function(line){ - var ldetails = line.get_tax_details(); - for(var id in ldetails){ - if(ldetails.hasOwnProperty(id)){ - details[id] = (details[id] || 0) + ldetails[id]; - } - } - }); - - for(var id in details){ - if(details.hasOwnProperty(id)){ - fulldetails.push({amount: details[id], tax: taxes_by_id[id], name: taxes_by_id[id].name}); - } - } - - return fulldetails; - }, - getPaidTotal: function() { - return (this.get('paymentLines')).reduce((function(sum, paymentLine) { - return sum + paymentLine.get_amount(); - }), 0); - }, - getChange: function(paymentline) { - if (!paymentline) { - var change = this.getPaidTotal() - this.getTotalTaxIncluded(); - } else { - var change = -this.getTotalTaxIncluded(); - var lines = this.get('paymentLines').models; - for (var i = 0; i < lines.length; i++) { - change += lines[i].get_amount(); - if (lines[i] === paymentline) { - break; - } - } - } - return round_pr(Math.max(0,change), this.pos.currency.rounding); - }, - getDueLeft: function(paymentline) { - if (!paymentline) { - var due = this.getTotalTaxIncluded() - this.getPaidTotal(); - } else { - var due = this.getTotalTaxIncluded(); - var lines = this.get('paymentLines').models; - for (var i = 0; i < lines.length; i++) { - if (lines[i] === paymentline) { - break; - } else { - due -= lines[i].get_amount(); - } - } - } - return round_pr(Math.max(0,due), this.pos.currency.rounding); - }, - isPaid: function(){ - return this.getDueLeft() === 0; - }, - isPaidWithCash: function(){ - return !!this.get('paymentLines').find( function(pl){ - return pl.cashregister.journal.type === 'cash'; - }); - }, - finalize: function(){ - this.destroy(); - }, - // the client related to the current order. - set_client: function(client){ - this.set('client',client); - }, - get_client: function(){ - return this.get('client'); - }, - get_client_name: function(){ - var client = this.get('client'); - return client ? client.name : ""; - }, - // the order also stores the screen status, as the PoS supports - // different active screens per order. This method is used to - // store the screen status. - set_screen_data: function(key,value){ - if(arguments.length === 2){ - this.screen_data[key] = value; - }else if(arguments.length === 1){ - for(var key in arguments[0]){ - this.screen_data[key] = arguments[0][key]; - } - } - }, - set_to_invoice: function(to_invoice) { - this.to_invoice = to_invoice; - }, - is_to_invoice: function(){ - return this.to_invoice; - }, - // remove all the paymentlines with zero money in it - clean_empty_paymentlines: function() { - var lines = this.get('paymentLines').models; - var empty = []; - for ( var i = 0; i < lines.length; i++) { - if (!lines[i].get_amount()) { - empty.push(lines[i]); - } - } - for ( var i = 0; i < empty.length; i++) { - this.removePaymentline(empty[i]); - } - }, - //see set_screen_data - get_screen_data: function(key){ - return this.screen_data[key]; - }, - // exports a JSON for receipt printing export_for_printing: function(){ var orderlines = []; var self = this; - this.get('orderLines').each(function(orderline){ + this.orderlines.each(function(orderline){ orderlines.push(orderline.export_for_printing()); }); var paymentlines = []; - this.get('paymentLines').each(function(paymentline){ + this.paymentlines.each(function(paymentline){ paymentlines.push(paymentline.export_for_printing()); }); var client = this.get('client'); @@ -1331,15 +1171,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var receipt = { orderlines: orderlines, paymentlines: paymentlines, - subtotal: this.getSubtotal(), - total_with_tax: this.getTotalTaxIncluded(), - total_without_tax: this.getTotalTaxExcluded(), - total_tax: this.getTax(), - total_paid: this.getPaidTotal(), - total_discount: this.getDiscountTotal(), - tax_details: this.getTaxDetails(), - change: this.getChange(), - name : this.getName(), + subtotal: this.get_subtotal(), + total_with_tax: this.get_total_with_tax(), + total_without_tax: this.get_total_without_tax(), + total_tax: this.get_total_tax(), + total_paid: this.get_total_paid(), + total_discount: this.get_total_discount(), + tax_details: this.get_tax_details(), + change: this.get_change(), + name : this.get_name(), client: client ? client.name : null , invoice_id: null, //TODO cashier: cashier ? cashier.name : null, @@ -1386,35 +1226,91 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return receipt; }, - export_as_JSON: function() { - var orderLines, paymentLines; - orderLines = []; - (this.get('orderLines')).each(_.bind( function(item) { - return orderLines.push([0, 0, item.export_as_JSON()]); - }, this)); - paymentLines = []; - (this.get('paymentLines')).each(_.bind( function(item) { - return paymentLines.push([0, 0, item.export_as_JSON()]); - }, this)); - return { - name: this.getName(), - amount_paid: this.getPaidTotal(), - amount_total: this.getTotalTaxIncluded(), - amount_tax: this.getTax(), - amount_return: this.getChange(), - lines: orderLines, - statement_ids: paymentLines, - pos_session_id: this.pos.pos_session.id, - partner_id: this.get_client() ? this.get_client().id : false, - user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id, - uid: this.uid, - sequence_number: this.sequence_number, - }; + is_empty: function(){ + return this.orderlines.models.length === 0; }, - getSelectedLine: function(){ + generate_unique_id: function() { + // Generates a public identification number for the order. + // The generated number must be unique and sequential. They are made 12 digit long + // to fit into EAN-13 barcodes, should it be needed + + function zero_pad(num,size){ + var s = ""+num; + while (s.length < size) { + s = "0" + s; + } + return s; + } + return zero_pad(this.pos.pos_session.id,5) +'-'+ + zero_pad(this.pos.pos_session.login_number,3) +'-'+ + zero_pad(this.sequence_number,4); + }, + get_name: function() { + return this.name; + }, + /* ---- Order Lines --- */ + add_orderline: function(line){ + if(line.order){ + line.order.remove_orderline(line); + } + line.order = this; + this.orderlines.add(line); + this.select_orderline(this.get_last_orderline()); + }, + get_orderline: function(id){ + var orderlines = this.orderlines.models; + for(var i = 0; i < orderlines.length; i++){ + if(orderlines[i].id === id){ + return orderlines[i]; + } + } + return null; + }, + get_orderlines: function(){ + return this.orderlines.models; + }, + get_last_orderline: function(){ + return this.orderlines.at(this.orderlines.length -1); + }, + remove_orderline: function( line ){ + this.orderlines.remove(line); + this.select_orderline(this.get_last_orderline()); + }, + add_product: 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}); + + 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); + } + + if(options.extras !== undefined){ + for (var prop in options.extras) { + line[prop] = options.extras[prop]; + } + } + + var last_orderline = this.get_last_orderline(); + if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){ + last_orderline.merge(line); + }else{ + this.orderlines.add(line); + } + this.select_orderline(this.get_last_orderline()); + }, + get_selected_orderline: function(){ return this.selected_orderline; }, - selectLine: function(line){ + select_orderline: function(line){ if(line){ if(line !== this.selected_orderline){ if(this.selected_orderline){ @@ -1427,13 +1323,44 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.selected_orderline = undefined; } }, - deselectLine: function(){ + deselect_orderline: function(){ if(this.selected_orderline){ this.selected_orderline.set_selected(false); this.selected_orderline = undefined; } }, - selectPaymentline: function(line){ + /* ---- Payment Lines --- */ + add_paymentline: function(cashregister) { + var newPaymentline = new module.Paymentline({},{cashregister:cashregister, pos: this.pos}); + if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ + newPaymentline.set_amount( Math.max(this.get_due(),0) ); + } + this.paymentlines.add(newPaymentline); + this.select_paymentline(newPaymentline); + + }, + get_paymentlines: function(){ + return this.paymentlines.models; + }, + remove_paymentline: function(line){ + if(this.selected_paymentline === line){ + this.select_paymentline(undefined); + } + this.paymentlines.remove(line); + }, + clean_empty_paymentlines: function() { + var lines = this.paymentlines.models; + var empty = []; + for ( var i = 0; i < lines.length; i++) { + if (!lines[i].get_amount()) { + empty.push(lines[i]); + } + } + for ( var i = 0; i < empty.length; i++) { + this.remove_paymentline(empty[i]); + } + }, + select_paymentline: function(line){ if(line !== this.selected_paymentline){ if(this.selected_paymentline){ this.selected_paymentline.set_selected(false); @@ -1445,6 +1372,145 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.trigger('change:selected_paymentline',this.selected_paymentline); } }, + /* ---- Payment Status --- */ + get_subtotal : function(){ + return this.orderlines.reduce((function(sum, orderLine){ + return sum + orderLine.get_display_price(); + }), 0); + }, + get_total_with_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_with_tax(); + }), 0); + }, + get_total_without_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }), 0); + }, + get_total_discount: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); + }), 0); + }, + get_total_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_tax(); + }), 0); + }, + get_total_paid: function() { + return this.paymentlines.reduce((function(sum, paymentLine) { + return sum + paymentLine.get_amount(); + }), 0); + }, + get_tax_details: function(){ + var details = {}; + var fulldetails = []; + var taxes_by_id = {}; + + for(var i = 0; i < this.pos.taxes.length; i++){ + taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i]; + } + + this.orderlines.each(function(line){ + var ldetails = line.get_tax_details(); + for(var id in ldetails){ + if(ldetails.hasOwnProperty(id)){ + details[id] = (details[id] || 0) + ldetails[id]; + } + } + }); + + for(var id in details){ + if(details.hasOwnProperty(id)){ + fulldetails.push({amount: details[id], tax: taxes_by_id[id], name: taxes_by_id[id].name}); + } + } + + return fulldetails; + }, + get_change: function(paymentline) { + if (!paymentline) { + var change = this.get_total_paid() - this.get_total_with_tax(); + } else { + var change = -this.get_total_with_tax(); + var lines = this.paymentlines.models; + for (var i = 0; i < lines.length; i++) { + change += lines[i].get_amount(); + if (lines[i] === paymentline) { + break; + } + } + } + return round_pr(Math.max(0,change), this.pos.currency.rounding); + }, + get_due: function(paymentline) { + if (!paymentline) { + var due = this.get_total_with_tax() - this.get_total_paid(); + } else { + var due = this.get_total_with_tax(); + var lines = this.paymentlines.models; + for (var i = 0; i < lines.length; i++) { + if (lines[i] === paymentline) { + break; + } else { + due -= lines[i].get_amount(); + } + } + } + return round_pr(Math.max(0,due), this.pos.currency.rounding); + }, + is_paid: function(){ + return this.get_due() === 0; + }, + is_paid_with_cash: function(){ + return !!this.paymentlines.find( function(pl){ + return pl.cashregister.journal.type === 'cash'; + }); + }, + finalize: function(){ + this.destroy(); + }, + destroy: function(args){ + Backbone.Model.prototype.destroy.apply(this,arguments); + this.pos.db.remove_unpaid_order(this); + }, + /* ---- Invoice --- */ + set_to_invoice: function(to_invoice) { + this.to_invoice = to_invoice; + }, + is_to_invoice: function(){ + return this.to_invoice; + }, + /* ---- Client / Customer --- */ + // the client related to the current order. + set_client: function(client){ + this.set('client',client); + }, + get_client: function(){ + return this.get('client'); + }, + get_client_name: function(){ + var client = this.get('client'); + return client ? client.name : ""; + }, + /* ---- Screen Status --- */ + // the order also stores the screen status, as the PoS supports + // different active screens per order. This method is used to + // store the screen status. + set_screen_data: function(key,value){ + if(arguments.length === 2){ + this.screen_data[key] = value; + }else if(arguments.length === 1){ + for(var key in arguments[0]){ + this.screen_data[key] = arguments[0][key]; + } + } + }, + //see set_screen_data + get_screen_data: function(key){ + return this.screen_data[key]; + }, }); module.OrderCollection = Backbone.Collection.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index b801d345d6b..1f577b48149 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -207,7 +207,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // what happens when a discount barcode is scanned : the default behavior // is to set the discount on the last order. barcode_discount_action: function(code){ - var last_orderline = this.pos.get_order().getLastOrderline(); + var last_orderline = this.pos.get_order().get_last_orderline(); if(last_orderline){ last_orderline.set_discount(code.value) } @@ -357,6 +357,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ConfirmPopupWidget = module.PopUpWidget.extend({ template: 'ConfirmPopupWidget', show: function(options){ + options = options || {}; var self = this; this._super(); @@ -470,16 +471,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa template: 'ErrorInvoiceTransferPopupWidget', }); - module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({ + module.UnsentOrdersPopupWidget = module.ConfirmPopupWidget.extend({ template: 'UnsentOrdersPopupWidget', - show: function(options){ - var self = this; - this._super(options); - this.renderElement(); - this.$('.button.confirm').click(function(){ - self.pos_widget.screen_selector.close_popup(); - }); - }, + }); + + module.UnpaidOrdersPopupWidget = module.ConfirmPopupWidget.extend({ + template: 'UnpaidOrdersPopupWidget', }); module.ScaleScreenWidget = module.ScreenWidget.extend({ @@ -534,7 +531,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } }, order_product: function(){ - this.pos.get_order().addProduct(this.get_product(),{ quantity: this.weight }); + this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight }); }, get_product_name: function(){ var product = this.get_product(); @@ -591,7 +588,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa if(product.to_weight && self.pos.config.iface_electronic_scale){ self.pos_widget.screen_selector.set_current_screen('scale',{product: product}); }else{ - self.pos.get_order().addProduct(product); + self.pos.get_order().add_product(product); } }, product_list: this.pos.db.get_product_by_category(0) @@ -1068,8 +1065,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa widget:this, order: order, receipt: order.export_for_printing(), - orderlines: order.get('orderLines').models, - paymentlines: order.get('paymentLines').models, + orderlines: order.get_orderlines(), + paymentlines: order.get_paymentlines(), })); }, }); @@ -1188,10 +1185,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa return numpad; }, click_delete_paymentline: function(cid){ - var lines = this.pos.get_order().get('paymentLines').models; + var lines = this.pos.get_order().get_paymentlines(); for ( var i = 0; i < lines.length; i++ ) { if (lines[i].cid === cid) { - this.pos.get_order().removePaymentline(lines[i]); + this.pos.get_order().remove_paymentline(lines[i]); this.reset_input(); this.render_paymentlines(); return; @@ -1199,10 +1196,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } }, click_paymentline: function(cid){ - var lines = this.pos.get_order().get('paymentLines').models; + var lines = this.pos.get_order().get_paymentlines(); for ( var i = 0; i < lines.length; i++ ) { if (lines[i].cid === cid) { - this.pos.get_order().selectPaymentline(lines[i]); + this.pos.get_order().select_paymentline(lines[i]); this.reset_input(); this.render_paymentlines(); return; @@ -1216,7 +1213,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa return; } - var lines = order.get('paymentLines').models; + var lines = order.get_paymentlines(); this.$('.paymentlines-container').empty(); var lines = $(QWeb.render('PaymentScreen-Paymentlines', { @@ -1243,7 +1240,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa break; } } - this.pos.get_order().addPaymentline( cashregister ); + this.pos.get_order().add_paymentline( cashregister ); this.reset_input(); this.render_paymentlines(); }, @@ -1332,7 +1329,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var order = this.pos.get_order(); if (!order) { return; - } else if (order.isPaid()) { + } else if (order.is_paid()) { self.$('.next').addClass('highlight'); }else{ self.$('.next').removeClass('highlight'); @@ -1358,7 +1355,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // FIXME: this check is there because the backend is unable to // process empty orders. This is not the right place to fix it. - if(order.get_orderlines().length === 0){ + if (order.get_orderlines().length === 0) { this.pos_widget.screen_selector.show_popup('error',{ 'message': _t('Empty Order'), 'comment': _t('There must be at least one product in your order before it can be validated'), @@ -1366,12 +1363,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa return; } - if (!order.isPaid() || this.invoicing) { + if (!order.is_paid() || this.invoicing) { return; } // The exact amount must be paid if there is no cash payment method defined. - if (Math.abs(order.getTotalTaxIncluded() - order.getPaidTotal()) > 0.00001) { + if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) { var cash = false; for (var i = 0; i < this.pos.cashregisters.length; i++) { cash = cash || (this.pos.cashregisters[i].journal.type === 'cash'); @@ -1385,7 +1382,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } } - if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) { + if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) { + this.pos.proxy.open_cashbox(); } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 288eda20a31..14a607b8fc3 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -111,7 +111,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(!self.editable){ return; } - self.pos.get_order().selectLine(this.orderline); + self.pos.get_order().select_orderline(this.orderline); self.pos_widget.numpad.state.reset(); }; this.client_change_handler = function(event){ @@ -144,20 +144,20 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.enable_numpad(); }else{ this.disable_numpad(); - order.deselectLine(); + order.deselect_orderline(); } } }, set_value: function(val) { var order = this.pos.get_order(); - if (this.editable && order.getSelectedLine()) { + if (this.editable && order.get_selected_orderline()) { var mode = this.numpad_state.get('mode'); if( mode === 'quantity'){ - order.getSelectedLine().set_quantity(val); + order.get_selected_orderline().set_quantity(val); }else if( mode === 'discount'){ - order.getSelectedLine().set_discount(val); + order.get_selected_orderline().set_discount(val); }else if( mode === 'price'){ - order.getSelectedLine().set_unit_price(val); + order.get_selected_orderline().set_unit_price(val); } } }, @@ -167,27 +167,32 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.renderElement(); } }, + orderline_add: function(){ + this.numpad_state.reset(); + this.renderElement('and_scroll_to_bottom'); + }, + orderline_remove: function(line){ + this.remove_orderline(line); + this.numpad_state.reset(); + this.update_summary(); + }, + orderline_change: function(line){ + this.rerender_orderline(line); + this.update_summary(); + }, bind_order_events: function() { - var order = this.pos.get_order(); order.unbind('change:client', this.client_change_handler); order.bind('change:client', this.client_change_handler); - var lines = order.get('orderLines'); - lines.unbind(); - lines.bind('add', function(){ - this.numpad_state.reset(); - this.renderElement(true); - },this); - lines.bind('remove', function(line){ - this.remove_orderline(line); - this.numpad_state.reset(); - this.update_summary(); - },this); - lines.bind('change', function(line){ - this.rerender_orderline(line); - this.update_summary(); - },this); + var lines = order.orderlines; + lines.unbind('add', this.orderline_add, this); + lines.bind('add', this.orderline_add, this); + lines.unbind('remove', this.orderline_remove, this); + lines.bind('remove', this.orderline_remove, this); + lines.unbind('change', this.orderline_change, this); + lines.bind('change', this.orderline_change, this); + }, render_orderline: function(orderline){ var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline}); @@ -201,7 +206,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa return el_node; }, remove_orderline: function(order_line){ - if(this.pos.get_order().get('orderLines').length === 0){ + if(this.pos.get_order().get_orderlines().length === 0){ this.renderElement(); }else{ order_line.node.parentNode.removeChild(order_line.node); @@ -225,7 +230,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if (!order) { return; } - var orderlines = order.get('orderLines').models; + var orderlines = order.get_orderlines(); var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); @@ -252,8 +257,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, update_summary: function(){ var order = this.pos.get_order(); - var total = order ? order.getTotalTaxIncluded() : 0; - var taxes = order ? total - order.getTotalTaxExcluded() : 0; + var total = order ? order.get_total_with_tax() : 0; + var taxes = order ? total - order.get_total_without_tax() : 0; this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total); this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes); @@ -481,7 +486,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(query){ var products = this.pos.db.search_product_in_category(category.id,query) if(buy_result && products.length === 1){ - this.pos.get_order().addProduct(products[0]); + this.pos.get_order().add_product(products[0]); this.clear_search(); }else{ this.product_list_widget.set_product_list(products); @@ -733,6 +738,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); }); + this.$('.button.show_unpaid_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('unpaid-orders'); + }); + this.$('.button.delete_unpaid_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('confirm',{ + message: _t('Delete Unpaid Orders ?'), + comment: _t('This operation will permanently destroy all unpaid orders from all sessions that have been put in the local storage. You will lose all the data and exit the point of sale. This operation cannot be undone.'), + confirm: function(){ + self.pos.db.remove_all_unpaid_orders(); + window.location = '/'; + }, + }); + }); _.each(this.eans, function(ean, name){ self.$('.button.'+name).click(function(){ self.$('input.ean').val(ean); @@ -889,7 +907,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.renderElement(); - self.pos.add_new_order(); + self.pos.load_orders(); + self.pos.set_start_order(); self.build_widgets(); @@ -1019,6 +1038,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{}); this.unsent_orders_popup.appendTo(this.$el); + this.unpaid_orders_popup = new module.UnpaidOrdersPopupWidget(this,{}); + this.unpaid_orders_popup.appendTo(this.$el); + // -------- Misc --------- this.order_selector = new module.OrderSelectorWidget(this,{}); @@ -1091,6 +1113,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'fullscreen': this.fullscreen_popup, 'selection': this.selection_popup, 'unsent-orders': this.unsent_orders_popup, + 'unpaid-orders': this.unpaid_orders_popup, }, default_screen: 'products', default_mode: 'cashier', @@ -1147,7 +1170,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } var draft_order = _.find( self.pos.get('orders').models, function(order){ - return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0; + return order.get_orderlines().length !== 0 && order.get_paymentlines().length === 0; }); if(draft_order){ if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) { diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 185cd6effb7..8987c51bbbd 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -454,7 +454,7 @@
- +
Please select a payment method. @@ -484,17 +484,17 @@ - + - + - + - + @@ -504,11 +504,11 @@ - + - - + + @@ -982,6 +982,29 @@
+ + + +