diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index ba00cf57094..46e4438a12a 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -394,7 +394,7 @@ class pos_session(osv.osv): for obj in self.browse(cr, uid, ids, context=context): for statement in obj.statement_ids: statement.unlink(context=context) - return True + return super(pos_session, self).unlink(cr, uid, ids, context=context) def open_cb(self, cr, uid, ids, context=None): @@ -553,6 +553,7 @@ class pos_order(osv.osv): orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] order_ids = [] + for tmp_order in orders_to_save: to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 51fa0738076..520c22db9c7 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -679,13 +679,33 @@ - - - - - - - + +

+ Barcode Patterns allow to match barcodes to actions or to embed information such as price and quantity in the barcode. + Barcode Patterns only work with EAN13 barcodes. +

+

+ Each type of barcode accepts a list of patterns seprated by commas. A scanned + barcode will be attributed to a type if it matches one of its patterns. + The patterns take the form of EAN13 barcodes. Numbers in the pattern must match + the number in the scanned barcode. A 'x' or a '*' in a pattern will match + any one number. If the patterns are shorter than EAN13 barcodes, they are assumed + to be prefixes and match at the beginning. Weight, Price and Discount patterns also + tell how the weight, price or discount is encoded in the barcode. 'N' indicate the + positions where the integer part is en encoded, and 'D' where the decimals are encoded. + If multiple pattern match one barcode, the longest pattern with the less 'x' or '*' is + considered the matching one. If a barcode matches no pattern it will not be found in + the POS. +

+ + + + + + + + +
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 30939636b23..0c44d3db3f0 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1665,6 +1665,16 @@ td { font-size: 18px; margin: 0px 16px; } +.pos .popup .comment.traceback { + height: 264px; + overflow: auto; + font-size: 14px; + text-align: left; + font-family: 'Inconsolata'; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} .pos .popup .footer{ position:absolute; bottom:0; diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 38230fb0ca2..08d4f879968 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -345,6 +345,9 @@ function openerp_pos_db(instance, module){ }); this.save('orders',orders); }, + remove_all_orders: function(){ + this.save('orders',[]); + }, get_orders: function(){ return this.load('orders',[]); }, diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 90fa3567cda..373a5f66761 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -19,8 +19,6 @@ openerp.point_of_sale = function(instance) { openerp_pos_widgets(instance,module); // import pos_widgets.js - openerp_pos_tests(instance,module); // import pos_tests.js - instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget'); }; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 92276db60de..d351fee5ff1 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -114,180 +114,250 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return done; }, - // helper function to load data from the server + // helper function to load data from the server. Obsolete use the models loader below. fetch: function(model, fields, domain, ctx){ this._load_progress = (this._load_progress || 0) + 0.05; this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress); return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() }, + // Server side model loaders. This is the list of the models that need to be loaded from + // the server. The models are loaded one by one by this list's order. The 'loaded' callback + // is used to store the data in the appropriate place once it has been loaded. This callback + // can return a deferred that will pause the loading of the next module. + // a shared temporary dictionary is available for loaders to communicate private variables + // used during loading such as object ids, etc. + models: [ + { + model: 'res.users', + fields: ['name','company_id'], + domain: function(self){ return [['id','=',self.session.uid]]; }, + loaded: function(self,users){ self.user = users[0]; }, + },{ + model: 'res.company', + fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' ], + domain: function(self){ return [['id','=',self.user.company_id[0]]]; }, + loaded: function(self,companies){ self.company = companies[0]; }, + },{ + model: 'product.uom', + fields: [], + domain: null, + loaded: function(self,units){ + self.units = units; + var units_by_id = {}; + for(var i = 0, len = units.length; i < len; i++){ + units_by_id[units[i].id] = units[i]; + units[i].groupable = ( units[i].category_id[0] === 1 ); + units[i].is_unit = ( units[i].id === 1 ); + } + self.units_by_id = units_by_id; + } + },{ + model: 'res.users', + fields: ['name','ean13'], + domain: null, + loaded: function(self,users){ self.users = users; }, + },{ + model: 'res.partner', + fields: ['name','street','city','country_id','phone','zip','mobile','email','ean13'], + domain: null, + loaded: function(self,partners){ + self.partners = partners; + self.db.add_partners(partners); + }, + },{ + model: 'account.tax', + fields: ['name','amount', 'price_include', 'type'], + domain: null, + loaded: function(self,taxes){ self.taxes = taxes; }, + },{ + model: 'pos.session', + fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'], + domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; }, + loaded: function(self,pos_sessions){ self.pos_session = pos_sessions[0]; }, + },{ + model: 'pos.config', + fields: [], + domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; }, + loaded: function(self,configs){ + self.config = configs[0]; + self.config.use_proxy = self.config.iface_payment_terminal || + self.config.iface_electronic_scale || + self.config.iface_print_via_proxy || + self.config.iface_scan_via_proxy || + self.config.iface_cashdrawer; + + self.barcode_reader.add_barcode_patterns({ + 'product': self.config.barcode_product, + 'cashier': self.config.barcode_cashier, + 'client': self.config.barcode_customer, + 'weight': self.config.barcode_weight, + 'discount': self.config.barcode_discount, + 'price': self.config.barcode_price, + }); + }, + },{ + model: 'stock.location', + fields: [], + domain: function(self){ return [['id','=', self.config.stock_location_id[0]]]; }, + loaded: function(self, locations){ self.shop = locations[0]; }, + },{ + model: 'product.pricelist', + fields: ['currency_id'], + domain: function(self){ return [['id','=',self.config.pricelist_id[0]]]; }, + loaded: function(self, pricelists){ self.pricelist = pricelists[0]; }, + },{ + model: 'res.currency', + fields: ['symbol','position','rounding','accuracy'], + domain: function(self){ return [['id','=',self.pricelist.currency_id[0]]]; }, + loaded: function(self, currencies){ + self.currency = currencies[0]; + }, + },{ + model: 'product.packaging', + fields: ['ean','product_tmpl_id'], + domain: null, + loaded: function(self, packagings){ + self.db.add_packagings(packagings); + }, + },{ + model: 'pos.category', + fields: ['id','name','parent_id','child_id','image'], + domain: null, + loaded: function(self, categories){ + self.db.add_categories(categories); + }, + },{ + model: 'product.product', + fields: ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants', + 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', + 'product_tmpl_id'], + domain: function(self){ return [['sale_ok','=',true],['available_in_pos','=',true]]; }, + context: function(self){ return { pricelist: self.pricelist.id }; }, + loaded: function(self, products){ + self.db.add_products(products); + }, + },{ + model: 'account.bank.statement', + fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], + domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; }, + loaded: function(self, bankstatements, tmp){ + self.bankstatements = bankstatements; + + tmp.journals = []; + _.each(bankstatements,function(statement){ + tmp.journals.push(statement.journal_id[0]); + }); + }, + },{ + model: 'account.journal', + fields: [], + domain: function(self,tmp){ return [['id','in',tmp.journals]]; }, + loaded: function(self, journals){ + self.journals = journals; + + // associate the bank statements with their journals. + var bankstatements = self.bankstatements; + for(var i = 0, ilen = bankstatements.length; i < ilen; i++){ + for(var j = 0, jlen = journals.length; j < jlen; j++){ + if(bankstatements[i].journal_id[0] === journals[j].id){ + bankstatements[i].journal = journals[j]; + bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; + } + } + } + self.cashregisters = bankstatements; + }, + },{ + loaded: function(self){ + self.company_logo = new Image(); + self.company_logo.crossOrigin = 'anonymous'; + var logo_loaded = new $.Deferred(); + self.company_logo.onload = function(){ + var img = self.company_logo; + var ratio = 1; + var targetwidth = 300; + var maxheight = 150; + if( img.width !== targetwidth ){ + ratio = targetwidth / img.width; + } + if( img.height * ratio > maxheight ){ + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + var c = document.createElement('canvas'); + c.width = width; + c.height = height + var ctx = c.getContext('2d'); + ctx.drawImage(self.company_logo,0,0, width, height); + + self.company_logo_base64 = c.toDataURL(); + window.logo64 = self.company_logo_base64; + logo_loaded.resolve(); + }; + self.company_logo.onerror = function(){ + logo_loaded.reject(); + }; + self.company_logo.src = window.location.origin + '/web/binary/company_logo'; + + return logo_loaded; + }, + }, + ], + // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. load_server_data: function(){ var self = this; + var loaded = new $.Deferred(); + var progress = 0; + var progress_step = 1.0 / self.models.length; + var tmp = {}; // this is used to share a temporary state between models loaders - var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) - .then(function(users){ - self.user = users[0]; - - return self.fetch('res.company', - [ - 'currency_id', - 'email', - 'website', - 'company_registry', - 'vat', - 'name', - 'phone', - 'partner_id', - ], - [['id','=',users[0].company_id[0]]], - {show_address_only: true}); - }).then(function(companies){ - self.company = companies[0]; - - return self.fetch('product.uom', null, null); - }).then(function(units){ - self.units = units; - var units_by_id = {}; - for(var i = 0, len = units.length; i < len; i++){ - units_by_id[units[i].id] = units[i]; - units[i].groupable = ( units[i].category_id[0] === 1 ); - units[i].is_unit = ( units[i].id === 1 ); - } - self.units_by_id = units_by_id; + function load_model(index){ + if(index >= self.models.length){ + loaded.resolve(); + }else{ + var model = self.models[index]; + self.pos_widget.loading_message(_t('Loading')+' '+(model.model || ''), progress); + var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; + var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; + var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context; + progress += progress_step; - return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); - }).then(function(users){ - self.users = users; - - return self.fetch('res.partner', ['name','street','city','country_id','phone','zip','mobile','email','ean13']); - }).then(function(partners){ - self.partners = partners; - self.db.add_partners(partners); - - return self.fetch('account.tax', ['name','amount', 'price_include', 'type']); - }).then(function(taxes){ - self.taxes = taxes; - - return self.fetch( - 'pos.session', - ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'], - [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] - ); - }).then(function(pos_sessions){ - self.pos_session = pos_sessions[0]; - - return self.fetch('pos.config',[],[['id','=', self.pos_session.config_id[0]]]); - }).then(function(configs){ - self.config = configs[0]; - self.config.use_proxy = self.config.iface_payment_terminal || - self.config.iface_electronic_scale || - self.config.iface_print_via_proxy || - self.config.iface_scan_via_proxy || - self.config.iface_cashdrawer; - - self.barcode_reader.add_barcode_patterns({ - 'product': self.config.barcode_product, - 'cashier': self.config.barcode_cashier, - 'client': self.config.barcode_customer, - 'weight': self.config.barcode_weight, - 'discount': self.config.barcode_discount, - 'price': self.config.barcode_price, - }); - return self.fetch('stock.location',[],[['id','=', self.config.stock_location_id[0]]]); - }).then(function(shops){ - self.shop = shops[0]; - - return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]); - }).then(function(pricelists){ - self.pricelist = pricelists[0]; - - return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]); - }).then(function(currencies){ - self.currency = currencies[0]; - - return self.fetch('product.packaging',['ean','product_tmpl_id']); - }).then(function(packagings){ - self.db.add_packagings(packagings); - - return self.fetch('pos.category', ['id','name','parent_id','child_id','image']); - }).then(function(categories){ - self.db.add_categories(categories); - - return self.fetch( - 'product.product', - ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants', - 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', - 'product_tmpl_id'], - [['sale_ok','=',true],['available_in_pos','=',true]], - {pricelist: self.pricelist.id} // context for price - ); - }).then(function(products){ - self.db.add_products(products); - - return self.fetch( - 'account.bank.statement', - ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], - [['state','=','open'],['pos_session_id', '=', self.pos_session.id]] - ); - }).then(function(bankstatements){ - var journals = []; - _.each(bankstatements,function(statement) { - journals.push(statement.journal_id[0]); - }); - self.bankstatements = bankstatements; - return self.fetch('account.journal', undefined, [['id','in', journals]]); - }).then(function(journals){ - self.journals = journals; - - // associate the bank statements with their journals. - var bankstatements = self.bankstatements; - for(var i = 0, ilen = bankstatements.length; i < ilen; i++){ - for(var j = 0, jlen = journals.length; j < jlen; j++){ - if(bankstatements[i].journal_id[0] === journals[j].id){ - bankstatements[i].journal = journals[j]; - bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; - } + if( model.model ){ + new instance.web.Model(model.model).query(fields).filter(domain).context(context).all() + .then(function(result){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,result,tmp)) + .then(function(){ load_model(index + 1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + loaded.reject(err); + } + },function(err){ + loaded.reject(err); + }); + }else if( model.loaded ){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,tmp)) + .then( function(){ load_model(index +1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + loaded.reject(err); } + }else{ + load_model(index + 1); } - self.cashregisters = bankstatements; + } + } - // Load the company Logo + try{ + load_model(0); + }catch(err){ + loaded.reject(err); + } - self.company_logo = new Image(); - self.company_logo.crossOrigin = 'anonymous'; - var logo_loaded = new $.Deferred(); - self.company_logo.onload = function(){ - var img = self.company_logo; - var ratio = 1; - var targetwidth = 300; - var maxheight = 150; - if( img.width !== targetwidth ){ - ratio = targetwidth / img.width; - } - if( img.height * ratio > maxheight ){ - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - var c = document.createElement('canvas'); - c.width = width; - c.height = height - var ctx = c.getContext('2d'); - ctx.drawImage(self.company_logo,0,0, width, height); - - self.company_logo_base64 = c.toDataURL(); - window.logo64 = self.company_logo_base64; - logo_loaded.resolve(); - }; - self.company_logo.onerror = function(){ - logo_loaded.reject(); - }; - self.company_logo.src = window.location.origin + '/web/binary/company_logo'; - - return logo_loaded; - }); - return loaded; }, @@ -324,20 +394,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. push_order: function(order) { var self = this; - this.proxy.log('push_order',order.export_as_JSON()); - var order_id = this.db.add_order(order.export_as_JSON()); + + if(order){ + this.proxy.log('push_order',order.export_as_JSON()); + this.db.add_order(order.export_as_JSON()); + } + var pushed = new $.Deferred(); - this.set('synch',{state:'connecting', pending:self.db.get_orders().length}); - this.flush_mutex.exec(function(){ - var flushed = self._flush_all_orders(); + var flushed = self._flush_orders(self.db.get_orders()); - flushed.always(function(){ + flushed.always(function(ids){ pushed.resolve(); }); - - return flushed; }); return pushed; }, @@ -361,8 +431,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var order_id = this.db.add_order(order.export_as_JSON()); - this.set('synch',{state:'connecting', pending:self.db.get_orders().length}); - this.flush_mutex.exec(function(){ var done = new $.Deferred(); // holds the mutex @@ -373,7 +441,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // things will happen as a duplicate will be sent next time // so we must make sure the server detects and ignores duplicated orders - var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true}); + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); transfer.fail(function(){ invoiced.reject('error-transfer'); @@ -382,10 +450,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // on success, get the order id generated by the server transfer.pipe(function(order_server_id){ + // generate the pdf and download it self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{ active_ids:order_server_id, }}); + invoiced.resolve(); done.resolve(); }); @@ -397,62 +467,33 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return invoiced; }, - // attemps to send all pending orders ( stored in the pos_db ) to the server, - // and remove the successfully sent ones from the db once - // it has been confirmed that they have been sent correctly. - flush: function() { + // wrapper around the _save_to_server that updates the synch status widget + _flush_orders: function(orders, options) { var self = this; - var flushed = new $.Deferred(); - this.flush_mutex.exec(function(){ - var done = new $.Deferred(); + this.set('synch',{ state: 'connecting', pending: orders.length}); - self._flush_all_orders() - .done( function(){ flushed.resolve();}) - .fail( function(){ flushed.reject(); }) - .always(function(){ done.resolve(); }); - - return done; - }); - - return flushed; - }, - - // attempts to send the locally stored order of id 'order_id' - // the sending is asynchronous and can take some time to decide if it is successful or not - // it is therefore important to only call this method from inside a mutex - // this method returns a deferred indicating wether the sending was successful or not - // there is a timeout parameter which is set to 2 seconds by default. - _flush_order: function( order_id, options) { - return this._flush_all_orders([this.db.get_order(order_id)], options); - }, - - // attempts to send all the locally stored orders. As with _flush_order, it should only be - // called from within a mutex. - // this method returns a deferred that always succeeds when all orders have been tried to be sent, - // even if none of them could actually be sent. - _flush_all_orders: function () { - var self = this; - self.set('synch', { - state: 'connecting', - pending: self.get('synch').pending - }); - return self._save_to_server(self.db.get_orders()).done(function () { + return self._save_to_server(orders, options).done(function (server_ids) { var pending = self.db.get_orders().length; + self.set('synch', { state: pending ? 'connecting' : 'connected', pending: pending }); + + return server_ids; }); }, // send an array of orders to the server // available options: // - timeout: timeout for the rpc call in ms + // returns a deferred that resolves with the list of + // server generated ids for the sent orders _save_to_server: function (orders, options) { if (!orders || !orders.length) { var result = $.Deferred(); - result.resolve(); + result.resolve([]); return result; } @@ -474,11 +515,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal shadow: !options.to_invoice, timeout: timeout } - ).then(function () { + ).then(function (server_ids) { _.each(orders, function (order) { self.db.remove_order(order.id); }); - }).fail(function (unused, event){ + return server_ids; + }).fail(function (error, event){ + if(error.code === 200 ){ // Business Logic Error, not a connection problem + self.pos_widget.screen_selector.show_popup('error-traceback',{ + message: error.data.message, + comment: error.data.debug + }); + } // prevent an error popup creation by the rpc failure // we want the failure to be silent as we send the orders in the background event.preventDefault(); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2f2f5e57312..169940e5061 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -73,8 +73,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa load_saved_screen: function(){ this.close_popup(); var selectedOrder = this.pos.get('selectedOrder'); - // this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh'); - this.set_current_screen(this.default_screen,null,'refresh'); + // FIXME : this changing screen behaviour is sometimes confusing ... + this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh'); + //this.set_current_screen(this.default_screen,null,'refresh'); }, set_user_mode: function(user_mode){ @@ -348,6 +349,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var self = this; this._super(); + $('body').append(''); + if( text && (text.message || text.comment) ){ this.$('.message').text(text.message); this.$('.comment').text(text.comment); @@ -365,6 +368,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, }); + module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({ + template:'ErrorTracebackPopupWidget', + }); module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({ template:'ErrorSessionPopupWidget', @@ -375,6 +381,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show: function(barcode){ this._super(); this.$('.barcode').text(barcode); + }, }); @@ -412,6 +419,18 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa template: 'ErrorInvoiceTransferPopupWidget', }); + module.UnsentOrdersPopupWidget = module.PopUpWidget.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.ScaleScreenWidget = module.ScreenWidget.extend({ template:'ScaleScreenWidget', @@ -557,6 +576,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ClientListScreenWidget = module.ScreenWidget.extend({ template: 'ClientListScreenWidget', + init: function(parent, options){ + this._super(parent, options); + this.partner_cache = new module.DomCache(); + }, + show_leftpane: false, auto_back: true, @@ -587,7 +611,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } this.$('.client-list-contents').delegate('.client-line','click',function(event){ - console.log('uh'); self.line_select(event,$(this),parseInt($(this).data('id'))); }); @@ -609,6 +632,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa perform_search: function(query, associate_result){ if(query){ var customers = this.pos.db.search_partner(query); + console.log(customers); this.display_client_details('hide'); if ( associate_result && customers.length === 1){ this.new_client = customers[0]; @@ -628,14 +652,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$('.searchbox input').focus(); }, render_list: function(partners){ - var contents = this.$('.client-list-contents'); - contents.empty(); - for(var i = 0, len = partners.length; i < len; i++){ - var clientline = $(QWeb.render('ClientLine',{partner:partners[i]})); - if( partners[i] === this.new_client ){ - clientline.addClass('highlight'); + var contents = this.$el[0].querySelector('.client-list-contents'); + contents.innerHTML = ""; + for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){ + var partner = partners[i]; + var clientline = this.partner_cache.get_node(partner.id); + if(!clientline){ + var clientline_html = QWeb.render('ClientLine',{partner:partners[i]}); + var clientline = document.createElement('tbody'); + clientline.innerHTML = clientline_html; + clientline = clientline.childNodes[1]; + this.partner_cache.cache_node(partner.id,clientline); } - contents.append(clientline); + if( partners === this.new_client ){ + clientline.classList.add('highlight'); + }else{ + clientline.classList.remove('highlight'); + } + contents.appendChild(clientline); } }, save_changes: function(){ diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 018b343de58..08aa6e87213 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -1,93 +1,89 @@ -function openerp_pos_tests(instance, module){ //module is instance.point_of_sale +(function() { + 'use strict'; - // Various UI Tests to measure performance and memory leaks. - module.UiTester = function(){ - var running = false; - var queue = new module.JobQueue(); + openerp.Tour.register({ + id: 'pos_basic_order', + name: 'Complete a basic order trough the Front-End', + path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening', + mode: 'test', + steps: [ + { + title: 'Wait fot the bloody screen to be ready', + wait: 200, + }, + { + title: 'Load the Session', + waitNot: '.oe_loading:visible', + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + { + title: 'Loading the Loading Screen', + waitFor: '.loader' + }, + { + title: 'Waiting for the end of loading...', + waitFor: '.loader:hidden', + }, + { + title: 'Loading The Point of Sale', + waitFor: '.pos', + }, + { + title: 'On va manger des CHIPS!', + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The chips have been added to the Order', + waitFor: '.order .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The order total has been updated to the correct value', + wait: 2000, + waitFor: '.order .total .value:contains("1.48 €")', + }, + { + title: "Let's buy more chips", + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: "Let's veryify we pay the correct price for two bags of chips", + waitFor: '.order .total .value:contains("2.96 €")', + }, + { + title: "Let's pay with a debit card", + element: ".paypad-button:contains('Bank')", + }, + { + title: "Let's accept the payment", + onload: function(){ + // The test cannot validate or cancel the print() ... so we replace it by a noop !. + window._print = window.print; + window.print = function(){ console.log('Print!') }; + }, + element: ".button .iconlabel:contains('Validate'):visible", + }, + { + title: "Let's finish the order", + element: ".button:not(.disabled) .iconlabel:contains('Next'):visible", + }, + { + 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", + }, + { + title: "Let's close the Point of Sale", + element: ".header-button:contains('Close')", + }, + { + title: "Wait for the backend to ready itself", + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + ], + }); - // stop the currently running test - this.stop = function(){ - queue.clear(); - }; +})(); - // randomly switch product categories - this.category_switch = function(interval){ - queue.schedule(function(){ - var breadcrumbs = $('.breadcrumb-button'); - var categories = $('.category-button'); - if(categories.length > 0){ - var rnd = Math.floor(Math.random()*categories.length); - categories.eq(rnd).click(); - }else{ - var rnd = Math.floor(Math.random()*breadcrumbs.length); - breadcrumbs.eq(rnd).click(); - } - },{repeat:true, duration:interval}); - }; - - // randomly order products then resets the order - this.order_products = function(interval){ - - queue.schedule(function(){ - var def = new $.Deferred(); - var order_queue = new module.JobQueue(); - var order_size = 1 + Math.floor(Math.random()*10); - - while(order_size--){ - order_queue.schedule(function(){ - var products = $('.product'); - if(products.length > 0){ - var rnd = Math.floor(Math.random()*products.length); - products.eq(rnd).click(); - } - },{duration:20}); - } - order_queue.finished().then(function(){ - $('.deleteorder-button').click(); - def.resolve(); - }); - return def; - },{repeat:true, duration: interval}); - - }; - - // makes a complete product order cycle ( print via proxy must be activated, and scale deactivated ) - this.full_order_cycle = function(interval){ - queue.schedule(function(){ - var def = new $.Deferred(); - var order_queue = new module.JobQueue(); - var order_size = 1 + Math.floor(Math.random()*50); - - while(order_size--){ - order_queue.schedule(function(){ - var products = $('.product'); - if(products.length > 0){ - var rnd = Math.floor(Math.random()*products.length); - products.eq(rnd).click(); - } - },{duration:50}); - } - order_queue.schedule(function(){ - $('.paypad-button:first').click(); - },{duration:250}); - order_queue.schedule(function(){ - $('.paymentline-input:first').val(10000); - $('.paymentline-input:first').keydown(); - $('.paymentline-input:first').keyup(); - },{duration:250}); - order_queue.schedule(function(){ - $('.pos-actionbar-button-list .button:eq(2)').click(); - },{duration:250}); - order_queue.schedule(function(){ - def.resolve(); - }); - return def; - },{repeat: true, duration: interval}); - }; - }; - - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ - window.pos_test_ui = new module.UiTester(); - } - -} diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 5db12165737..0f846b9d924 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -797,6 +797,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.$('.button.reference').click(function(){ self.pos.barcode_reader.scan(self.$('input.ean').val()); }); + this.$('.button.show_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('unsent-orders'); + }); + this.$('.button.delete_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('confirm',{ + message: _t('Delete Unsent Orders ?'), + comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'), + confirm: function(){ + self.pos.db.remove_all_orders(); + self.pos.set({synch: { state:'connected', pending: 0 }}); + }, + }); + }); _.each(this.eans, function(ean, name){ self.$('.button.'+name).click(function(){ self.$('input.ean').val(ean); @@ -840,7 +853,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.set_status(synch.state, synch.pending); }); this.$el.click(function(){ - self.pos.flush(); + self.pos.push_order(); }); }, }); @@ -1005,19 +1018,42 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');}); - self.pos.flush(); + self.pos.push_order(); - }).fail(function(){ // error when loading models data from the backend - return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id']) - .pipe( _.bind(function(res){ - return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']}) - .pipe(_.bind(function(result){ - var action = result.result; - this.do_action(action); - }, this)); - }, self)); + }).fail(function(err){ // error when loading models data from the backend + self.loading_error(err); }); }, + loading_error: function(err){ + var self = this; + + var message = err.message; + var comment = err.stack; + + if(err.message === 'XmlHttpRequestError '){ + message = 'Network Failure (XmlHttpRequestError)'; + comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.'; + }else if(err.message === 'OpenERP Server Error'){ + message = err.data.message; + comment = err.data.debug; + } + + if( typeof comment !== 'string' ){ + comment = 'Traceback not available.'; + } + + var popup = $(QWeb.render('ErrorTracebackPopupWidget',{ + widget: { message: message, comment: comment }, + })); + + popup.find('.button').click(function(){ + self.close(); + }); + + popup.css({ zindex: 9001 }); + + popup.appendTo(this.$el); + }, loading_progress: function(fac){ this.$('.loader .loader-feedback').removeClass('oe_hidden'); this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'}); @@ -1082,9 +1118,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {}); this.error_invoice_transfer_popup.appendTo(this.$el); + this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{}); + this.error_traceback_popup.appendTo(this.$el); + this.confirm_popup = new module.ConfirmPopupWidget(this,{}); this.confirm_popup.appendTo(this.$el); + this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{}); + this.unsent_orders_popup.appendTo(this.$el); + // -------- Misc --------- this.close_button = new module.HeaderButtonWidget(this,{ @@ -1139,7 +1181,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'choose-receipt': this.choose_receipt_popup, 'error-no-client': this.error_no_client_popup, 'error-invoice-transfer': this.error_invoice_transfer_popup, + 'error-traceback': this.error_traceback_popup, 'confirm': this.confirm_popup, + 'unsent-orders': this.unsent_orders_popup, }, default_screen: 'products', default_mode: 'cashier', diff --git a/addons/point_of_sale/static/src/sounds/error.wav b/addons/point_of_sale/static/src/sounds/error.wav new file mode 100644 index 00000000000..472f3910719 Binary files /dev/null and b/addons/point_of_sale/static/src/sounds/error.wav differ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 929212cce8d..5946133086c 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -635,6 +635,20 @@ + + + +