From fd4cb45dfd83c9ab32e13dd0d8d98ba34be21ea9 Mon Sep 17 00:00:00 2001 From: "Saurang Suthar (OpenERP)" Date: Mon, 11 Jun 2012 12:38:45 +0530 Subject: [PATCH 001/123] [FIX]stock:improved code by reverting all unwanted changes bzr revid: ssu@tinyerp.com-20120611070845-k28njbigvd8pq5l6 --- addons/stock/report/picking.rml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/addons/stock/report/picking.rml b/addons/stock/report/picking.rml index 898c078157d..5f395c9f005 100644 --- a/addons/stock/report/picking.rml +++ b/addons/stock/report/picking.rml @@ -299,21 +299,6 @@ - - - - - - - - - Total - - - [[ formatLang(get_qtytotal(picking.move_lines)['quantity']) ]] [[ get_qtytotal(picking.move_lines)['uom'] ]] - - - From bc23c8b7275c950641100ac2b276adece4a64620 Mon Sep 17 00:00:00 2001 From: "Saurang Suthar (OpenERP)" Date: Mon, 11 Jun 2012 14:16:28 +0530 Subject: [PATCH 002/123] [FIX]stock:remove unnecessary function bzr revid: ssu@tinyerp.com-20120611084628-musbyk3jhlg1ulaj --- addons/stock/report/picking.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/addons/stock/report/picking.py b/addons/stock/report/picking.py index 27141c917a9..29a53fba7a2 100644 --- a/addons/stock/report/picking.py +++ b/addons/stock/report/picking.py @@ -29,14 +29,7 @@ class picking(report_sxw.rml_parse): super(picking, self).__init__(cr, uid, name, context=context) self.localcontext.update({ 'time': time, - 'get_qtytotal':self._get_qtytotal }) - def _get_qtytotal(self,move_lines): - total = 0.0 - uom = move_lines[0].product_uom.name - for move in move_lines: - total+=move.product_qty - return {'quantity':total,'uom':uom} report_sxw.report_sxw('report.stock.picking.list','stock.picking','addons/stock/report/picking.rml',parser=picking) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 3982e3524a1ab8bbe18b931d614fbcb46ece07bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 17 Jul 2012 16:29:23 +0200 Subject: [PATCH 003/123] [FIX] point_of_sale: various css fixes bzr revid: fva@openerp.com-20120717142923-tk4nkp4mdbegcndi --- addons/point_of_sale/static/src/css/pos.css | 3 +++ addons/point_of_sale/static/src/js/pos_scrollbar_widget.js | 1 + 2 files changed, 4 insertions(+) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 0b92dce6504..d90a12b22eb 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -522,6 +522,7 @@ position:relative; vertical-align: top; display: inline-block; + line-height: 100px; font-size: 11px; margin: 5px; width: 120px; @@ -548,6 +549,7 @@ right: 2px; vertical-align: top; color: white; + line-height: 14px; background: #7f82ac; padding: 2px 5px; border-radius: 3px; @@ -561,6 +563,7 @@ box-sizing: border-box; bottom:0; top:auto; + line-height: 14px; width:100%; background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); diff --git a/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js b/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js index d4f6903c435..7c765028231 100644 --- a/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js +++ b/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js @@ -65,6 +65,7 @@ function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_ this.auto_hide(false); this.$element.bind('mousewheel',function(event,delta){ self.scroll(delta*self.wheel_step); + return false; }); this.$element.bind('click',function(event){ var vpos = event.pageY - self.$element.offset().top; From 1e00d4e9ec1c678f01ea53cb1e53a88b77b975b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 17 Jul 2012 18:25:17 +0200 Subject: [PATCH 004/123] [IMP] point_of_sale: made it easier to change the flux between screens bzr revid: fva@openerp.com-20120717162517-pjsq3x81zfw38siy --- .../static/src/js/pos_screens.js | 42 +++++++++++++------ .../static/src/js/pos_widgets.js | 10 +++-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js index eeb45e8a27d..c89228dcc18 100644 --- a/addons/point_of_sale/static/src/js/pos_screens.js +++ b/addons/point_of_sale/static/src/js/pos_screens.js @@ -349,6 +349,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ScaleInviteScreenWidget = module.ScreenWidget.extend({ template:'ScaleInviteScreenWidget', + next_screen:'scale', + previous_screen:'products', + show: function(){ this._super(); var self = this; @@ -359,7 +362,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var weight = self.pos.proxy.weighting_read_kg(); if(weight > 0.001){ clearInterval(this.intervalID); - self.pos_widget.screen_selector.set_current_screen('scale'); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); } },500); @@ -370,7 +373,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa click: function(){ clearInterval(this.intervalID); self.pos.proxy.weighting_end(); - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } } ); @@ -383,6 +386,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ScaleScreenWidget = module.ScreenWidget.extend({ template:'ScaleScreenWidget', + + next_screen: 'products', + previous_screen: 'products', + show: function(){ this._super(); this.renderElement(); @@ -392,7 +399,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa label: 'back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } }); @@ -401,7 +408,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ self.order_product(); - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); }, }); @@ -419,7 +426,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(); this.$('.product-picture').click(function(){ self.order_product(); - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); }); }, get_product: function(){ @@ -458,6 +465,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ClientPaymentScreenWidget = module.ScreenWidget.extend({ template:'ClientPaymentScreenWidget', + next_screen: 'welcome', + previous_screen: 'products', show: function(){ this._super(); var self = this; @@ -486,7 +495,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa self.pos.push_order(currentOrder.exportAsJSON()).then(function() { currentOrder.destroy(); self.pos.proxy.transaction_end(); - self.pos_widget.screen_selector.set_current_screen('welcome'); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); }); }else if(payment === 'payment_rejected'){ clearInterval(this.intervalID); @@ -501,7 +510,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa click: function(){ //TODO Go to ask for weighting screen clearInterval(this.intervalID); self.pos.proxy.payment_canceled(); - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } } ); @@ -515,12 +524,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.WelcomeScreenWidget = module.ScreenWidget.extend({ template:'WelcomeScreenWidget', + next_screen: 'products', + show_numpad: false, show_leftpane: false, barcode_client_action: function(ean){ this._super(ean); - this.pos_widget.screen_selector.set_current_screen('products'); + this.pos_widget.screen_selector.set_current_screen(self.next_screen); }, show: function(){ @@ -532,6 +543,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ProductScreenWidget = module.ScreenWidget.extend({ template:'ProductScreenWidget', + scale_screen: 'scale_invite', + client_next_screen: 'client_payment', + show_numpad: true, show_leftpane: true, @@ -543,7 +557,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.product_list_widget = new module.ProductListWidget(this,{ click_product_action: function(product){ if(product.get('to_weight') && self.pos.use_scale){ - self.pos_widget.screen_selector.set_current_screen('scale_invite', {product: product}); + self.pos_widget.screen_selector.set_current_screen(self.scale_screen, {product: product}); }else{ self.pos.get('selectedOrder').addProduct(product); } @@ -568,7 +582,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa label: 'pay', icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', click: function(){ - self.pos_widget.screen_selector.set_current_screen('client_payment'); + self.pos_widget.screen_selector.set_current_screen(self.client_next_screen); } }); } @@ -643,6 +657,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.PaymentScreenWidget = module.ScreenWidget.extend({ template: 'PaymentScreenWidget', + back_screen: 'products', + next_screen: 'receipt', init: function(parent, options) { this._super(parent,options); this.model = options.model; @@ -664,7 +680,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa label: 'Back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ - self.pos_widget.screen_selector.set_current_screen('products'); + self.pos_widget.screen_selector.set_current_screen(self.back_screen); }, }); @@ -682,7 +698,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.payment_screen.set_numpad_state(null); }, back: function() { - this.pos_widget.screen_selector.set_current_screen('products'); + this.pos_widget.screen_selector.set_current_screen(self.back_screen); }, validateCurrentOrder: function() { var self = this; @@ -697,7 +713,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa self.pos.proxy.print_receipt(currentOrder.export_for_printing()); self.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen }else{ - self.pos_widget.screen_selector.set_current_screen('receipt'); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); } }); }, diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js index b20bee712ee..0ee8cbf519d 100644 --- a/addons/point_of_sale/static/src/js/pos_widgets.js +++ b/addons/point_of_sale/static/src/js/pos_widgets.js @@ -676,6 +676,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.build_widgets(); + self.screen_selector.set_default_screen(); + + self.pos.barcode_reader.connect(); + instance.webclient.set_content_full_screen(true); if (!self.pos.get('pos_session')) { @@ -700,6 +704,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }); }, + + // This method instantiates all the screens, widgets, etc. If you want to add new screens change the + // startup screen, etc, override this method. build_widgets: function() { // -------- Screens --------- @@ -788,9 +795,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa default_mode: this.pos.use_selfcheckout ? 'client' : 'cashier', }); - this.screen_selector.set_default_screen(); - - this.pos.barcode_reader.connect(); }, //FIXME this method is probably not at the right place ... From 3577764184ebe2c6f34e59c8e781f4455882a1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 17 Jul 2012 18:26:09 +0200 Subject: [PATCH 005/123] [FIX] point_of_sale: removed auto-installation of the module bzr revid: fva@openerp.com-20120717162609-sj48xe8u8k52v4j2 --- addons/point_of_sale/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 5e7e5578f72..4c0bb14cc10 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -99,6 +99,6 @@ Main features : 'static/src/css/keyboard.css' ], 'qweb': ['static/src/xml/pos.xml'], - 'auto_install': True, + 'auto_install': False, } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From c94104586517da23fec98b8b50f93fb1ab30556c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 18 Jul 2012 11:29:54 +0200 Subject: [PATCH 006/123] [IMP] point_of_sale: very very subtle css improvement bzr revid: fva@openerp.com-20120718092954-ww84yaxh1p2cyf1b --- addons/point_of_sale/static/src/css/pos.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index d90a12b22eb..4ec88e02185 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -530,9 +530,9 @@ background:#fff; border: 1px solid #fff; border-radius: 3px; - -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2); - box-shadow: 0px 1px 8px rgba(0,0,0,0.2); + -webkit-box-shadow: 0px 1px 8px rgba(127,130,172,0.4); + -moz-box-shadow: 0px 1px 8px rgba(127,130,172,0.4); + box-shadow: 0px 1px 8px rgba(127,130,172,0.4); } .point-of-sale .product .product-img { From 6df84daaee48ea46ace902aee121c26aa1b243f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 19 Jul 2012 17:25:19 +0200 Subject: [PATCH 007/123] [IMP] point_of_sale: removed action bar bzr revid: fva@openerp.com-20120719152519-l6gbweu58fk54ga3 --- addons/point_of_sale/static/src/css/pos.css | 41 +++++++-- .../static/src/js/pos_screens.js | 84 ++++++++++++------ .../static/src/js/pos_widgets.js | 86 ++++++++++++++----- addons/point_of_sale/static/src/xml/pos.xml | 31 ++----- 4 files changed, 163 insertions(+), 79 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 4ec88e02185..b03633e181c 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -156,11 +156,11 @@ } .point-of-sale #rightheader button { - color: black; - height:29px; - margin:2px; + color: #273072; + height:27px; + margin:3px; margin-right:0px; - border: 1px solid black; + border: 1px solid #353A7E; background: #7f82ac; background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); background: -moz-linear-gradient(#b2b3d7, #7f82ac); @@ -187,6 +187,33 @@ font-weight: 900; } +/* c) The session buttons */ + +.point-of-sale #rightheader .header-button{ + float:right; + height:32px; + padding-left:10px; + padding-right:10px; + border-right: 1px solid #3a3a3a; + border-left: 1px solid #3a3a3a; + color:#DDD; + line-height:32px; + text-align:center; + cursor: pointer; + + -webkit-transition-property: background; + -webkit-transition-duration: 0.2s; + -webkit-transition-timing-function: ease-out; +} +.point-of-sale #rightheader .header-button:last-child{ + border-left: 1px solid #3a3a3a; +} +.point-of-sale #rightheader .header-button:hover{ + background: rgba(0,0,0,0.2); + text-shadow: #000 0px 0px 3px; + color:#EEE; +} + /* c) The notifications indicator */ .point-of-sale .oe_pos_synch-notification{ @@ -234,7 +261,8 @@ left:0; width:440px; top:0px; - bottom:105px; + /*bottom:105px;*/ + bottom:0; border-right: solid 1px #CECBCB; background-color: white; } @@ -318,7 +346,8 @@ .point-of-sale #rightpane { position: absolute; top: 0; - bottom: 105px; + /*bottom: 105px;*/ + bottom:0; left: 440px; right: 0; vertical-align: top; diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js index c89228dcc18..93e25ab21dc 100644 --- a/addons/point_of_sale/static/src/js/pos_screens.js +++ b/addons/point_of_sale/static/src/js/pos_screens.js @@ -152,14 +152,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.screen_selector.show_popup('help'); }, - logout_button_action: function(){ - this.pos_widget.screen_selector.set_user_mode('client'); - }, - - close_button_action: function(){ - this.pos_widget.try_close(); - }, - barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned barcode_product_error_popup: 'error', //if defined, this popup will be loaded when there's an error in the popup @@ -223,6 +215,29 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } }, + // shows an action bar on the screen. The actionbar is automatically shown when you add a button + // with add_action_button() + show_action_bar: function(){ + this.pos_widget.action_bar.show(); + this.$element.css({'bottom':'105px'}); + }, + + // hides the action bar. The actionbar is automatically hidden when it is empty + hide_action_bar: function(){ + this.pos_widget.action_bar.hide(); + this.$element.css({'bottom':'0px'}); + }, + + // adds a new button to the action bar. The button definition takes three parameters, all optional : + // - label: the text below the button + // - icon: a small icon that will be shown + // - click: a callback that will be executed when the button is clicked. + // the method returns a reference to the button widget, and automatically show the actionbar. + add_action_button: function(button_def){ + this.show_action_bar(); + return this.pos_widget.action_bar.add_new_button(button_def); + }, + // this method shows the screen and sets up all the widget related to this screen. Extend this method // if you want to alter the behavior of the screen. show: function(){ @@ -231,15 +246,30 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$element.show(); } + if(this.pos_widget.action_bar.get_button_count() > 0){ + this.show_action_bar(); + }else{ + this.hide_action_bar(); + } + var self = this; var cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier'; this.pos_widget.set_numpad_visible(this.show_numpad && cashier_mode); this.pos_widget.set_leftpane_visible(this.show_leftpane); this.pos_widget.set_cashier_controls_visible(cashier_mode); - this.pos_widget.action_bar.set_element_visible('help-button', !cashier_mode, function(){ self.help_button_action(); }); - this.pos_widget.action_bar.set_element_visible('logout-button', cashier_mode && this.pos.use_selfcheckout, function(){ self.logout_button_action(); }); - this.pos_widget.action_bar.set_element_visible('close-button', cashier_mode, function(){ self.close_button_action(); }); + /*this.pos_widget.action_bar.set_element_visible('help-button', !cashier_mode, function(){ self.help_button_action(); });*/ + + if(cashier_mode && this.pos.use_selfcheckout){ + this.pos_widget.client_button.show(); + }else{ + this.pos_widget.client_button.hide(); + } + if(cashier_mode){ + this.pos_widget.close_button.show(); + }else{ + this.pos_widget.close_button.hide(); + } this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode()); @@ -251,7 +281,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); }, - // this method is called when the screen is closed to make place for a new screen. this is a good place // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close() close: function(){ @@ -366,8 +395,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } },500); - this.pos_widget.action_bar.add_new_button( - { + this.add_action_button({ label: 'back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ @@ -375,8 +403,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa self.pos.proxy.weighting_end(); self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } - } - ); + }); }, close: function(){ this._super(); @@ -395,7 +422,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.renderElement(); var self = this; - this.pos_widget.action_bar.add_new_button({ + + this.add_action_button({ label: 'back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ @@ -403,7 +431,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } }); - this.validate_button = this.pos_widget.action_bar.add_new_button({ + this.validate_button = this.add_action_button({ label: 'Validate', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ @@ -465,8 +493,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ClientPaymentScreenWidget = module.ScreenWidget.extend({ template:'ClientPaymentScreenWidget', + next_screen: 'welcome', previous_screen: 'products', + show: function(){ this._super(); var self = this; @@ -503,17 +533,15 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } },500); - this.pos_widget.action_bar.add_new_button( - { + this.add_action_button({ label: 'back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', - click: function(){ //TODO Go to ask for weighting screen + click: function(){ clearInterval(this.intervalID); self.pos.proxy.payment_canceled(); self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } - } - ); + }); }, close: function(){ this._super(); @@ -578,7 +606,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } if(this.pos_widget.screen_selector.current_mode === 'client'){ - this.pos_widget.action_bar.add_new_button({ + this.add_action_button({ label: 'pay', icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', click: function(){ @@ -618,13 +646,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(); var self = this; - this.pos_widget.action_bar.add_new_button({ + this.add_action_button({ label: 'Print', icon: '/point_of_sale/static/src/img/icons/png48/printer.png', click: function(){ self.print(); }, }); - this.pos_widget.action_bar.add_new_button({ + this.add_action_button({ label: 'Next Order', icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', click: function() { self.finishOrder(); }, @@ -676,7 +704,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.set_numpad_state(this.pos_widget.numpad.state); - this.back_button = this.pos_widget.action_bar.add_new_button({ + this.back_button = this.add_action_button({ label: 'Back', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ @@ -684,7 +712,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, }); - this.validate_button = this.pos_widget.action_bar.add_new_button({ + this.validate_button = this.add_action_button({ label: 'Validate', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js index 0ee8cbf519d..a8948437f2c 100644 --- a/addons/point_of_sale/static/src/js/pos_widgets.js +++ b/addons/point_of_sale/static/src/js/pos_widgets.js @@ -112,7 +112,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa template:'OrderWidget', init: function(parent, options) { this._super(parent,options); - this.compact = false; + this.compact = options.compact !== undefined ? options.compact : true; this.set_numpad_state(options.numpadState); this.pos.bind('change:selectedOrder', this.change_selected_order, this); this.bind_orderline_events(); @@ -166,6 +166,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this._super(); if(!this.compact){ + console.log('not compact'); $('.point-of-sale .order-container').css({'bottom':'0px'}); } @@ -212,6 +213,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.$('.summary .value.total').html(this.format_currency(total)); }, set_compact: function(compact){ + console.log('set_compact',compact); if(this.compact !== compact){ this.compact = compact; this.renderElement(); @@ -313,6 +315,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa module.ActionButtonWidget = instance.web.Widget.extend({ template:'ActionButtonWidget', + icon_template:'ActionButtonWidgetWithIcon', init: function(parent, options){ this._super(parent, options); this.label = options.label || 'button'; @@ -320,7 +323,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.click_action = options.click; if(options.icon){ this.icon = options.icon; - this.template = 'ActionButtonWidgetWithIcon'; + this.template = this.icon_template; } }, renderElement: function(){ @@ -359,18 +362,20 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.button_list = []; return this; }, + get_button_count: function(){ + return this.button_list.length; + }, add_new_button: function(button_options){ - if(arguments.length == 1){ - var button = new module.ActionButtonWidget(this,button_options); - this.button_list.push(button); - button.appendTo($('.pos-actionbar-button-list')); - return button; - }else{ - for(var i = 0; i < arguments.length; i++){ - this.add_new_button(arguments[i]); - } - } - return undefined; + var button = new module.ActionButtonWidget(this,button_options); + this.button_list.push(button); + button.appendTo($('.pos-actionbar-button-list')); + return button; + }, + show:function(){ + this.$element.show(); + }, + hide:function(){ + this.$element.hide(); }, }); @@ -419,14 +424,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this._super(); var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons - console.log('barfoo'); - _.each(this.subcategories, function(category){ if(category.category_image_small){ - console.log("Image:",category.category_image_small); hasimages = true; }else{ - console.log('NoImage:',category.category_image_small); } }); @@ -603,14 +604,35 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); + module.HeaderButtonWidget = module.PosBaseWidget.extend({ + template: 'HeaderButtonWidget', + init: function(parent, options){ + options = options || {}; + this._super(parent, options); + this.action = options.action; + this.label = options.label; + }, + renderElement: function(){ + var self = this; + this._super(); + if(this.action){ + this.$element.click(function(){ self.action(); }); + } + }, + show: function(){ this.$element.show(); }, + hide: function(){ this.$element.hide(); }, + + }); + + // ---------- Main Point of Sale Widget ---------- // this is used to notify the user that data is being synchronized on the network module.SynchNotificationWidget = module.PosBaseWidget.extend({ template: "SynchNotificationWidget", - init: function(parent,options) { + init: function(parent, options){ options = options || {}; - this._super(parent,options); + this._super(parent, options); }, renderElement: function() { var self = this; @@ -652,6 +674,14 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.leftpane_visible = true; this.leftpane_width = '440px'; this.cashier_controls_visible = true; + var degree = 0; + /* + //Epileptic mode + setInterval(function(){ + $('body').css({'-webkit-filter':'sepia('+Math.round(Math.random())+') hue-rotate('+Math.random()*360+'deg) blur('+Math.random()*5+'px)' }); + },100); + */ + }, start: function() { @@ -708,6 +738,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // This method instantiates all the screens, widgets, etc. If you want to add new screens change the // startup screen, etc, override this method. build_widgets: function() { + var self = this; // -------- Screens --------- @@ -749,13 +780,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // -------- Misc --------- this.notification = new module.SynchNotificationWidget(this,{}); - this.notification.replace(this.$('.placeholder-SynchNotificationWidget')); + this.notification.appendTo(this.$('#rightheader')); this.username = new module.UsernameWidget(this,{}); this.username.replace(this.$('.placeholder-UsernameWidget')); this.action_bar = new module.ActionBarWidget(this); - this.action_bar.appendTo($(".point-of-sale #content")); + this.action_bar.appendTo($(".point-of-sale #rightpane")); this.paypad = new module.PaypadWidget(this, {}); this.paypad.replace($('#placeholder-PaypadWidget')); @@ -770,6 +801,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'keyboard_model': 'simple' }); this.onscreen_keyboard.appendTo($(".point-of-sale #content")); + + this.close_button = new module.HeaderButtonWidget(this,{ + label:'Close', + action: function(){ self.try_close(); }, + }); + this.close_button.appendTo(this.$('#rightheader')); + + this.client_button = new module.HeaderButtonWidget(this,{ + label:'Self-Checkout', + action: function(){ self.screen_selector.set_user_mode('client'); }, + }); + this.client_button.appendTo(this.$('#rightheader')); + // -------- Screen Selector --------- diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 441189dae24..b690f6ebe6b 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -15,7 +15,7 @@
    - +
    @@ -44,6 +44,12 @@
    + +
    + +
    +
    +
    There are pending operations that could not be saved into the database, are you sure you want to exit?
    @@ -531,30 +537,7 @@
    -
    -
    -
    - -
    Help
    -
    -
    -
    -
    - -
    Close
    -
    -
    -
    -
    - -
    Client Mode
    -
    -
    -
      -
    From 48f50f9944f72128119d48bea12670899919bcee Mon Sep 17 00:00:00 2001 From: "Dharti Ratani (OpenERP)" Date: Fri, 20 Jul 2012 18:50:03 +0530 Subject: [PATCH 008/123] Adding a rng for label in digram view bzr revid: dhr@tinyerp.com-20120720132003-kq0ikv9b6ewkasxx --- openerp/addons/base/rng/view.rng | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openerp/addons/base/rng/view.rng b/openerp/addons/base/rng/view.rng index c583040495b..8f7b2647237 100644 --- a/openerp/addons/base/rng/view.rng +++ b/openerp/addons/base/rng/view.rng @@ -184,6 +184,9 @@ + + + From bf7e2b0e762601f5aa2c54323158585649c3d7ac Mon Sep 17 00:00:00 2001 From: "Dharti Ratani (OpenERP)" Date: Fri, 20 Jul 2012 18:54:14 +0530 Subject: [PATCH 009/123] Modifying diagram view to set label in diagram header bzr revid: dhr@tinyerp.com-20120720132414-pdftmig39m3ro93b --- addons/web_diagram/static/src/js/diagram.js | 13 +++++++++++++ addons/web_diagram/static/src/xml/base_diagram.xml | 1 + 2 files changed, 14 insertions(+) diff --git a/addons/web_diagram/static/src/js/diagram.js b/addons/web_diagram/static/src/js/diagram.js index f06e757f7f1..e7e85eea96a 100644 --- a/addons/web_diagram/static/src/js/diagram.js +++ b/addons/web_diagram/static/src/js/diagram.js @@ -51,6 +51,9 @@ instance.web.DiagramView = instance.web.View.extend({ var action = $(this).data('pager-action'); self.on_pager_action(action); }); + this.labels = _.filter(this.fields_view.arch.children, function(label){ + return label.tag == "label" ; + }); this.do_update_pager(); @@ -104,11 +107,21 @@ instance.web.DiagramView = instance.web.View.extend({ ); }, + get_label : function(){ + var self = this + var html_label = _.each(self.labels,function(label){ + html_label = "" + label.attrs.string + "" + self.$element.find('.oe_diagram_diagram_header').append(html_label) + }) + + }, + on_diagram_loaded: function(record) { var id_record = record['id']; if(id_record) { this.id = id_record; this.get_diagram_info(); + this.get_label(); } }, diff --git a/addons/web_diagram/static/src/xml/base_diagram.xml b/addons/web_diagram/static/src/xml/base_diagram.xml index 96e32f59648..40a98a59413 100644 --- a/addons/web_diagram/static/src/xml/base_diagram.xml +++ b/addons/web_diagram/static/src/xml/base_diagram.xml @@ -14,6 +14,7 @@
    +
    From 7a1cee1401bf7c5ea97981a69b4f25a0601ba1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Fri, 20 Jul 2012 17:24:21 +0200 Subject: [PATCH 010/123] [IMP] point_of_sale: refactored the ean checksum method bzr revid: fva@openerp.com-20120720152421-lfq05r6xkczl1y56 --- addons/product/product.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index 3cbff880cf9..c50ad3c27b9 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -30,15 +30,10 @@ from tools.translate import _ def is_pair(x): return not x%2 -def check_ean(eancode): - if not eancode: - return True +def ean_checksum(eancode): + """returns the checksum of an ean string of length 13, returns -1 if the string has the wrong length""" if len(eancode) <> 13: - return False - try: - int(eancode) - except: - return False + return -1 oddsum=0 evensum=0 total=0 @@ -54,10 +49,20 @@ def check_ean(eancode): total=(oddsum * 3) + evensum check = int(10 - math.ceil(total % 10.0)) %10 + return check - if check != int(eancode[-1]): +def check_ean(eancode): + """returns True if eancode is a valid ean13 string, or null""" + if not eancode: + return True + if len(eancode) <> 13: return False - return True + try: + int(eancode) + except: + return False + return ean_checksum(eancode) == int(eancode[-1]) + #---------------------------------------------------------- # UOM #---------------------------------------------------------- From f1fbf44b5c9ae1275c9fb9e6a5c1ef8e337742a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Fri, 20 Jul 2012 19:00:12 +0200 Subject: [PATCH 011/123] [WIP] point_of_sale: barcode validation wizard (wip) bzr revid: fva@openerp.com-20120720170012-eqp81clbpbfsr3hl --- addons/point_of_sale/point_of_sale.py | 38 ++++++++++++------ addons/point_of_sale/point_of_sale_view.xml | 44 +++++++++++++++------ addons/product/product.py | 13 ++++++ 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index ed3fa9c1730..d86d4c3d8ad 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -22,6 +22,7 @@ import pdb import io import openerp import addons +import openerp.addons.product.product import time from datetime import datetime @@ -143,8 +144,6 @@ class pos_config(osv.osv): obj.sequence_id.unlink() return super(pos_config, self).unlink(cr, uid, ids, context=context) -pos_config() - class pos_session(osv.osv): _name = 'pos.session' _order = 'id desc' @@ -425,8 +424,6 @@ class pos_session(osv.osv): 'context' : context, } -pos_session() - class pos_order(osv.osv): _name = "pos.order" _description = "Point of Sale" @@ -1038,8 +1035,6 @@ class pos_order(osv.osv): self.create_account_move(cr, uid, ids, context=context) return True -pos_order() - class account_bank_statement(osv.osv): _inherit = 'account.bank.statement' _columns= { @@ -1138,8 +1133,6 @@ class pos_order_line(osv.osv): }) return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context) -pos_order_line() - class pos_category(osv.osv): _name = 'pos.category' _description = "Point of Sale Category" @@ -1211,12 +1204,24 @@ class pos_category(osv.osv): # 'category_image': _get_default_image, #} -pos_category() - import io, StringIO +class ean_wizard(osv.osv_memory): + _name = 'pos.ean_wizard' + _columns = { + 'ean13_pattern': fields.char('Ean13 Pattern', size=32, required=True, translate=True), + } + def sanitize_ean13(self, cr, uid, ids, context): + for r in self.browse(cr,uid,ids): + ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern) + m = context.get('active_model') + m_id = context.get('active_id') + self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13}) + class product_product(osv.osv): _inherit = 'product.product' + + def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None): result = {} for obj in self.browse(cr, uid, ids, context=context): @@ -1247,7 +1252,16 @@ class product_product(osv.osv): 'to_weight' : False, } -product_product() - + def add_ean13(self, cr, uid, ids, context): + return { + 'name': _('Return Products'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.ean_wizard', + 'target' : 'new', + 'view_id': False, + 'context':context, + } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index f3b491116b7..1145189b408 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -618,12 +618,22 @@ form - - - - - - + + + + + + + + + + + + + + +
    -
    +
    Change: @@ -269,6 +269,9 @@

    Please scan an item or your member card

    +
    +

    Thank you for shopping with us.

    +
    From 7dabfe49eaa00d42649910af355811c6c2fd2512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 30 Jul 2012 17:42:28 +0200 Subject: [PATCH 025/123] [FIX] point_of_sale: removing useless console.log bzr revid: fva@openerp.com-20120730154228-5vabk5qyvjez9dgk --- addons/point_of_sale/static/src/js/pos_screens.js | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js index f79b60052c0..07f1071fa6d 100644 --- a/addons/point_of_sale/static/src/js/pos_screens.js +++ b/addons/point_of_sale/static/src/js/pos_screens.js @@ -581,7 +581,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var self = this; $('.goodbye-message').css({opacity:1}).show(); setTimeout(function(){ - console.log('kill'); $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();}); },3000); }, From 03a3533dc5519e15b3725e037c4017732dca7181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 2 Aug 2012 16:18:01 +0200 Subject: [PATCH 026/123] [WIP] point_of_sale: IndexedDB Backend bzr revid: fva@openerp.com-20120802141801-hpjqwkwlooqdaddu --- addons/point_of_sale/static/src/js/db.js | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 addons/point_of_sale/static/src/js/db.js diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js new file mode 100644 index 00000000000..86ad9a53a59 --- /dev/null +++ b/addons/point_of_sale/static/src/js/db.js @@ -0,0 +1,66 @@ +function openerp_pos_db(instance, module){ + function importIndexedDB(){ + if('indexedDB' in window){ + return true; + }else if('webkitIndexedDB' in window){ + window.indexedDB = window.webkitIndexedDB; + window.IDBCursor = window.webkitIDBCursor; + window.IDBDatabase = window.webkitIDBDatabase; + window.IDBDatabaseError = window.webkitIDBDatabaseError; + window.IDBDatabaseException = window.webkitIDBDatabaseException; + window.IDBFactory = window.webkitIDBFactory; + window.IDBIndex = window.webkitIDBIndex; + window.IDBKeyRange = window.webkitIDBKeyRange; + window.IDBObjectSrore = window.webkitIDBOjbectStore; + window.IDBRequest = window.webkitIDBRequest; + window.IDBTransaction = window.webkitIDBTransaction; + }else if('mozIndexedDB' in window){ + window.indexedDB = window.mozIndexedDB; + }else{ + throw new Error("DATABASE ERROR: IndexedDB not implemented. Please upgrade your web browser"); + } + } + importIndexedDB(); + + modula.PosDB = modula.Class.extend({ + + state: 'connecting', // 'connecting' || 'connected' || 'failed' + version: 1, + name: 'openerp_pos_db', + + init: function(options){ + var open_request = indexedDB.open(this.name, this.version); + }, + upgrade_db: function(oldversion, transaction){ + this.db = transaction.db; + var productStore = this.db.createObjectStore("products", {keyPath: "id"}); + + productStore.createIndex("ean13", "ean13", {unique:true}); + + productStore.createIndex("name", "name", {unique:false}); + + productStore.createIndex("category","category", {unique:false}); + + var imageStore = this.db.createObjectStore("images", {keyPath: "id"}); + + imageStore.createIndex("product_id", "product_id", {unique:true}); + }, + _add_data: function(store, data, result_callback){ + var transaction = this.db.transaction([store], IDBTransaction.READ_WRITE); + + transaction.oncomplete = function(event){ if(result_callback){ result_callback(event); }}; + + }, + add_product: function(product){ + }, + get_product_by_ean: function(ean, result_callback){ + }, + get_product_by_category: function(category, result_callback){ + }, + get_product_image: function(product, result_callback){ + }, + search_product: function(query,result_callback){ + }, + }); + +} From 996183acf719efb7bf44cef3c9709cedc866a0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 6 Aug 2012 17:40:54 +0200 Subject: [PATCH 027/123] [WIP] point_of_sale: indexedDB backend bzr revid: fva@openerp.com-20120806154054-1yubgy8o8wwlc5b8 --- addons/point_of_sale/__openerp__.py | 1 + addons/point_of_sale/static/src/js/db.js | 66 ------------------- .../point_of_sale/static/src/js/pos_main.js | 2 + .../point_of_sale/static/src/js/pos_models.js | 1 + .../static/src/js/pos_widgets.js | 2 + 5 files changed, 6 insertions(+), 66 deletions(-) delete mode 100644 addons/point_of_sale/static/src/js/db.js diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 9d985d40408..c4258511a68 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -86,6 +86,7 @@ Main features: 'js': [ 'static/lib/backbone/backbone-0.9.2.js', 'static/lib/mousewheel/jquery.mousewheel-3.0.6.js', + 'static/src/js/pos_db.js', 'static/src/js/pos_models.js', 'static/src/js/pos_basewidget.js', 'static/src/js/pos_keyboard_widget.js', diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js deleted file mode 100644 index 86ad9a53a59..00000000000 --- a/addons/point_of_sale/static/src/js/db.js +++ /dev/null @@ -1,66 +0,0 @@ -function openerp_pos_db(instance, module){ - function importIndexedDB(){ - if('indexedDB' in window){ - return true; - }else if('webkitIndexedDB' in window){ - window.indexedDB = window.webkitIndexedDB; - window.IDBCursor = window.webkitIDBCursor; - window.IDBDatabase = window.webkitIDBDatabase; - window.IDBDatabaseError = window.webkitIDBDatabaseError; - window.IDBDatabaseException = window.webkitIDBDatabaseException; - window.IDBFactory = window.webkitIDBFactory; - window.IDBIndex = window.webkitIDBIndex; - window.IDBKeyRange = window.webkitIDBKeyRange; - window.IDBObjectSrore = window.webkitIDBOjbectStore; - window.IDBRequest = window.webkitIDBRequest; - window.IDBTransaction = window.webkitIDBTransaction; - }else if('mozIndexedDB' in window){ - window.indexedDB = window.mozIndexedDB; - }else{ - throw new Error("DATABASE ERROR: IndexedDB not implemented. Please upgrade your web browser"); - } - } - importIndexedDB(); - - modula.PosDB = modula.Class.extend({ - - state: 'connecting', // 'connecting' || 'connected' || 'failed' - version: 1, - name: 'openerp_pos_db', - - init: function(options){ - var open_request = indexedDB.open(this.name, this.version); - }, - upgrade_db: function(oldversion, transaction){ - this.db = transaction.db; - var productStore = this.db.createObjectStore("products", {keyPath: "id"}); - - productStore.createIndex("ean13", "ean13", {unique:true}); - - productStore.createIndex("name", "name", {unique:false}); - - productStore.createIndex("category","category", {unique:false}); - - var imageStore = this.db.createObjectStore("images", {keyPath: "id"}); - - imageStore.createIndex("product_id", "product_id", {unique:true}); - }, - _add_data: function(store, data, result_callback){ - var transaction = this.db.transaction([store], IDBTransaction.READ_WRITE); - - transaction.oncomplete = function(event){ if(result_callback){ result_callback(event); }}; - - }, - add_product: function(product){ - }, - get_product_by_ean: function(ean, result_callback){ - }, - get_product_by_category: function(category, result_callback){ - }, - get_product_image: function(product, result_callback){ - }, - search_product: function(query,result_callback){ - }, - }); - -} diff --git a/addons/point_of_sale/static/src/js/pos_main.js b/addons/point_of_sale/static/src/js/pos_main.js index 71ac69c6067..b9263b91314 100644 --- a/addons/point_of_sale/static/src/js/pos_main.js +++ b/addons/point_of_sale/static/src/js/pos_main.js @@ -5,6 +5,8 @@ openerp.point_of_sale = function(instance) { var module = instance.point_of_sale; + openerp_pos_db(instance,module); // import db.js + openerp_pos_models(instance,module); // import pos_models.js openerp_pos_basewidget(instance,module); // import pos_basewidget.js diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js index 548b6b749f9..634e0d9b862 100644 --- a/addons/point_of_sale/static/src/js/pos_models.js +++ b/addons/point_of_sale/static/src/js/pos_models.js @@ -74,6 +74,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.weightable_categories = []; // a flat list of all categories that directly contain weightable products this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy + this.db = new module.PosDB(); // a database used to store the products and product images // pos settings this.use_scale = false; diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js index b41ccbfcc9a..a30a0ebe521 100644 --- a/addons/point_of_sale/static/src/js/pos_widgets.js +++ b/addons/point_of_sale/static/src/js/pos_widgets.js @@ -968,12 +968,14 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }); }, close: function() { + var self = this; this.pos.barcode_reader.disconnect(); return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe( _.bind(function(res) { return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) { var action = result.result; action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}}); + //self.destroy(); this.do_action(action); }, this)); }, this)); From dc8f543d17a7840b6c02729c2649d0bb37538710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 7 Aug 2012 18:18:44 +0200 Subject: [PATCH 028/123] [WIP] point_of_sale: indexeddb : compatible localstorage backend. + anthony's modifications to the views... bzr revid: fva@openerp.com-20120807161844-hd7ioxipnc6794jf --- addons/point_of_sale/point_of_sale_view.xml | 62 +-- addons/point_of_sale/static/src/js/pos_db.js | 466 ++++++++++++++++++ .../point_of_sale/static/src/js/pos_models.js | 2 +- .../wizard/pos_session_opening.xml | 2 +- 4 files changed, 487 insertions(+), 45 deletions(-) create mode 100644 addons/point_of_sale/static/src/js/pos_db.js diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 6a43cec39be..9aa9c095f65 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -3,10 +3,6 @@ - Others @@ -155,9 +151,6 @@ - - - Products @@ -170,10 +163,7 @@ You must define a Product for everything you buy or sell. Products can be raw materials, stockable products, consumables or services. The Product form contains detailed information about your products related to procurement logistics, sales price, product category, suppliers and so on. - + Sale lines @@ -673,11 +663,8 @@ tree,form - - Payment Methods @@ -689,10 +676,6 @@ Payment methods are defined by accounting journals having the field Payment Method checked. - POS Sales Lines @@ -716,9 +699,6 @@ - - - Invoices @@ -729,8 +709,6 @@ [('origin','like','POS')] - Start Point of Sale @@ -848,11 +826,6 @@ - - - - - pos.order.list.select pos.order @@ -1034,7 +993,6 @@ - pos.ean_wizard @@ -1050,6 +1008,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/addons/point_of_sale/static/src/js/pos_db.js b/addons/point_of_sale/static/src/js/pos_db.js new file mode 100644 index 00000000000..a38457a9f8d --- /dev/null +++ b/addons/point_of_sale/static/src/js/pos_db.js @@ -0,0 +1,466 @@ +/** + * This file contains an IndexedDB (html5 database) backend for the Point Of Sale. + * The IDB is used to store the list of products and especially their thumbnails, which may + * not fit in the localStorage. + * The IDB offers a big performance boost for products lookup, but not so much for search as + * searching is not yet implemented in the IndexedDB API and must be performed manually in JS. + * + */ +function openerp_pos_db(instance, module){ + + // this is used for testing + window.gen_products = function(options){ + options = options || {}; + var count = options.count || 100; + var imageSize = options.imageSize || 1800; + + var imagedata = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F']; + + function gen_image(){ + var image = ''; + for(var i = 0; i < imageSize; i++){ + image += imagedata[Math.floor(Math.random()*imagedata.length)]; + } + return image; + } + + var categories = [ 'drinks', 'food', 'beers', 'meat', 'fruits', 'candy', 'bread', 'fruity beers', 'tea', 'coffee' ]; + var syllabes = [ + 'ko','no','mo','ro','do','so','ho','to','yo', + 'ke','ne','me','re','de','se','he','te','ye', + 'ki','ni','mi','ri','di','si','hi','ti','yi', + 'ku','nu','mu','ru','du','su','hu','tu','yu', + 'ka','na','ma','ra','da','sa','ha','ta','ya', + ' ', '-', '!', ' ', ' ', '-', ' ', ' ', ' ', + ]; + var id = 1284; + var ean = 845923; + + function gen_product(){ + var name = ''; + var sc = Math.ceil(Math.random()*10); + for(var i = 0; i < sc; i++){ + name = name + syllabes[Math.floor(Math.random()*syllabes.length)]; + } + return { + id: ''+(id++), + price: Math.random()*100, + ean13:''+(ean++), + name: name, + category: categories[Math.floor(Math.random()*categories.length)], + product_image_small: gen_image(), + }; + } + + var products = []; + for(var i = 0; i < count; i++){ + products.push(gen_product()); + } + return products; + }; + + //this is used for testing + window.log = function(x){ + console.log(x); + }; + + function importIndexedDB(){ + if('indexedDB' in window){ + return true; + }else if('webkitIndexedDB' in window){ + window.indexedDB = window.webkitIndexedDB; + window.IDBCursor = window.webkitIDBCursor; + window.IDBDatabase = window.webkitIDBDatabase; + window.IDBDatabaseError = window.webkitIDBDatabaseError; + window.IDBDatabaseException = window.webkitIDBDatabaseException; + window.IDBFactory = window.webkitIDBFactory; + window.IDBIndex = window.webkitIDBIndex; + window.IDBKeyRange = window.webkitIDBKeyRange; + window.IDBObjectSrore = window.webkitIDBOjbectStore; + window.IDBRequest = window.webkitIDBRequest; + window.IDBTransaction = window.webkitIDBTransaction; + }else if('mozIndexedDB' in window){ + window.indexedDB = window.mozIndexedDB; + }else{ + throw new Error("DATABASE ERROR: IndexedDB not implemented. Please upgrade your web browser"); + } + } + importIndexedDB(); + + module.PosDB = instance.web.Class.extend({ + + version: 1, + name: 'openerp_pos_db', + limit: 50, + + init: function(options){ + var self = this; + options = options || {}; + + this.version = options.version || this.version; + this.name = options.name || this.name; + + var open_request = indexedDB.open(this.name, this.version); + + open_request.onblocked = function(event){ + throw new Error("DATABASE ERROR: request blocked."); + }; + + // new API (firefox) + open_request.onupgradeneeded = function(event){ + console.log('using new API'); + var transaction = open_request.transaction; + self._upgrade_db(event.oldVersion,transaction); + }; + + open_request.onsuccess = function(event){ + console.log('db',self.db); + self.db = open_request.result; + var oldVersion = Number(self.db.version); + + if(oldVersion !== self.version){ + // if we get to this point, it means that onupgradeneeded hasn't been called. + // so we try the old api (webkit) + if(!self.db.setVersion){ + throw new Error("DATABASE ERROR: database API is broken. Your web browser is probably out of date"); + } + console.log('using old API'); + var version_request = self.db.setVersion(self.version); + version_request.onsuccess = function(event){ + var transaction = version_request.result; + self._upgrade_db(oldVersion, transaction); + }; + } + }; + }, + _upgrade_db: function(oldversion, transaction){ + console.log('upgrade_db:',oldversion); + + this.db = transaction.db; + var productStore = this.db.createObjectStore('products', {keyPath: 'id'}); + + productStore.createIndex('ean13', 'ean13', {unique:true}); + + productStore.createIndex('name', 'name', {unique:false}); + + productStore.createIndex('category','category', {unique:false}); + + var imageStore = this.db.createObjectStore('images', {keyPath: 'id'}); + }, + + _add_data: function(store, data){ + var transaction = this.db.transaction([store], 'readwrite'); + var objectStore = transaction.objectStore(store); + var request = objectStore.put(data); + }, + /* adds a product to the database, replacing the original product if it is already present. + * the product must be provided as a json as returned by the openerp backend + */ + add_product: function(product){ + if(product instanceof Array){ + for(var i = 0, len = product.length; i < len; i++){ + this.add_product(product[i]); + } + }else{ + if(product.product_image_small){ + var image = product.product_image_small; + var product = _.clone(product); + delete product['product_image_small']; + this._add_data('images', { id: product.id, image:image}); + } + this._add_data('products',product); + } + }, + + /* removes all the data in the database ! */ + clear: function(done_callback){ + var transaction = this.db.transaction(['products','images'],'readwrite'); + transaction.objectStore('products').clear(); + transaction.objectStore('images').clear(); + if(done_callback){ + transaction.oncomplete = function(){ done_callback(); }; + } + }, + + get_product_count: function(result_callback){ + this.db.transaction('products').objectStore('products').count().onsuccess = function(event){ + result_callback(event.target.result); + }; + }, + + get_image_count: function(result_callback){ + this.db.transaction('images').objectStore('images').count().onsuccess = function(event){ + result_callback(event.target.result); + }; + }, + + /* fetches a product with an id of 'id', returns the product as the first parameter + * of the result_callback function + */ + get_product_by_id: function(id, result_callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + var request = objectStore.get(id); + request.onsuccess = function(event){ + if(result_callback){ + result_callback(event.target.result); + } + }; + }, + get_product_by_name: function(name, result_callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + var index = objectStore.index('name'); + index.get(name).onsuccess = function(event){ + result_callback(event.target.result); + }; + }, + /* fetches the product with the ean13 matching the one provided. returns the product + * as the first parameter of the result_callback function + */ + get_product_by_ean13: function(ean13, result_callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + var index = objectStore.index('ean13'); + index.get(ean13).onsuccess = function(event){ + result_callback(event.target.result); + }; + }, + /* fetches all products belonging to a category, and returns the list of products + * as the first paramter of the result_callback function + */ + get_product_by_category: function(category, result_callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + var index = objectStore.index('category'); + var list = []; + index.openCursor(IDBKeyRange.only(category)).onsuccess = function(event){ + var cursor = event.target.result; + if(cursor){ + list.push(cursor.value); + cursor.continue(); + }else{ + result_callback(list); + } + }; + }, + /* Fetches a picture associated with a product. + * product: This is a product json object as returned by the openerp backend. + * result_callback: a callback that gets the image data as parameter. + */ + get_product_image: function(product, result_callback){ + var transaction = this.db.transaction('images'); + var objectStore = transaction.objectStore('images'); + var request = objectStore.get(product.id); + request.onsuccess = function(event){ + result_callback(event.target.result.image); + } + }, + /* fields: a string or a list of string representing the list of the fields that + * will be searched. the values of those field must be a string. + * query: a string that will be matched in the content of one of the searched fields. + * result_callback: a function that will be called with a list of all the products with + * a field that match the query. + */ + search_product: function(fields,query,result_callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + var list = []; + if(!(fields instanceof Array)){ + fields = [ fields ]; + } + query = query.toLowerCase(); + objectStore.openCursor().onsuccess = function(event){ + var cursor = event.target.result; + if(cursor){ + var obj = cursor.value; + for(var i = 0, len = fields.length; i < len; i++){ + if(obj[fields[i]].toLowerCase().indexOf(query) != -1){ + list.push(obj); + break; + } + } + cursor.continue(); + }else{ + result_callback(list); + } + } + }, + for_all_products: function(callback){ + var transaction = this.db.transaction('products'); + var objectStore = transaction.objectStore('products'); + objectStore.openCursor().onsuccess = function(event){ + var cursor = event.target.result; + if(cursor){ + var ret = callback(cursor.value); + if(ret !== 'break'){ + cursor.continue(); + } + } + }; + }, + }); + window.PosDB = module.PosDB; + + module.PosLS = instance.web.Class.extend({ + name: 'openerp_pos_ls', + limit: 50, + init: function(options){ + options = options || {}; + this.name = options.name || this.name; + this.limit = options.limit || this.limit; + this.products = this.name + '_products'; + this.images = this.name + '_images'; + }, + _get_products: function(){ + var products = localStorage[this.products]; + if(products){ + return JSON.parse(products) || {}; + }else{ + return {}; + } + }, + _set_products: function(products){ + localStorage[this.products] = JSON.stringify(products); + }, + _get_images: function(){ + var images = localStorage[this.images]; + if(images){ + return JSON.parse(images) || {}; + }else{ + return {}; + } + }, + _set_images: function(images){ + localStorage[this.images] = JSON.stringify(images); + }, + add_product: function(products){ + var stored_images = this._get_images(); + var stored_products = this._get_products(); + + if(!products instanceof Array){ + products = [products]; + } + for(var i = 0, len = products.length; i < len; i++){ + var product = products[i]; + if(product.product_image_small){ + product = _.clone(product); + stored_images[product.id] = product.product_image_small; + delete product['product_image_small']; + } + stored_products[product.id] = product; + } + this._set_images(stored_images); + this._set_products(stored_products); + }, + clear: function(done_callback){ + localStorage.removeItem(this.products); + localStorage.removeItem(this.images); + }, + _count_props : function(obj){ + if(obj.__count__){ + return obj.__count__; + }else{ + var count = 0; + for(var prop in obj){ + if(obj.hasOwnProperty(prop)){ + count++; + } + } + return count; + } + }, + get_product_count: function(result_callback){ + result_callback(this._count_props(this._get_products())); + }, + get_image_count: function(result_callback){ + result_callback(this._count_props(this._get_images())); + }, + get_product_by_id: function(id, result_callback){ + var products = this._get_products(); + result_callback( products[id] ); + }, + get_product_by_name: function(name, result_callback){ + var products = this._get_products(); + for(var i in products){ + if(products[i] && products[i].name === name){ + result_callback(products[i]); + return; + } + } + result_callback(undefined); + }, + get_product_by_ean13: function(ean13, result_callback){ + var products = this._get_products(); + for(var i in products){ + if( products[i] && products[i].ean13 === ean13){ + result_callback(products[i]); + return; + } + } + result_callback(undefined); + }, + get_product_by_category: function(category, result_callback){ + var products = this._get_products(); + var list = []; + for(var i in products){ + if( products[i] && products[i].category === category){ + list.push(products[i]); + } + } + result_callback(list); + }, + get_product_image: function(product, result_callback){ + var images = this._get_images(); + result_callback(images[product.id]); + }, + search_product: function(fields, query, result_callback){ + var products = this._get_products(); + var list = []; + if(typeof query !== 'string'){ + if(query.toString){ + query = query.toString(); + }else{ + throw new Error('search_product: the query must be a string or must be convertible to string'); + } + } + + query = query.toLowerCase(); + + if(!(fields instanceof Array)){ + fields = [fields]; + } + for(var i in products){ + for(var j = 0, jlen = fields.length; j < jlen; j++){ + var field = products[i][fields[j]]; + if(field === null || field === undefined){ + continue; + } + if(typeof field !== 'string'){ + if(field.toString){ + field = field.toString(); + }else{ + continue; + } + } + if(field.toLowerCase().indexOf(query) != -1){ + list.push(products[i]); + break; + } + } + } + result_callback(list); + }, + for_all_products: function(callback){ + var products = this._get_products(); + for(var i in products){ + var ret = callback(products[i]); + if(ret === 'break'){ + break; + } + } + }, + }); + + window.PosLS = module.PosLS; +} diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js index 634e0d9b862..b4684c037b2 100644 --- a/addons/point_of_sale/static/src/js/pos_models.js +++ b/addons/point_of_sale/static/src/js/pos_models.js @@ -74,7 +74,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.weightable_categories = []; // a flat list of all categories that directly contain weightable products this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy - this.db = new module.PosDB(); // a database used to store the products and product images + this.db = new module.PosLS(); // a database used to store the products and product images // pos settings this.use_scale = false; diff --git a/addons/point_of_sale/wizard/pos_session_opening.xml b/addons/point_of_sale/wizard/pos_session_opening.xml index 226edf70b62..ec7fae9c07e 100644 --- a/addons/point_of_sale/wizard/pos_session_opening.xml +++ b/addons/point_of_sale/wizard/pos_session_opening.xml @@ -28,7 +28,7 @@ pos.session.opening form form - new + inline From 03b79a983280fab5180ce0f7fe281e320ff729b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 9 Aug 2012 19:21:13 +0200 Subject: [PATCH 029/123] [WIP] point_of_sale: much progress on the indexeddb backend bzr revid: fva@openerp.com-20120809172113-ex3o5vyt4ckv4au6 --- addons/point_of_sale/static/src/js/pos_db.js | 225 +++++++++++++----- .../static/src/js/pos_devices.js | 63 ++--- .../point_of_sale/static/src/js/pos_models.js | 181 +++----------- .../static/src/js/pos_screens.js | 25 +- .../static/src/js/pos_widgets.js | 132 +++------- 5 files changed, 275 insertions(+), 351 deletions(-) diff --git a/addons/point_of_sale/static/src/js/pos_db.js b/addons/point_of_sale/static/src/js/pos_db.js index a38457a9f8d..59ea0da1684 100644 --- a/addons/point_of_sale/static/src/js/pos_db.js +++ b/addons/point_of_sale/static/src/js/pos_db.js @@ -146,6 +146,8 @@ function openerp_pos_db(instance, module){ productStore.createIndex('category','category', {unique:false}); var imageStore = this.db.createObjectStore('images', {keyPath: 'id'}); + + var categoryStore = this.db.createObjectStore('categories', {keypath:'name'}); }, _add_data: function(store, data){ @@ -286,6 +288,9 @@ function openerp_pos_db(instance, module){ } } }, + /* the callback function will be called with all products as parameter, by increasing id. + * if the callback returns 'break' the iteration will stop + */ for_all_products: function(callback){ var transaction = this.db.transaction('products'); var objectStore = transaction.objectStore('products'); @@ -299,6 +304,29 @@ function openerp_pos_db(instance, module){ } }; }, + /* the callback function will be called with all products as parameter by increasing id. + * if the callback returns 'break', the iteration will stop + * if the callback returns a product object, it will be inserted in the db, possibly + * overwriting an existing product. The intended usage is for the callback to return a + * modified version version of the product with the same id. Anything else and you're on your own. + */ + modify_all_products: function(callback){ + var transaction = this.db.transaction('products','readwrite'); + var objectStore = transaction.objectStore('products'); + objectStore.openCursor().onsuccess = function(event){ + var cursor = event.target.result; + if(cursor){ + var ret = callback(cursor.value); + if(ret === undefined || ret === null){ + cursor.continue(); + }else if(ret === 'break'){ + return; + }else{ + objectStore.put(ret); + } + } + }; + }, }); window.PosDB = module.PosDB; @@ -310,7 +338,74 @@ function openerp_pos_db(instance, module){ this.name = options.name || this.name; this.limit = options.limit || this.limit; this.products = this.name + '_products'; - this.images = this.name + '_images'; + this.categories = this.name + '_categories'; + + this.category_by_id = {}; + this.root_category_id = 0; + this.category_products = {}; + this.category_ancestors = {}; + this.category_childs = {}; + this.category_parent = {}; + }, + get_category_by_id: function(categ_id){ + if(categ_id instanceof Array){ + var list = []; + for(var i = 0, len = categ_id.length; i < len; i++){ + var cat = this.category_by_id[categ_id[i]]; + if(cat){ + list.push(cat); + }else{ + console.error("get_category_by_id: no category has id:",categ_id[i]); + } + } + return list; + }else{ + return this.category_by_id[categ_id]; + } + }, + get_category_childs_ids: function(categ_id){ + return this.category_childs[categ_id] || []; + }, + get_category_ancestors_ids: function(categ_id){ + return this.category_ancestors[categ_id] || []; + }, + get_category_parent_id: function(categ_id){ + return this.category_parent[categ_id] || this.root_category_id; + }, + add_categories: function(categories){ + var self = this; + if(!this.category_by_id[this.root_category_id]){ + this.category_by_id[this.root_category_id] = { + id : this.root_category_id, + name : 'Root', + }; + } + for(var i=0, len = categories.length; i < len; i++){ + console.log('putting category : ',categories[i].id, categories[i].name); + this.category_by_id[categories[i].id] = categories[i]; + } + for(var i=0, len = categories.length; i < len; i++){ + var cat = categories[i]; + var parent_id = cat.parent_id[0] || this.root_category_id; + console.log('category parent',cat.id, parent_id); + this.category_parent[cat.id] = cat.parent_id[0]; + if(!this.category_childs[parent_id]){ + this.category_childs[parent_id] = []; + } + this.category_childs[parent_id].push(cat.id); + } + function make_ancestors(cat_id, ancestors){ + self.category_ancestors[cat_id] = ancestors; + + ancestors = ancestors.slice(0); + ancestors.push(cat_id); + + var childs = self.category_childs[cat_id] || []; + for(var i=0, len = childs.length; i < len; i++){ + make_ancestors(childs[i], ancestors); + } + } + make_ancestors(this.root_category_id, []); }, _get_products: function(){ var products = localStorage[this.products]; @@ -323,39 +418,51 @@ function openerp_pos_db(instance, module){ _set_products: function(products){ localStorage[this.products] = JSON.stringify(products); }, - _get_images: function(){ - var images = localStorage[this.images]; - if(images){ - return JSON.parse(images) || {}; + _get_categories: function(){ + var categories = localStorage[this.categories]; + if(categories){ + return JSON.parse(categories) || {}; }else{ return {}; } }, - _set_images: function(images){ - localStorage[this.images] = JSON.stringify(images); + _set_categories: function(categories){ + localStorage[this.categories] = JSON.stringify(categories); }, add_product: function(products){ - var stored_images = this._get_images(); var stored_products = this._get_products(); + var stored_categories = this._get_categories(); if(!products instanceof Array){ products = [products]; } for(var i = 0, len = products.length; i < len; i++){ var product = products[i]; - if(product.product_image_small){ - product = _.clone(product); - stored_images[product.id] = product.product_image_small; - delete product['product_image_small']; + var categ_id = product.pos_categ_id[0]; + if(!stored_categories[categ_id]){ + stored_categories[categ_id] = []; + } + stored_categories[categ_id].push(product.id); + var ancestors = this.get_category_ancestors_ids(categ_id) || []; + console.log('ancestors:',ancestors); + + for(var j = 0; j < ancestors.length; j++){ + if(! stored_categories[ancestors[j]]){ + stored_categories[ancestors[j]] = []; + } + stored_categories[ancestors[j]].push(product.id); } stored_products[product.id] = product; } - this._set_images(stored_images); this._set_products(stored_products); + this._set_categories(stored_categories); }, clear: function(done_callback){ localStorage.removeItem(this.products); - localStorage.removeItem(this.images); + localStorage.removeItem(this.categories); + if(done_callback){ + done_callback(); + } }, _count_props : function(obj){ if(obj.__count__){ @@ -373,9 +480,6 @@ function openerp_pos_db(instance, module){ get_product_count: function(result_callback){ result_callback(this._count_props(this._get_products())); }, - get_image_count: function(result_callback){ - result_callback(this._count_props(this._get_images())); - }, get_product_by_id: function(id, result_callback){ var products = this._get_products(); result_callback( products[id] ); @@ -400,66 +504,55 @@ function openerp_pos_db(instance, module){ } result_callback(undefined); }, - get_product_by_category: function(category, result_callback){ - var products = this._get_products(); + get_product_by_category: function(category_id, result_callback){ + var stored_categories = this._get_categories(); + var stored_products = this._get_products(); + var product_ids = stored_categories[category_id]; var list = []; - for(var i in products){ - if( products[i] && products[i].category === category){ - list.push(products[i]); - } + for(var i = 0, len = product_ids.length; i < len; i++){ + list.push(stored_products[product_ids[i]]); } result_callback(list); }, - get_product_image: function(product, result_callback){ - var images = this._get_images(); - result_callback(images[product.id]); - }, - search_product: function(fields, query, result_callback){ - var products = this._get_products(); - var list = []; - if(typeof query !== 'string'){ - if(query.toString){ - query = query.toString(); - }else{ - throw new Error('search_product: the query must be a string or must be convertible to string'); + search_product_in_category: function(category_id, fields, query, result_callback){ + var self = this; + this.get_product_by_category(category_id, function(products){ + var list = []; + + query = query.toString().toLowerCase(); + + if(!(fields instanceof Array)){ + fields = [fields]; } - } - - query = query.toLowerCase(); - - if(!(fields instanceof Array)){ - fields = [fields]; - } - for(var i in products){ - for(var j = 0, jlen = fields.length; j < jlen; j++){ - var field = products[i][fields[j]]; - if(field === null || field === undefined){ - continue; - } - if(typeof field !== 'string'){ - if(field.toString){ - field = field.toString(); - }else{ + for(var i in products){ + for(var j = 0, jlen = fields.length; j < jlen; j++){ + var field = products[i][fields[j]]; + if(field === null || field === undefined){ continue; } - } - if(field.toLowerCase().indexOf(query) != -1){ - list.push(products[i]); - break; + if(typeof field !== 'string'){ + if(field.toString){ + field = field.toString(); + }else{ + continue; + } + } + if(field.toLowerCase().indexOf(query) != -1){ + list.push(products[i]); + break; + } } } - } - result_callback(list); + result_callback(list); + }); }, - for_all_products: function(callback){ - var products = this._get_products(); - for(var i in products){ - var ret = callback(products[i]); - if(ret === 'break'){ - break; - } - } + add_order: function(order,done_callback){ }, + remove_order: function(order_id, done_callback){ + }, + get_orders: function(result_callback){ + }, + }); window.PosLS = module.PosLS; diff --git a/addons/point_of_sale/static/src/js/pos_devices.js b/addons/point_of_sale/static/src/js/pos_devices.js index c7d96eccffc..d39597c0b02 100644 --- a/addons/point_of_sale/static/src/js/pos_devices.js +++ b/addons/point_of_sale/static/src/js/pos_devices.js @@ -277,41 +277,37 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal this.action_callback[action] = undefined; } }, - - + // returns the checksum of the ean, or -1 if the ean has not the correct length, ean must be a string + ean_checksum: function(ean){ + var code = ean.split(''); + if(code.length !== 13){ + return -1; + } + var oddsum = 0, evensum = 0, total = 0; + code = code.reverse().splice(1); + for(var i = 0; i < code.length; i++){ + if(i % 2 == 0){ + oddsum += Number(code[i]); + }else{ + evensum += Number(code[i]); + } + } + total = oddsum * 3 + evensum; + return Number((10 - total % 10) % 10); + }, // returns true if the ean is a valid EAN codebar number by checking the control digit. // ean must be a string check_ean: function(ean){ - var code = ean.split(''); - for(var i = 0; i < code.length; i++){ - code[i] = Number(code[i]); + return ean_checksum(ean) === Number(ean[ean.length-1]); + }, + // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. + sanitize_ean:function(ean){ + ean = ean.substr(0,13); + + for(var n = 0, count = (13 - ean.length); n < count; n++){ + ean = ean + '0'; } - var st1 = code.slice(); - var st2 = st1.slice(0,st1.length-1).reverse(); - // some EAN13 barcodes have a length of 12, as they start by 0 - while (st2.length < 12) { - st2.push(0); - } - var countSt3 = 1; - var st3 = 0; - $.each(st2, function() { - if (countSt3%2 === 1) { - st3 += this; - } - countSt3 ++; - }); - st3 *= 3; - var st4 = 0; - var countSt4 = 1; - $.each(st2, function() { - if (countSt4%2 === 0) { - st4 += this; - } - countSt4 ++; - }); - var st5 = st3 + st4; - var cd = (10 - (st5%10)) % 10; - return code[code.length-1] === cd; + return ean.substr(0,12) + this.ean_checksum(ean); }, // attempts to interpret an ean (string encoding an ean) @@ -334,6 +330,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal type:'unknown', // prefix:'', ean:ean, + base_ean: ean, id:'', value: 0, unit: 'none', @@ -355,18 +352,22 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal parse_result.type = 'error'; } else if( match_prefix(this.price_prefix_set,'price')){ parse_result.id = ean.substring(0,7); + parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.value = Number(ean.substring(7,12))/100.0; parse_result.unit = 'euro'; } else if( match_prefix(this.weight_prefix_set,'weight')){ parse_result.id = ean.substring(0,7); parse_result.value = Number(ean.substring(7,12))/1000.0; + parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.unit = 'Kg'; } else if( match_prefix(this.client_prefix_set,'client')){ parse_result.id = ean.substring(0,7); + parse_result.unit = 'Kg'; } else if( match_prefix(this.cashier_prefix_set,'cashier')){ parse_result.id = ean.substring(0,7); } else if( match_prefix(this.discount_prefix_set,'discount')){ parse_result.id = ean.substring(0,7); + parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.value = Number(ean.substring(7,12))/100.0; parse_result.unit = '%'; } else { diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js index b4684c037b2..ee0c770e6e6 100644 --- a/addons/point_of_sale/static/src/js/pos_models.js +++ b/addons/point_of_sale/static/src/js/pos_models.js @@ -64,17 +64,16 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal initialize: function(session, attributes) { Backbone.Model.prototype.initialize.call(this, attributes); var self = this; + this.session = session; this.dao = new module.LocalStorageDAO(); // used to store the order's data on the Hard Drive this.ready = $.Deferred(); // used to notify the GUI that the PosModel has loaded all resources this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time - //this.build_tree = _.bind(this.build_tree, this); // ??? - this.session = session; - this.categories = {}; - this.root_category = null; - this.weightable_categories = []; // a flat list of all categories that directly contain weightable products + this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes - this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy - this.db = new module.PosLS(); // a database used to store the products and product images + this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy + this.db = new module.PosLS(); // a database used to store the products and categories + + window.db = this.db; // pos settings this.use_scale = false; @@ -267,30 +266,14 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return session_data_def; }); - // associate the products with their categories - var prod_process_def = $.when(cat_def, session_def) - .pipe(function(){ - var product_list = self.get('product_list'); - var categories = self.get('categories'); - var cat_by_id = {}; - for(var i = 0; i < categories.length; i++){ - cat_by_id[categories[i].id] = categories[i]; - } - //set the parent in the category - for(var i = 0; i < categories.length; i++){ - categories[i].parent_category = cat_by_id[categories[i].parent_id[0]]; - } - for(var i = 0; i < product_list.length; i++){ - product_list[i].pos_category = cat_by_id[product_list[i].pos_categ_id[0]]; - } - }); - // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. - $.when(pack_def, cat_def, user_def, users_def, uom_def, session_def, tax_def, prod_process_def, user_def, this.flush()) + $.when(pack_def, cat_def, user_def, users_def, uom_def, session_def, tax_def, user_def, this.flush()) .then(function(){ - self.build_categories(); + self.db.clear(); + self.db.add_categories(self.get('categories')); + self.db.add_product(self.get('product_list')); self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); - //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging + self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging self.ready.resolve(); },function(){ //we failed to load some backend data, or the backend was badly configured. @@ -325,7 +308,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // order and a valid selected order on_removed_order: function(removed_order){ if( this.get('orders').isEmpty()){ - this.add_and_select_order(new module.Order({ pos: this })); + this.add_new_order(); } if( this.get('selectedOrder') === removed_order){ this.set({ selectedOrder: this.get('orders').last() }); @@ -340,13 +323,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }); }, - add_and_select_order: function(newOrder) { - (this.get('orders')).add(newOrder); - return this.set({ - selectedOrder: newOrder - }); + //creates a new empty order and sets it as the current order + add_new_order: function(){ + var order = new module.Order({pos:this}); + this.get('orders').add(order); + this.set('selectedOrder', order); }, - + // attemps to send all pending orders ( stored in the DAO ) to the server. // it will do it one by one, and remove the successfully sent ones from the DAO once // it has been confirmed that they have been received. @@ -390,119 +373,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }); }, - // this adds several properties to the categories in order to make it easier to diplay them - // fields added include the list of product relevant to each category, list of child categories, - // list of ancestors, etc. - build_categories : function(){ - var categories = this.get('categories'); - var products = this.get('product_list'); - - //append the content of array2 into array1 - function append(array1, array2){ - for(var i = 0, len = array2.length; i < len; i++){ - array1.push(array2[i]); + scan_product: function(parsed_ean){ + var self = this; + var def = new $.Deferred(); + this.db.get_product_by_ean13(parsed_ean.base_ean, function(product){ + var selectedOrder = this.get('selectedOrder'); + if(!product){ + def.reject('product-not-found: '+parsed_ean.base_ean); + return; } - } - - function appendSet(set1, set2){ - for(key in set2){ - set1[key] = set2[key]; + if(parsed_ean.type === 'price'){ + selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value}); + }else if(parsed_ean.type === 'weight'){ + selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false}); + }else{ + selectedOrder.addProduct(new module.Product(product)); } - } - - var categories_by_id = {}; - for(var i = 0; i < categories.length; i++){ - categories_by_id[categories[i].id] = categories[i]; - } - this.categories_by_id = categories_by_id; - - var root_category = { - name : 'Root', - id : 0, - parent : null, - childrens : [], - }; - - // add parent and childrens field to categories, find root_categories - for(var i = 0; i < categories.length; i++){ - var cat = categories[i]; - - cat.parent = categories_by_id[cat.parent_id[0]]; - if(!cat.parent){ - root_category.childrens.push(cat); - cat.parent = root_category; - } - - cat.childrens = []; - for(var j = 0; j < cat.child_id.length; j++){ - cat.childrens.push(categories_by_id[ cat.child_id[j] ]); - } - } - - categories.push(root_category); - - // set some default fields for next steps - for(var i = 0; i < categories.length; i++){ - var cat = categories[i]; - - cat.product_list = []; //list of all products in the category - cat.product_set = {}; // [product.id] === true if product is in category - cat.weightable_product_list = []; - cat.weightable_product_set = {}; - cat.weightable = false; //true if directly contains weightable products - } - - this.root_category = root_category; - - //we add the products to the categories. - for(var i = 0, len = products.length; i < len; i++){ - var product = products[i]; - var cat = categories_by_id[product.pos_categ_id[0]]; - if(cat){ - cat.product_list.push(product); - cat.product_set[product.id] = true; - if(product.to_weight){ - cat.weightable_product_list.push(product); - cat.weightable_product_set[product.id] = true; - cat.weightable = true; - } - } - } - - // we build a flat list of all categories that directly contains weightable products - this.weightable_categories = []; - for(var i = 0, len = categories.length; i < len; i++){ - var cat = categories[i]; - if(cat.weightable){ - this.weightable_categories.push(cat); - } - } - - // add ancestor field to categories, contains the list of parents of parents, from root to parent - function make_ancestors(cat, ancestors){ - cat.ancestors = ancestors.slice(0); - ancestors.push(cat); - - for(var i = 0; i < cat.childrens.length; i++){ - make_ancestors(cat.childrens[i], ancestors.slice(0)); - } - } - - //add the products of the subcategories to the parent categories - function make_products(cat){ - for(var i = 0; i < cat.childrens.length; i++){ - make_products(cat.childrens[i]); - - append(cat.product_list, cat.childrens[i].product_list); - append(cat.weightable_product_list, cat.childrens[i].weightable_product_list); - - appendSet(cat.product_set, cat.childrens[i].product_set); - appendSet(cat.weightable_product_set, cat.childrens[i].weightable_product_set); - } - } - - make_ancestors(root_category,[]); - make_products(root_category); + def.resolve(); + }); + return def; }, }); diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js index 07f1071fa6d..310d80a3603 100644 --- a/addons/point_of_sale/static/src/js/pos_screens.js +++ b/addons/point_of_sale/static/src/js/pos_screens.js @@ -159,17 +159,20 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if // there's an error. barcode_product_action: function(ean){ - if(this.pos_widget.scan_product(ean)){ - this.pos.proxy.scan_item_success(ean); - if(this.barcode_product_screen){ - this.pos_widget.screen_selector.set_current_screen(this.barcode_product_screen); - } - }else{ - this.pos.proxy.scan_item_error_unrecognized(ean); - if(this.barcode_product_error_popup && this.pos_widget.screen_selector.get_user_mode() !== 'cashier'){ - this.pos_widget.screen_selector.show_popup(this.barcode_product_error_popup); - } - } + var self = this; + this.pos_widget.scan_product(ean) + .done(function(){ + self.pos.proxy.scan_item_success(ean); + if(self.barcode_product_screen){ + self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen); + } + }) + .fail(function(){ + self.pos.proxy.scan_item_error_unrecognized(ean); + if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){ + self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup); + } + }); }, // what happens when a cashier id barcode is scanned. diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js index a30a0ebe521..34de4cf567f 100644 --- a/addons/point_of_sale/static/src/js/pos_widgets.js +++ b/addons/point_of_sale/static/src/js/pos_widgets.js @@ -396,29 +396,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // changes the category. if undefined, sets to root category set_category : function(category){ + var db = this.pos.db; if(!category){ - this.category = this.pos.root_category; + this.category = db.get_category_by_id(db.root_category_id); }else{ this.category = category; } - this.breadcrumb = []; - for(var i = 1; i < this.category.ancestors.length; i++){ - this.breadcrumb.push(this.category.ancestors[i]); + var ancestors_ids = db.get_category_ancestors_ids(this.category.id); + for(var i = 1; i < ancestors_ids.length; i++){ + this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i])); } - if(this.category !== this.pos.root_category){ + if(this.category.id !== db.root_category_id){ this.breadcrumb.push(this.category); } - if(this.product_type === 'weightable'){ - this.subcategories = []; - for(var i = 0; i < this.category.childrens.length; i++){ - if(this.category.childrens[i].weightable_product_list.length > 0){ - this.subcategories.push( this.category.childrens[i]); - } - } - }else{ - this.subcategories = this.category.childrens || []; - } + this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id)); }, renderElement: function(){ @@ -443,7 +435,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa $(button).appendTo(this.$('.category-list')).click(function(event){ var id = category.id; - var cat = self.pos.categories_by_id[id]; + var cat = self.pos.db.get_category_by_id(id); self.set_category(cat); self.renderElement(); self.search_and_categories(cat); @@ -452,7 +444,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // breadcrumb click actions this.$(".oe-pos-categories-list a").click(function(event){ var id = $(event.target).data("category-id"); - var category = self.pos.categories_by_id[id]; + var category = self.pos.db.get_category_by_id(id); self.set_category(category); self.renderElement(); self.search_and_categories(category); @@ -475,50 +467,39 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // filters the products, and sets up the search callbacks search_and_categories: function(category){ var self = this; - - var all_products = this.pos.get('product_list'); - var all_packages = this.pos.get('product.packaging'); // find all products belonging to the current category - var products = []; - if(this.product_type === 'weightable'){ - products = all_products.filter( function(product){ - return self.category.weightable_product_set[product.id]; - }); - }else{ - products = all_products.filter( function(product){ - return self.category.product_set[product.id]; - }); - } + this.pos.db.get_product_by_category(this.category.id, function(products){ + // product lists watch for reset events on 'products' to re-render. + // FIXME that means all productlist widget re-render... even the hidden ones ! + self.pos.get('products').reset(products); + }); - // product lists watch for reset events on 'products' to re-render. - // FIXME that means all productlist widget re-render... even the hidden ones ! - this.pos.get('products').reset(products); - - // find all the products whose name match the query in the searchbox + // filter the products according to the search string this.$('.searchbox input').keyup(function(){ - var results, search_str; - search_str = $(this).val().toLowerCase(); - if(search_str){ - results = products.filter( function(p){ - return p.name.toLowerCase().indexOf(search_str) != -1 || - (p.ean13 && p.ean13.indexOf(search_str) != -1); + query = $(this).val().toLowerCase(); + if(query){ + self.pos.db.search_product_in_category(self.category.id, ['name','ean13'], query, function(products){ + self.pos.get('products').reset(products); + self.$('.search-clear').fadeIn(); }); - self.$element.find('.search-clear').fadeIn(); }else{ - results = products; - self.$element.find('.search-clear').fadeOut(); + self.pos.db.get_product_by_category(self.category.id, function(products){ + self.pos.get('products').reset(products); + self.$('.search-clear').fadeOut(); + }); } - self.pos.get('products').reset(results); - }); - this.$('.searchbox input').click(function(){ }); + this.$('.searchbox input').click(function(){}); //Why ??? + //reset the search when clicking on reset this.$('.search-clear').click(function(){ - self.pos.get('products').reset(products); - self.$('.searchbox input').val('').focus(); - self.$('.search-clear').fadeOut(); + self.pos.db.get_product_by_category(self.category.id, function(products){ + self.pos.get('products').reset(products); + self.$('.searchbox input').val('').focus(); + self.$('.search-clear').fadeOut(); + }); }); }, }); @@ -693,7 +674,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.build_currency_template(); self.renderElement(); - self.$('.neworder-button').click(_.bind(self.create_new_order, self)); + self.$('.neworder-button').click(function(){ + self.pos.add_new_order(); + }); //when a new order is created, add an order button widget self.pos.get('orders').bind('add', function(new_order){ @@ -721,7 +704,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session'); } - self.$('.loader').animate({opacity:0},3000,'swing',function(){self.$('.loader').hide();}); + self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();}); self.$('.loader img').hide(); if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ @@ -851,51 +834,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, - //FIXME this method is probably not at the right place ... - scan_product: function(parsed_ean){ - var selectedOrder = this.pos.get('selectedOrder'); - var scannedProductModel = this.get_product_by_ean(parsed_ean); - if (!scannedProductModel){ - return false; - } else { - if(parsed_ean.type === 'price'){ - selectedOrder.addProduct(new module.Product(scannedProductModel), { price:parsed_ean.value}); - }else if(parsed_ean.type === 'weight'){ - selectedOrder.addProduct(new module.Product(scannedProductModel), { quantity:parsed_ean.value, merge:false}); - }else{ - selectedOrder.addProduct(new module.Product(scannedProductModel)); - } - return true; - } - }, - - get_product_by_ean: function(parsed_ean) { - var allProducts = this.pos.get('product_list'); - var allPackages = this.pos.get('product.packaging'); - var scannedProductModel = undefined; - - if (parsed_ean.type === 'price' || parsed_ean.type === 'weight') { - var itemCode = parsed_ean.id; - var scannedPackaging = _.detect(allPackages, function(pack) { - return pack.ean && pack.ean.substring(0,7) === itemCode; - }); - if (scannedPackaging) { - scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];}); - }else{ - scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 && (pc.ean13.substring(0,7) === parsed_ean.id);}); - } - } else if(parsed_ean.type === 'unit'){ - scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === parsed_ean.ean;}); //TODO DOES NOT SCALE - } - return scannedProductModel; - }, - // creates a new order, and add it to the list of orders. - create_new_order: function() { - var new_order; - new_order = new module.Order({ pos: this.pos }); - this.pos.get('orders').add(new_order); - this.pos.set({ selectedOrder: new_order }); - }, changed_pending_operations: function () { var self = this; this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length); From 78d8b5d39498beb45231f8b0bcd093f670ca41b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 9 Aug 2012 21:55:55 +0200 Subject: [PATCH 030/123] [WIP] point_of_sale: indexeddb : products and categories are no longer in memory bzr revid: fva@openerp.com-20120809195555-oluxe7xe8k2ugcoi --- addons/point_of_sale/static/src/js/pos_db.js | 2 +- .../point_of_sale/static/src/js/pos_models.js | 30 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/addons/point_of_sale/static/src/js/pos_db.js b/addons/point_of_sale/static/src/js/pos_db.js index 59ea0da1684..d660b130dd2 100644 --- a/addons/point_of_sale/static/src/js/pos_db.js +++ b/addons/point_of_sale/static/src/js/pos_db.js @@ -429,7 +429,7 @@ function openerp_pos_db(instance, module){ _set_categories: function(categories){ localStorage[this.categories] = JSON.stringify(categories); }, - add_product: function(products){ + add_products: function(products){ var stored_products = this._get_products(); var stored_categories = this._get_categories(); diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js index ee0c770e6e6..b6b58180f43 100644 --- a/addons/point_of_sale/static/src/js/pos_models.js +++ b/addons/point_of_sale/static/src/js/pos_models.js @@ -72,6 +72,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy this.db = new module.PosLS(); // a database used to store the products and categories + this.db.clear(); window.db = this.db; @@ -98,12 +99,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal 'products': new module.ProductCollection(), 'cashRegisters': null, - 'product_list': null, // the list of all products, does not change. 'bank_statements': null, 'taxes': null, 'pos_session': null, 'pos_config': null, - 'categories': null, 'selectedOrder': undefined, }); @@ -139,10 +138,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal self.set('currency',currencies[0]); }); - var cat_def = fetch('pos.category', ['id','name', 'parent_id', 'child_id', 'category_image_small']) - .pipe(function(result){ - return self.set({'categories': result}); - }); var uom_def = fetch( //unit of measure 'product.uom', @@ -213,18 +208,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal self.use_selfcheckout = pos_config.iface_self_checkout || false; self.use_cashbox = pos_config.iface_cashdrawer || false; - return shop_def = fetch('sale.shop',[], [['id','=',pos_config.shop_id[0]]]) + return fetch('sale.shop',[], [['id','=',pos_config.shop_id[0]]]) }).pipe(function(shops){ self.set('shop',shops[0]); + return fetch('pos.category', ['id','name', 'parent_id', 'child_id', 'category_image_small']) + }).pipe( function(categories){ + self.db.add_categories(categories); return fetch( 'product.product', - //context {pricelist: shop.pricelist_id[0]} ['name', 'list_price','price','pos_categ_id', 'taxes_id','product_image_small', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'], [['pos_categ_id','!=', false]], - {pricelist: shops[0].pricelist_id[0]} // context for price - ); - }).pipe( function(product_list){ - self.set({'product_list': product_list}); + {pricelist: self.get('shop').pricelist_id[0]} // context for price + ); + }).pipe( function(products){ + self.db.add_products(products); }); var bank_def = fetch( @@ -267,13 +264,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }); // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. - $.when(pack_def, cat_def, user_def, users_def, uom_def, session_def, tax_def, user_def, this.flush()) + $.when(pack_def, user_def, users_def, uom_def, session_def, tax_def, user_def, this.flush()) .then(function(){ - self.db.clear(); - self.db.add_categories(self.get('categories')); - self.db.add_product(self.get('product_list')); self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); - self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging + //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging self.ready.resolve(); },function(){ //we failed to load some backend data, or the backend was badly configured. @@ -286,7 +280,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal log_loaded_data: function(){ console.log('PosModel data has been loaded:'); console.log('PosModel: categories:',this.get('categories')); - console.log('PosModel: product_list:',this.get('product_list')); console.log('PosModel: units:',this.get('units')); console.log('PosModel: bank_statements:',this.get('bank_statements')); console.log('PosModel: journals:',this.get('journals')); @@ -300,7 +293,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal console.log('PosModel: user_list:',this.get('user_list')); console.log('PosModel: user:',this.get('user')); console.log('PosModel.session:',this.session); - console.log('PosModel.categories:',this.categories); console.log('PosModel end of data log.'); }, From 760939d83f4d51f56e8d4da21501116ac1bceef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Fri, 10 Aug 2012 15:58:32 +0200 Subject: [PATCH 031/123] [IMP] point_of_sale: IndexedDB: the LocalStorage DB is working now bzr revid: fva@openerp.com-20120810135832-7xb3yq0r1p829nv1 --- addons/point_of_sale/static/src/js/pos_db.js | 124 ++++++++++++------- 1 file changed, 80 insertions(+), 44 deletions(-) diff --git a/addons/point_of_sale/static/src/js/pos_db.js b/addons/point_of_sale/static/src/js/pos_db.js index d660b130dd2..7b252bb8c41 100644 --- a/addons/point_of_sale/static/src/js/pos_db.js +++ b/addons/point_of_sale/static/src/js/pos_db.js @@ -1,15 +1,20 @@ /** + * WIP : AS OF THIS COMMIT, THE INDEXEDB BACKEND IS IN A NON WORKING STATE + * * This file contains an IndexedDB (html5 database) backend for the Point Of Sale. * The IDB is used to store the list of products and especially their thumbnails, which may * not fit in the localStorage. * The IDB offers a big performance boost for products lookup, but not so much for search as * searching is not yet implemented in the IndexedDB API and must be performed manually in JS. + * + * this file also contains a localstorage implementation of the database to be used by browsers + * that don't support the indexeddb api. * */ function openerp_pos_db(instance, module){ // this is used for testing - window.gen_products = function(options){ + var gen_products = function(options){ options = options || {}; var count = options.count || 100; var imageSize = options.imageSize || 1800; @@ -60,10 +65,13 @@ function openerp_pos_db(instance, module){ }; //this is used for testing - window.log = function(x){ + var log = function(x){ console.log(x); }; + // window.gen_products = gen_products; + // window.log = log; + function importIndexedDB(){ if('indexedDB' in window){ return true; @@ -330,9 +338,12 @@ function openerp_pos_db(instance, module){ }); window.PosDB = module.PosDB; + /* PosLS is a LocalStorage based implementation of the point of sale database, + it performs better for few products, but does not scale beyond 500 products. + */ module.PosLS = instance.web.Class.extend({ - name: 'openerp_pos_ls', - limit: 50, + name: 'openerp_pos_ls', //the prefix of the localstorage data + limit: 100, // the maximum number of results returned by a search init: function(options){ options = options || {}; this.name = options.name || this.name; @@ -340,6 +351,10 @@ function openerp_pos_db(instance, module){ this.products = this.name + '_products'; this.categories = this.name + '_categories'; + //products cache put the data in memory to avoid roundtrips to the localstorage + this.products_cache = null; + this.categories_cache = null; + this.category_by_id = {}; this.root_category_id = 0; this.category_products = {}; @@ -347,6 +362,7 @@ function openerp_pos_db(instance, module){ this.category_childs = {}; this.category_parent = {}; }, + /* returns the category object from its id */ get_category_by_id: function(categ_id){ if(categ_id instanceof Array){ var list = []; @@ -363,15 +379,24 @@ function openerp_pos_db(instance, module){ return this.category_by_id[categ_id]; } }, + /* returns a list of the category's child categories ids, or an empty list + * if a category has no childs */ get_category_childs_ids: function(categ_id){ return this.category_childs[categ_id] || []; }, + /* returns a list of all ancestors (parent, grand-parent, etc) categories ids + * starting from the root category to the direct parent */ get_category_ancestors_ids: function(categ_id){ return this.category_ancestors[categ_id] || []; }, + /* returns the parent category's id of a category, or the root_category_id if no parent. + * the root category is parent of itself. */ get_category_parent_id: function(categ_id){ return this.category_parent[categ_id] || this.root_category_id; }, + /* adds categories definitions to the database. categories is a list of categories objects as + * returned by the openerp server. Categories must be inserted before the products or the + * product/ categories association may (will) not work properly */ add_categories: function(categories){ var self = this; if(!this.category_by_id[this.root_category_id]){ @@ -381,13 +406,11 @@ function openerp_pos_db(instance, module){ }; } for(var i=0, len = categories.length; i < len; i++){ - console.log('putting category : ',categories[i].id, categories[i].name); this.category_by_id[categories[i].id] = categories[i]; } for(var i=0, len = categories.length; i < len; i++){ var cat = categories[i]; var parent_id = cat.parent_id[0] || this.root_category_id; - console.log('category parent',cat.id, parent_id); this.category_parent[cat.id] = cat.parent_id[0]; if(!this.category_childs[parent_id]){ this.category_childs[parent_id] = []; @@ -407,7 +430,11 @@ function openerp_pos_db(instance, module){ } make_ancestors(this.root_category_id, []); }, + /* this internal method returns from disc a dictionary associating ids to the products */ _get_products: function(){ + if(this.products_cache){ + return this.products_cache; + } var products = localStorage[this.products]; if(products){ return JSON.parse(products) || {}; @@ -415,10 +442,16 @@ function openerp_pos_db(instance, module){ return {}; } }, + /* this internal method saves a dictionary associating ids to product to the disc */ _set_products: function(products){ localStorage[this.products] = JSON.stringify(products); + this.products_cache = products; }, + /* this internal method returns from disc a dictionary associating ids to the categories */ _get_categories: function(){ + if(this.categories_cache){ + return this.categories_cache; + } var categories = localStorage[this.categories]; if(categories){ return JSON.parse(categories) || {}; @@ -426,8 +459,10 @@ function openerp_pos_db(instance, module){ return {}; } }, + /* this internal method saves to disc an array associating ids to the categories */ _set_categories: function(categories){ localStorage[this.categories] = JSON.stringify(categories); + this.categories_cache = categories; }, add_products: function(products){ var stored_products = this._get_products(); @@ -444,7 +479,6 @@ function openerp_pos_db(instance, module){ } stored_categories[categ_id].push(product.id); var ancestors = this.get_category_ancestors_ids(categ_id) || []; - console.log('ancestors:',ancestors); for(var j = 0; j < ancestors.length; j++){ if(! stored_categories[ancestors[j]]){ @@ -457,6 +491,7 @@ function openerp_pos_db(instance, module){ this._set_products(stored_products); this._set_categories(stored_categories); }, + /* removes all the data from the database. TODO : being able to selectively remove data */ clear: function(done_callback){ localStorage.removeItem(this.products); localStorage.removeItem(this.categories); @@ -464,18 +499,15 @@ function openerp_pos_db(instance, module){ done_callback(); } }, + /* this internal methods returns the count of properties in an object. */ _count_props : function(obj){ - if(obj.__count__){ - return obj.__count__; - }else{ - var count = 0; - for(var prop in obj){ - if(obj.hasOwnProperty(prop)){ - count++; - } + var count = 0; + for(var prop in obj){ + if(obj.hasOwnProperty(prop)){ + count++; } - return count; } + return count; }, get_product_count: function(result_callback){ result_callback(this._count_props(this._get_products())); @@ -509,50 +541,54 @@ function openerp_pos_db(instance, module){ var stored_products = this._get_products(); var product_ids = stored_categories[category_id]; var list = []; - for(var i = 0, len = product_ids.length; i < len; i++){ + for(var i = 0, len = Math.min(product_ids.length,this.limit); i < len; i++){ list.push(stored_products[product_ids[i]]); } result_callback(list); }, + /* returns as a parameter of the result_callback function a list of products with : + * - a category that is or is a child of category_id, + * - a field in fields that contains a value that contains the query + * If a search is started before the previous has returned, the previous search may be cancelled + * (and the corresponding result_callback never called) + */ search_product_in_category: function(category_id, fields, query, result_callback){ var self = this; - this.get_product_by_category(category_id, function(products){ - var list = []; + var stored_categories = this._get_categories(); + var stored_products = this._get_products(); + var product_ids = stored_categories[category_id]; + var list = []; + var count = 0; + + query = query.toString().toLowerCase(); - query = query.toString().toLowerCase(); - - if(!(fields instanceof Array)){ - fields = [fields]; - } - for(var i in products){ - for(var j = 0, jlen = fields.length; j < jlen; j++){ - var field = products[i][fields[j]]; - if(field === null || field === undefined){ - continue; - } - if(typeof field !== 'string'){ - if(field.toString){ - field = field.toString(); - }else{ - continue; - } - } - if(field.toLowerCase().indexOf(query) != -1){ - list.push(products[i]); - break; - } + if(!(fields instanceof Array)){ + fields = [fields]; + } + for(var i = 0, len = product_ids.length; i < len && count < this.limit; i++){ + var product = stored_products[product_ids[i]]; + for(var j = 0, jlen = fields.length; j < jlen; j++){ + var field = product[fields[j]]; + if(field === null || field === undefined){ + continue; + } + field = field.toString().toLowerCase(); + if(field.indexOf(query) != -1){ + list.push(product); + count++; + break; } } - result_callback(list); - }); + } + result_callback(list); }, + // TODO move the order storage from the crappy DAO in pos_models.js add_order: function(order,done_callback){ }, remove_order: function(order_id, done_callback){ }, get_orders: function(result_callback){ }, - }); window.PosLS = module.PosLS; From 8759e49b8355eec82709aee70405ecc86f20be3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Fri, 10 Aug 2012 16:19:30 +0200 Subject: [PATCH 032/123] [FIX] point_of_sale: reverting changes on the pos's views bzr revid: fva@openerp.com-20120810141930-3xkh9ny0splzrs75 --- addons/point_of_sale/point_of_sale_view.xml | 62 +++++++++++++------ .../wizard/pos_session_opening.xml | 2 +- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 9aa9c095f65..6a43cec39be 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -3,6 +3,10 @@ + Others @@ -151,6 +155,9 @@ + + + Products @@ -163,7 +170,10 @@ You must define a Product for everything you buy or sell. Products can be raw materials, stockable products, consumables or services. The Product form contains detailed information about your products related to procurement logistics, sales price, product category, suppliers and so on. - + Sale lines @@ -663,8 +673,11 @@ tree,form + + Payment Methods @@ -676,6 +689,10 @@ Payment methods are defined by accounting journals having the field Payment Method checked. + POS Sales Lines @@ -699,6 +716,9 @@ + + + Invoices @@ -709,6 +729,8 @@ [('origin','like','POS')] + Start Point of Sale @@ -826,6 +848,11 @@ + + + + + pos.order.list.select pos.order @@ -993,6 +1034,7 @@ + pos.ean_wizard @@ -1008,24 +1050,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/addons/point_of_sale/wizard/pos_session_opening.xml b/addons/point_of_sale/wizard/pos_session_opening.xml index ec7fae9c07e..226edf70b62 100644 --- a/addons/point_of_sale/wizard/pos_session_opening.xml +++ b/addons/point_of_sale/wizard/pos_session_opening.xml @@ -28,7 +28,7 @@ pos.session.opening form form - inline + new From 246b47602002424c6ec45ed854251e99846d75b9 Mon Sep 17 00:00:00 2001 From: Tejas Tank Date: Mon, 13 Aug 2012 14:45:19 +0530 Subject: [PATCH 033/123] Workflow editor label. bzr revid: tta@openerp.com-20120813091519-lcxiemc2wojfx2d6 --- openerp/addons/base/ir/workflow/workflow_view.xml | 3 ++- openerp/addons/base/rng/view.rng | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/workflow/workflow_view.xml b/openerp/addons/base/ir/workflow/workflow_view.xml index c20eeaed085..084d66f720f 100644 --- a/openerp/addons/base/ir/workflow/workflow_view.xml +++ b/openerp/addons/base/ir/workflow/workflow_view.xml @@ -41,7 +41,7 @@ workflow diagram - + @@ -55,6 +55,7 @@ + diff --git a/openerp/addons/base/rng/view.rng b/openerp/addons/base/rng/view.rng index 8f7b2647237..0d6527e5972 100644 --- a/openerp/addons/base/rng/view.rng +++ b/openerp/addons/base/rng/view.rng @@ -181,12 +181,12 @@ - + - - - - + + + + From e79a418f9329015d9e0d8886edae47b2b2e7e1b7 Mon Sep 17 00:00:00 2001 From: "Rucha (Open ERP)" Date: Mon, 13 Aug 2012 16:22:37 +0530 Subject: [PATCH 034/123] [IMP,ADD]: crm: Improved and added mail messages for leads/opportunities bzr revid: rpa@tinyerp.com-20120813105237-14cnt886cgyqlncv --- addons/crm/crm_lead_demo.xml | 120 ++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index ea26a24cad8..a274dadfe01 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -49,7 +49,6 @@ Can you send details, - Want to know features and benifits to use the new software. @@ -550,53 +549,86 @@ Andrew - Kitchen design + Plan to buy a Laptop crm.lead - html - - email - sent - - - - Reply - crm.lead - - html - - comment - - - - Reply - crm.lead - - html - - - comment - - - - Reply - crm.lead - - html - - - comment - - - - Your inquiry - crm.lead - - html - + html + <![CDATA[Email0 inquiry]]><div><font size="2">Hello,</font></div><div><font size="2"><br></font></div><div><font size="2">I am interested in your company's product and I plan to buy a new laptop having latest technologies and affordable price.</font></div><div><font size="2">Can you please send me product catalogue?</font></div> email received - + + + + Re: Plan to buy a Laptop + crm.lead + + + plain + Dear Customer, +Thanks for showing interest in our products. +We have attached the catalogue, +We would like to know your interests, Let us know if we can call you for more details. + +Thanks + email + + sent + + + Re: Plan to buy a Laptop + crm.lead + + html + <div>Thanks for the information,</div><div>I will visit the store soon.</div> + + email + + received + + + Re: Plan to buy a Laptop + crm.lead + + html + <font color="#1f1f1f">Can you tell me if the store is open at 9:00 PM?</b></font> + + email + received + + + + Re: Plan to buy a Laptop + crm.lead + + plain + Yes, its open till 10:00 PM, you are welcome! + email + sent + + + + + Inquiry + crm.lead + + plain + Hello, +I am Jason from Le Club SARL, +I am intertested to attend Training organized in your company, +Can you send details, + email + received + + + + + Need Details + crm.lead + + plain + Want to know features and benifits to use the new software. + comment + From b6c254942ab491a2273b42c377f1cf4772b96073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 13 Aug 2012 16:43:55 +0200 Subject: [PATCH 035/123] [FIX] point_of_sale: images are working now bzr revid: fva@openerp.com-20120813144355-vgojt7llli8u9vye --- addons/point_of_sale/point_of_sale_demo.xml | 179 +++++++++++++------- addons/point_of_sale/static/src/css/pos.css | 7 +- 2 files changed, 124 insertions(+), 62 deletions(-) diff --git a/addons/point_of_sale/point_of_sale_demo.xml b/addons/point_of_sale/point_of_sale_demo.xml index 9c87693a566..8a17299882d 100644 --- a/addons/point_of_sale/point_of_sale_demo.xml +++ b/addons/point_of_sale/point_of_sale_demo.xml @@ -158,7 +158,124 @@ - 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aVK5ZUlhJ7RvgfrWEsHSl0OiONqx6lV/BNr/AMsrydf97B/oKxeW031ZqsxqdUirL4Lus/uL6PH+2p/pWbyxdGWsy7xIH8Gavj93fWmf9pWpf2b5lf2jH+URfBOrFvnv7Xb7I1NZb5ieYr+UtweCXwPP1A8do4/8auOXQW7ZnLMJPZF2Pwbp6ffnupPYuAP0FaxwVJdzOWNqvsatnpdjYDFtbJGf7wHP510RhGOyOeVSUt2WqsgKACgAoAKACgAoAKACgAoAKACgAFCAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAFCAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAFCAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAFCAKACgAoAKACgAoAKACgApAFMAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAQGkAZoAM0AGaADNABmgAzQAZoAM0AGaADNABmgAzQAZoAM0ALRcAzRcBM0AGaLgANAC5oAKYBQAUgA0AGaADNABmgAFABTAKACgAoAKACgAoAKACgAzSAM0AGaAAc0AFABmgAzQAZoQBmgAzQAZoAM0AFMAoADSAM0AJmgYZoELmi4wouAUXEFABQAUAApgAAD/2Q== + /9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK +CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU +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 +IIGCCBjBggYzBBHpgggrnGYzHrGIxH//2Q== 2.83 @@ -1024,65 +1141,5 @@ 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 - - - 1.55 - Lays Natural XXL 300g - - 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 - - - 1.48 - Lays Natural Light 170g - - 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 - - - 1.48 - Lays Natural Light 170g - - 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 - - - 0.39 - Lays Natural 45g - - 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 - - - - 7.60 - IJsboerke Chocolat 2.5L - - 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 - - - 7.60 - IJsboerke Mocha 2.5L - - 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 - - - 7.40 - IJsboerke Vanilla 2.5L - - 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 - - - 8.40 - IJsboerke Stracciatella 2.5L - - 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 - - - 7.60 - IJsboerke 2.5L White Lady - - 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 - - diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 4101296c87a..5191b91b24b 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -31,6 +31,7 @@ background: -ms-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); background: linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); } + .point-of-sale .darker-shadow-top{ position: absolute; top:0; @@ -42,6 +43,7 @@ background: -ms-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); background: linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); } + /* ********* The black loading screen ********* */ .point-of-sale .loader{ @@ -527,9 +529,11 @@ cursor: pointer; } +/* .point-of-sale .category-button .category-img img { height: 100px; } +*/ .point-of-sale .category-button .category-name { position: absolute; @@ -577,7 +581,8 @@ } .point-of-sale .product .product-img img { - height: 100px; + max-height: 100px; + max-width: 120px; } .point-of-sale .product .price-tag { From 48062d4cf1a509afb0c364027ca5f99dc8c85bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 13 Aug 2012 18:00:26 +0200 Subject: [PATCH 036/123] [IMP] point_of_sale: removed IndexeDB and asynchronous backend bzr revid: fva@openerp.com-20120813160026-04w28p3pojzvdlkj --- addons/point_of_sale/static/src/js/pos_db.js | 349 ++---------------- .../point_of_sale/static/src/js/pos_models.js | 34 +- .../static/src/js/pos_screens.js | 24 +- .../static/src/js/pos_widgets.js | 32 +- 4 files changed, 66 insertions(+), 373 deletions(-) diff --git a/addons/point_of_sale/static/src/js/pos_db.js b/addons/point_of_sale/static/src/js/pos_db.js index 0be3d058da4..5c790b45d9e 100644 --- a/addons/point_of_sale/static/src/js/pos_db.js +++ b/addons/point_of_sale/static/src/js/pos_db.js @@ -1,284 +1,5 @@ -/** - * WIP : AS OF THIS COMMIT, THE INDEXEDB BACKEND IS IN A NON WORKING STATE - * - * This file contains an IndexedDB (html5 database) backend for the Point Of Sale. - * The IDB is used to store the list of products and especially their thumbnails, which may - * not fit in the localStorage. - * The IDB offers a big performance boost for products lookup, but not so much for search as - * searching is not yet implemented in the IndexedDB API and must be performed manually in JS. - * - * this file also contains a localstorage implementation of the database to be used by browsers - * that don't support the indexeddb api. - * - */ function openerp_pos_db(instance, module){ - function importIndexedDB(){ - if('indexedDB' in window){ - return true; - }else if('webkitIndexedDB' in window){ - window.indexedDB = window.webkitIndexedDB; - window.IDBCursor = window.webkitIDBCursor; - window.IDBDatabase = window.webkitIDBDatabase; - window.IDBDatabaseError = window.webkitIDBDatabaseError; - window.IDBDatabaseException = window.webkitIDBDatabaseException; - window.IDBFactory = window.webkitIDBFactory; - window.IDBIndex = window.webkitIDBIndex; - window.IDBKeyRange = window.webkitIDBKeyRange; - window.IDBObjectSrore = window.webkitIDBOjbectStore; - window.IDBRequest = window.webkitIDBRequest; - window.IDBTransaction = window.webkitIDBTransaction; - }else if('mozIndexedDB' in window){ - window.indexedDB = window.mozIndexedDB; - }else{ - throw new Error("DATABASE ERROR: IndexedDB not implemented. Please upgrade your web browser"); - } - } - importIndexedDB(); - - module.PosDB = instance.web.Class.extend({ - - version: 1, - name: 'openerp_pos_db', - limit: 50, - - init: function(options){ - var self = this; - options = options || {}; - - this.version = options.version || this.version; - this.name = options.name || this.name; - - var open_request = indexedDB.open(this.name, this.version); - - open_request.onblocked = function(event){ - throw new Error("DATABASE ERROR: request blocked."); - }; - - // new API (firefox) - open_request.onupgradeneeded = function(event){ - console.log('using new API'); - var transaction = open_request.transaction; - self._upgrade_db(event.oldVersion,transaction); - }; - - open_request.onsuccess = function(event){ - console.log('db',self.db); - self.db = open_request.result; - var oldVersion = Number(self.db.version); - - if(oldVersion !== self.version){ - // if we get to this point, it means that onupgradeneeded hasn't been called. - // so we try the old api (webkit) - if(!self.db.setVersion){ - throw new Error("DATABASE ERROR: database API is broken. Your web browser is probably out of date"); - } - console.log('using old API'); - var version_request = self.db.setVersion(self.version); - version_request.onsuccess = function(event){ - var transaction = version_request.result; - self._upgrade_db(oldVersion, transaction); - }; - } - }; - }, - _upgrade_db: function(oldversion, transaction){ - console.log('upgrade_db:',oldversion); - - this.db = transaction.db; - var productStore = this.db.createObjectStore('products', {keyPath: 'id'}); - - productStore.createIndex('ean13', 'ean13', {unique:true}); - - productStore.createIndex('name', 'name', {unique:false}); - - productStore.createIndex('category','category', {unique:false}); - - var imageStore = this.db.createObjectStore('images', {keyPath: 'id'}); - - var categoryStore = this.db.createObjectStore('categories', {keypath:'name'}); - }, - - _add_data: function(store, data){ - var transaction = this.db.transaction([store], 'readwrite'); - var objectStore = transaction.objectStore(store); - var request = objectStore.put(data); - }, - /* adds a product to the database, replacing the original product if it is already present. - * the product must be provided as a json as returned by the openerp backend - */ - add_product: function(product){ - if(product instanceof Array){ - for(var i = 0, len = product.length; i < len; i++){ - this.add_product(product[i]); - } - }else{ - if(product.product_image_small){ - var image = product.product_image_small; - var product = _.clone(product); - delete product['product_image_small']; - this._add_data('images', { id: product.id, image:image}); - } - this._add_data('products',product); - } - }, - - /* removes all the data in the database ! */ - clear: function(done_callback){ - var transaction = this.db.transaction(['products','images'],'readwrite'); - transaction.objectStore('products').clear(); - transaction.objectStore('images').clear(); - if(done_callback){ - transaction.oncomplete = function(){ done_callback(); }; - } - }, - - get_product_count: function(result_callback){ - this.db.transaction('products').objectStore('products').count().onsuccess = function(event){ - result_callback(event.target.result); - }; - }, - - get_image_count: function(result_callback){ - this.db.transaction('images').objectStore('images').count().onsuccess = function(event){ - result_callback(event.target.result); - }; - }, - - /* fetches a product with an id of 'id', returns the product as the first parameter - * of the result_callback function - */ - get_product_by_id: function(id, result_callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - var request = objectStore.get(id); - request.onsuccess = function(event){ - if(result_callback){ - result_callback(event.target.result); - } - }; - }, - get_product_by_name: function(name, result_callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - var index = objectStore.index('name'); - index.get(name).onsuccess = function(event){ - result_callback(event.target.result); - }; - }, - /* fetches the product with the ean13 matching the one provided. returns the product - * as the first parameter of the result_callback function - */ - get_product_by_ean13: function(ean13, result_callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - var index = objectStore.index('ean13'); - index.get(ean13).onsuccess = function(event){ - result_callback(event.target.result); - }; - }, - /* fetches all products belonging to a category, and returns the list of products - * as the first paramter of the result_callback function - */ - get_product_by_category: function(category, result_callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - var index = objectStore.index('category'); - var list = []; - index.openCursor(IDBKeyRange.only(category)).onsuccess = function(event){ - var cursor = event.target.result; - if(cursor){ - list.push(cursor.value); - cursor.continue(); - }else{ - result_callback(list); - } - }; - }, - /* Fetches a picture associated with a product. - * product: This is a product json object as returned by the openerp backend. - * result_callback: a callback that gets the image data as parameter. - */ - get_product_image: function(product, result_callback){ - var transaction = this.db.transaction('images'); - var objectStore = transaction.objectStore('images'); - var request = objectStore.get(product.id); - request.onsuccess = function(event){ - result_callback(event.target.result.image); - } - }, - /* fields: a string or a list of string representing the list of the fields that - * will be searched. the values of those field must be a string. - * query: a string that will be matched in the content of one of the searched fields. - * result_callback: a function that will be called with a list of all the products with - * a field that match the query. - */ - search_product: function(fields,query,result_callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - var list = []; - if(!(fields instanceof Array)){ - fields = [ fields ]; - } - query = query.toLowerCase(); - objectStore.openCursor().onsuccess = function(event){ - var cursor = event.target.result; - if(cursor){ - var obj = cursor.value; - for(var i = 0, len = fields.length; i < len; i++){ - if(obj[fields[i]].toLowerCase().indexOf(query) != -1){ - list.push(obj); - break; - } - } - cursor.continue(); - }else{ - result_callback(list); - } - } - }, - /* the callback function will be called with all products as parameter, by increasing id. - * if the callback returns 'break' the iteration will stop - */ - for_all_products: function(callback){ - var transaction = this.db.transaction('products'); - var objectStore = transaction.objectStore('products'); - objectStore.openCursor().onsuccess = function(event){ - var cursor = event.target.result; - if(cursor){ - var ret = callback(cursor.value); - if(ret !== 'break'){ - cursor.continue(); - } - } - }; - }, - /* the callback function will be called with all products as parameter by increasing id. - * if the callback returns 'break', the iteration will stop - * if the callback returns a product object, it will be inserted in the db, possibly - * overwriting an existing product. The intended usage is for the callback to return a - * modified version version of the product with the same id. Anything else and you're on your own. - */ - modify_all_products: function(callback){ - var transaction = this.db.transaction('products','readwrite'); - var objectStore = transaction.objectStore('products'); - objectStore.openCursor().onsuccess = function(event){ - var cursor = event.target.result; - if(cursor){ - var ret = callback(cursor.value); - if(ret === undefined || ret === null){ - cursor.continue(); - }else if(ret === 'break'){ - return; - }else{ - objectStore.put(ret); - } - } - }; - }, - }); - window.PosDB = module.PosDB; - /* PosLS is a LocalStorage based implementation of the point of sale database, it performs better for few products, but does not scale beyond 500 products. */ @@ -372,7 +93,7 @@ function openerp_pos_db(instance, module){ make_ancestors(this.root_category_id, []); }, /* this internal method returns from disc a dictionary associating ids to the products */ - _get_products: function(){ + _load_products: function(){ if(this.products_cache){ return this.products_cache; } @@ -384,12 +105,12 @@ function openerp_pos_db(instance, module){ } }, /* this internal method saves a dictionary associating ids to product to the disc */ - _set_products: function(products){ + _save_products: function(products){ localStorage[this.products] = JSON.stringify(products); this.products_cache = products; }, /* this internal method returns from disc a dictionary associating ids to the categories */ - _get_categories: function(){ + _load_categories: function(){ if(this.categories_cache){ return this.categories_cache; } @@ -401,13 +122,13 @@ function openerp_pos_db(instance, module){ } }, /* this internal method saves to disc an array associating ids to the categories */ - _set_categories: function(categories){ + _save_categories: function(categories){ localStorage[this.categories] = JSON.stringify(categories); this.categories_cache = categories; }, add_products: function(products){ - var stored_products = this._get_products(); - var stored_categories = this._get_categories(); + var stored_products = this._load_products(); + var stored_categories = this._load_categories(); if(!products instanceof Array){ products = [products]; @@ -429,16 +150,13 @@ function openerp_pos_db(instance, module){ } stored_products[product.id] = product; } - this._set_products(stored_products); - this._set_categories(stored_categories); + this._save_products(stored_products); + this._save_categories(stored_categories); }, /* removes all the data from the database. TODO : being able to selectively remove data */ - clear: function(done_callback){ + clear: function(){ localStorage.removeItem(this.products); localStorage.removeItem(this.categories); - if(done_callback){ - done_callback(); - } }, /* this internal methods returns the count of properties in an object. */ _count_props : function(obj){ @@ -450,42 +168,27 @@ function openerp_pos_db(instance, module){ } return count; }, - get_product_count: function(result_callback){ - result_callback(this._count_props(this._get_products())); + get_product_by_id: function(id){ + return this._load_products()[id]; }, - get_product_by_id: function(id, result_callback){ - var products = this._get_products(); - result_callback( products[id] ); - }, - get_product_by_name: function(name, result_callback){ - var products = this._get_products(); - for(var i in products){ - if(products[i] && products[i].name === name){ - result_callback(products[i]); - return; - } - } - result_callback(undefined); - }, - get_product_by_ean13: function(ean13, result_callback){ - var products = this._get_products(); + get_product_by_ean13: function(ean13){ + var products = this._load_products(); for(var i in products){ if( products[i] && products[i].ean13 === ean13){ - result_callback(products[i]); - return; + return products[i]; } } - result_callback(undefined); + return undefined; }, - get_product_by_category: function(category_id, result_callback){ - var stored_categories = this._get_categories(); - var stored_products = this._get_products(); + get_product_by_category: function(category_id){ + var stored_categories = this._load_categories(); + var stored_products = this._load_products(); var product_ids = stored_categories[category_id]; var list = []; for(var i = 0, len = Math.min(product_ids.length,this.limit); i < len; i++){ list.push(stored_products[product_ids[i]]); } - result_callback(list); + return list; }, /* returns as a parameter of the result_callback function a list of products with : * - a category that is or is a child of category_id, @@ -493,10 +196,10 @@ function openerp_pos_db(instance, module){ * If a search is started before the previous has returned, the previous search may be cancelled * (and the corresponding result_callback never called) */ - search_product_in_category: function(category_id, fields, query, result_callback){ + search_product_in_category: function(category_id, fields, query){ var self = this; - var stored_categories = this._get_categories(); - var stored_products = this._get_products(); + var stored_categories = this._load_categories(); + var stored_products = this._load_products(); var product_ids = stored_categories[category_id]; var list = []; var count = 0; @@ -521,14 +224,14 @@ function openerp_pos_db(instance, module){ } } } - result_callback(list); + return list; }, // TODO move the order storage from the crappy DAO in pos_models.js - add_order: function(order,done_callback){ + add_order: function(order){ }, - remove_order: function(order_id, done_callback){ + remove_order: function(order_id){ }, - get_orders: function(result_callback){ + get_orders: function(){ }, }); diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js index 1fb8a0174fa..0f4d1b8aa64 100644 --- a/addons/point_of_sale/static/src/js/pos_models.js +++ b/addons/point_of_sale/static/src/js/pos_models.js @@ -216,7 +216,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal self.db.add_categories(categories); return fetch( 'product.product', - ['name', 'list_price','price','pos_categ_id', 'taxes_id','image', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'], + ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'], [['pos_categ_id','!=', false]], {pricelist: self.get('shop').pricelist_id[0]} // context for price ); @@ -367,23 +367,21 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal scan_product: function(parsed_ean){ var self = this; - var def = new $.Deferred(); - this.db.get_product_by_ean13(parsed_ean.base_ean, function(product){ - var selectedOrder = this.get('selectedOrder'); - if(!product){ - def.reject('product-not-found: '+parsed_ean.base_ean); - return; - } - if(parsed_ean.type === 'price'){ - selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value}); - }else if(parsed_ean.type === 'weight'){ - selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false}); - }else{ - selectedOrder.addProduct(new module.Product(product)); - } - def.resolve(); - }); - return def; + var product = this.db.get_product_by_ean13(parsed_ean.base_ean); + var selectedOrder = this.get('selectedOrder'); + + if(!product){ + return false; + } + + if(parsed_ean.type === 'price'){ + selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value}); + }else if(parsed_ean.type === 'weight'){ + selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false}); + }else{ + selectedOrder.addProduct(new module.Product(product)); + } + return true; }, }); diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js index 92665c5c381..659ee09c232 100644 --- a/addons/point_of_sale/static/src/js/pos_screens.js +++ b/addons/point_of_sale/static/src/js/pos_screens.js @@ -160,19 +160,17 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // there's an error. barcode_product_action: function(ean){ var self = this; - this.pos_widget.scan_product(ean) - .done(function(){ - self.pos.proxy.scan_item_success(ean); - if(self.barcode_product_screen){ - self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen); - } - }) - .fail(function(){ - self.pos.proxy.scan_item_error_unrecognized(ean); - if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){ - self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup); - } - }); + if(pos_widget.scan_product(ean)){ + self.pos.proxy.scan_item_success(ean); + if(self.barcode_product_screen){ + self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen); + } + }else{ + self.pos.proxy.scan_item_error_unrecognized(ean); + if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){ + self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup); + } + } }, // what happens when a cashier id barcode is scanned. diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js index 106a7ca7c4b..329bb05aca8 100644 --- a/addons/point_of_sale/static/src/js/pos_widgets.js +++ b/addons/point_of_sale/static/src/js/pos_widgets.js @@ -242,7 +242,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.renderElement(); }, get_image_url: function() { - return 'data:image/gif;base64,'+this.model.get('image'); + return 'web/binary/image?session_id='+instance.connection.session_id+'&model=product.product&field=image&id='+this.model.get('id')+'&cache=1800'; }, renderElement: function() { this._super(); @@ -471,25 +471,20 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var self = this; // find all products belonging to the current category - this.pos.db.get_product_by_category(this.category.id, function(products){ - // product lists watch for reset events on 'products' to re-render. - // FIXME that means all productlist widget re-render... even the hidden ones ! - self.pos.get('products').reset(products); - }); + var products = this.pos.db.get_product_by_category(this.category.id); + self.pos.get('products').reset(products); // filter the products according to the search string this.$('.searchbox input').keyup(function(){ query = $(this).val().toLowerCase(); if(query){ - self.pos.db.search_product_in_category(self.category.id, ['name','ean13'], query, function(products){ - self.pos.get('products').reset(products); - self.$('.search-clear').fadeIn(); - }); + var products = self.pos.db.search_product_in_category(self.category.id, ['name','ean13'], query); + self.pos.get('products').reset(products); + self.$('.search-clear').fadeIn(); }else{ - self.pos.db.get_product_by_category(self.category.id, function(products){ - self.pos.get('products').reset(products); - self.$('.search-clear').fadeOut(); - }); + var products = self.pos.db.get_product_by_category(self.category.id); + self.pos.get('products').reset(products); + self.$('.search-clear').fadeOut(); } }); @@ -497,11 +492,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa //reset the search when clicking on reset this.$('.search-clear').click(function(){ - self.pos.db.get_product_by_category(self.category.id, function(products){ - self.pos.get('products').reset(products); - self.$('.searchbox input').val('').focus(); - self.$('.search-clear').fadeOut(); - }); + var products = self.pos.db.get_product_by_category(self.category.id); + self.pos.get('products').reset(products); + self.$('.searchbox input').val('').focus(); + self.$('.search-clear').fadeOut(); }); }, }); From 8627aa0b28c643b45baecd369a3dac4d006b916f Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 14 Aug 2012 00:16:01 +0200 Subject: [PATCH 037/123] [IMP] point_of_sale add html5 manifest on /pos/app bzr revid: al@openerp.com-20120813221601-gfvwiwii6s1b785d --- addons/point_of_sale/__openerp__.py | 1 - addons/point_of_sale/controllers/__init__.py | 3 ++ .../{controllers.py => controllers/main.py} | 31 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 addons/point_of_sale/controllers/__init__.py rename addons/point_of_sale/{controllers.py => controllers/main.py} (66%) diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 3f78580b958..1b6a6f1390f 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -83,7 +83,6 @@ Main features: 'application': True, # Web client 'js': [ - 'static/lib/backbone/backbone-0.9.2.js', 'static/lib/mousewheel/jquery.mousewheel-3.0.6.js', 'static/src/js/pos_db.js', 'static/src/js/pos_models.js', diff --git a/addons/point_of_sale/controllers/__init__.py b/addons/point_of_sale/controllers/__init__.py new file mode 100644 index 00000000000..b5f0bcc9ec6 --- /dev/null +++ b/addons/point_of_sale/controllers/__init__.py @@ -0,0 +1,3 @@ +import main +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/point_of_sale/controllers.py b/addons/point_of_sale/controllers/main.py similarity index 66% rename from addons/point_of_sale/controllers.py rename to addons/point_of_sale/controllers/main.py index 33a96dacf84..7fb417e92c2 100644 --- a/addons/point_of_sale/controllers.py +++ b/addons/point_of_sale/controllers/main.py @@ -1,14 +1,44 @@ # -*- coding: utf-8 -*- import logging +import simplejson try: import openerp.addons.web.common.http as openerpweb + from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template except ImportError: import web.common.http as openerpweb class PointOfSaleController(openerpweb.Controller): _cp_path = '/pos' + @openerpweb.httprequest + def app(self, req, s_action=None, **kw): + js = "\n ".join('' % i for i in manifest_list(req, None, 'js')) + css = "\n ".join('' % i for i in manifest_list(req, None, 'css')) + + cookie = req.httprequest.cookies.get("instance0|session_id") + session_id = cookie.replace("%22","") + template = html_template.replace(' Date: Tue, 14 Aug 2012 00:16:45 +0200 Subject: [PATCH 038/123] [FIX] remove unused or duplicate files bzr revid: al@openerp.com-20120813221645-s8n8du7fy2sqhl0g --- .../static/lib/backbone/backbone-0.5.3.js | 1158 ------------- .../static/lib/backbone/backbone-0.9.2.js | 1431 ----------------- addons/point_of_sale/static/test.html | 22 - 3 files changed, 2611 deletions(-) delete mode 100644 addons/point_of_sale/static/lib/backbone/backbone-0.5.3.js delete mode 100644 addons/point_of_sale/static/lib/backbone/backbone-0.9.2.js delete mode 100644 addons/point_of_sale/static/test.html diff --git a/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.js b/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.js deleted file mode 100644 index 8958c798466..00000000000 --- a/addons/point_of_sale/static/lib/backbone/backbone-0.5.3.js +++ /dev/null @@ -1,1158 +0,0 @@ -// Backbone.js 0.5.3 -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone - -(function(){ - - // Initial Setup - // ------------- - - // Save a reference to the global object. - var root = this; - - // Save the previous value of the `Backbone` variable. - var previousBackbone = root.Backbone; - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both CommonJS and the browser. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.5.3'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; - - // For Backbone's purposes, jQuery or Zepto owns the `$` variable. - var $ = root.jQuery || root.Zepto; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will - // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a - // `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // ----------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may `bind` or `unbind` a callback function to an event; - // `trigger`-ing an event fires all callbacks in succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.bind('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - Backbone.Events = { - - // Bind an event, specified by a string name, `ev`, to a `callback` function. - // Passing `"all"` will bind the callback to all events fired. - bind : function(ev, callback, context) { - var calls = this._callbacks || (this._callbacks = {}); - var list = calls[ev] || (calls[ev] = []); - list.push([callback, context]); - return this; - }, - - // Remove one or many callbacks. If `callback` is null, removes all - // callbacks for the event. If `ev` is null, removes all bound callbacks - // for all events. - unbind : function(ev, callback) { - var calls; - if (!ev) { - this._callbacks = {}; - } else if (calls = this._callbacks) { - if (!callback) { - calls[ev] = []; - } else { - var list = calls[ev]; - if (!list) return this; - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] && callback === list[i][0]) { - list[i] = null; - break; - } - } - } - } - return this; - }, - - // Trigger an event, firing all bound callbacks. Callbacks are passed the - // same arguments as `trigger` is, apart from the event name. - // Listening for `"all"` passes the true event name as the first argument. - trigger : function(eventName) { - var list, calls, ev, callback, args; - var both = 2; - if (!(calls = this._callbacks)) return this; - while (both--) { - ev = both ? eventName : 'all'; - if (list = calls[ev]) { - for (var i = 0, l = list.length; i < l; i++) { - if (!(callback = list[i])) { - list.splice(i, 1); i--; l--; - } else { - args = both ? Array.prototype.slice.call(arguments, 1) : arguments; - callback[0].apply(callback[1] || this, args); - } - } - } - } - return this; - } - - }; - - // Backbone.Model - // -------------- - - // Create a new model, with defined attributes. A client id (`cid`) - // is automatically generated and assigned for you. - Backbone.Model = function(attributes, options) { - var defaults; - attributes || (attributes = {}); - if (defaults = this.defaults) { - if (_.isFunction(defaults)) defaults = defaults.call(this); - attributes = _.extend({}, defaults, attributes); - } - this.attributes = {}; - this._escapedAttributes = {}; - this.cid = _.uniqueId('c'); - this.set(attributes, {silent : true}); - this._changed = false; - this._previousAttributes = _.clone(this.attributes); - if (options && options.collection) this.collection = options.collection; - this.initialize(attributes, options); - }; - - // Attach all inheritable methods to the Model prototype. - _.extend(Backbone.Model.prototype, Backbone.Events, { - - // A snapshot of the model's previous attributes, taken immediately - // after the last `"change"` event was fired. - _previousAttributes : null, - - // Has the item been changed since the last `"change"` event? - _changed : false, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute : 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Return a copy of the model's `attributes` object. - toJSON : function() { - return _.clone(this.attributes); - }, - - // Get the value of an attribute. - get : function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape : function(attr) { - var html; - if (html = this._escapedAttributes[attr]) return html; - var val = this.attributes[attr]; - return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has : function(attr) { - return this.attributes[attr] != null; - }, - - // Set a hash of model attributes on the object, firing `"change"` unless you - // choose to silence it. - set : function(attrs, options) { - - // Extract attributes and options. - options || (options = {}); - if (!attrs) return this; - if (attrs.attributes) attrs = attrs.attributes; - var now = this.attributes, escaped = this._escapedAttributes; - - // Run validation. - if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // We're about to start triggering change events. - var alreadyChanging = this._changing; - this._changing = true; - - // Update attributes. - for (var attr in attrs) { - var val = attrs[attr]; - if (!_.isEqual(now[attr], val)) { - now[attr] = val; - delete escaped[attr]; - this._changed = true; - if (!options.silent) this.trigger('change:' + attr, this, val, options); - } - } - - // Fire the `"change"` event, if the model has been changed. - if (!alreadyChanging && !options.silent && this._changed) this.change(options); - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. `unset` is a noop if the attribute doesn't exist. - unset : function(attr, options) { - if (!(attr in this.attributes)) return this; - options || (options = {}); - var value = this.attributes[attr]; - - // Run validation. - var validObj = {}; - validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - // Remove the attribute. - delete this.attributes[attr]; - delete this._escapedAttributes[attr]; - if (attr == this.idAttribute) delete this.id; - this._changed = true; - if (!options.silent) { - this.trigger('change:' + attr, this, void 0, options); - this.change(options); - } - return this; - }, - - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. - clear : function(options) { - options || (options = {}); - var attr; - var old = this.attributes; - - // Run validation. - var validObj = {}; - for (attr in old) validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - this.attributes = {}; - this._escapedAttributes = {}; - this._changed = true; - if (!options.silent) { - for (attr in old) { - this.trigger('change:' + attr, this, void 0, options); - } - this.change(options); - } - return this; - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overriden, - // triggering a `"change"` event. - fetch : function(options) { - options || (options = {}); - var model = this; - var success = options.success; - options.success = function(resp, status, xhr) { - if (!model.set(model.parse(resp, xhr), options)) return false; - if (success) success(model, resp); - }; - options.error = wrapError(options.error, model, options); - return (this.sync || Backbone.sync).call(this, 'read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save : function(attrs, options) { - options || (options = {}); - if (attrs && !this.set(attrs, options)) return false; - var model = this; - var success = options.success; - options.success = function(resp, status, xhr) { - if (!model.set(model.parse(resp, xhr), options)) return false; - if (success) success(model, resp, xhr); - }; - options.error = wrapError(options.error, model, options); - var method = this.isNew() ? 'create' : 'update'; - return (this.sync || Backbone.sync).call(this, method, this, options); - }, - - // Destroy this model on the server if it was already persisted. Upon success, the model is removed - // from its collection, if it has one. - destroy : function(options) { - options || (options = {}); - if (this.isNew()) return this.trigger('destroy', this, this.collection, options); - var model = this; - var success = options.success; - options.success = function(resp) { - model.trigger('destroy', model, model.collection, options); - if (success) success(model, resp); - }; - options.error = wrapError(options.error, model, options); - return (this.sync || Backbone.sync).call(this, 'delete', this, options); - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url : function() { - var base = getUrl(this.collection) || this.urlRoot || urlError(); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse : function(resp, xhr) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone : function() { - return new this.constructor(this); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew : function() { - return this.id == null; - }, - - // Call this method to manually fire a `change` event for this model. - // Calling this will cause all objects observing the model to update. - change : function(options) { - this.trigger('change', this, options); - this._previousAttributes = _.clone(this.attributes); - this._changed = false; - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged : function(attr) { - if (attr) return this._previousAttributes[attr] != this.attributes[attr]; - return this._changed; - }, - - // Return an object containing all the attributes that have changed, or false - // if there are no changed attributes. Useful for determining what parts of a - // view need to be updated and/or what attributes need to be persisted to - // the server. - changedAttributes : function(now) { - now || (now = this.attributes); - var old = this._previousAttributes; - var changed = false; - for (var attr in now) { - if (!_.isEqual(old[attr], now[attr])) { - changed = changed || {}; - changed[attr] = now[attr]; - } - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous : function(attr) { - if (!attr || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes : function() { - return _.clone(this._previousAttributes); - }, - - // Run validation against a set of incoming attributes, returning `true` - // if all is well. If a specific `error` callback has been passed, - // call that instead of firing the general `"error"` event. - _performValidation : function(attrs, options) { - var error = this.validate(attrs); - if (error) { - if (options.error) { - options.error(this, error, options); - } else { - this.trigger('error', this, error, options); - } - return false; - } - return true; - } - - }); - - // Backbone.Collection - // ------------------- - - // Provides a standard collection class for our sets of models, ordered - // or unordered. If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.comparator) this.comparator = options.comparator; - _.bindAll(this, '_onModelEvent', '_removeReference'); - this._reset(); - if (models) this.reset(models, {silent: true}); - this.initialize.apply(this, arguments); - }; - - // Define the Collection's inheritable methods. - _.extend(Backbone.Collection.prototype, Backbone.Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model : Backbone.Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON : function() { - return this.map(function(model){ return model.toJSON(); }); - }, - - // Add a model, or list of models to the set. Pass **silent** to avoid - // firing the `added` event for every new model. - add : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._add(models[i], options); - } - } else { - this._add(models, options); - } - return this; - }, - - // Remove a model, or a list of models from the set. Pass silent to avoid - // firing the `removed` event for every model removed. - remove : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._remove(models[i], options); - } - } else { - this._remove(models, options); - } - return this; - }, - - // Get a model from the set by id. - get : function(id) { - if (id == null) return null; - return this._byId[id.id != null ? id.id : id]; - }, - - // Get a model from the set by client id. - getByCid : function(cid) { - return cid && this._byCid[cid.cid || cid]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Force the collection to re-sort itself. You don't need to call this under normal - // circumstances, as the set will maintain sort order as each item is added. - sort : function(options) { - options || (options = {}); - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Pluck an attribute from each model in the collection. - pluck : function(attr) { - return _.map(this.models, function(model){ return model.get(attr); }); - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `reset` when finished. - reset : function(models, options) { - models || (models = []); - options || (options = {}); - this.each(this._removeReference); - this._reset(); - this.add(models, {silent: true}); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `add: true` is passed, appends the - // models to the collection instead of resetting. - fetch : function(options) { - options || (options = {}); - var collection = this; - var success = options.success; - options.success = function(resp, status, xhr) { - collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); - if (success) success(collection, resp); - }; - options.error = wrapError(options.error, collection, options); - return (this.sync || Backbone.sync).call(this, 'read', this, options); - }, - - // Create a new instance of a model in this collection. After the model - // has been created on the server, it will be added to the collection. - // Returns the model, or 'false' if validation on a new model fails. - create : function(model, options) { - var coll = this; - options || (options = {}); - model = this._prepareModel(model, options); - if (!model) return false; - var success = options.success; - options.success = function(nextModel, resp, xhr) { - coll.add(nextModel, options); - if (success) success(nextModel, resp, xhr); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse : function(resp, xhr) { - return resp; - }, - - // Proxy to _'s chain. Can't be proxied the same way the rest of the - // underscore methods are proxied because it relies on the underscore - // constructor. - chain: function () { - return _(this.models).chain(); - }, - - // Reset all internal state. Called when the collection is reset. - _reset : function(options) { - this.length = 0; - this.models = []; - this._byId = {}; - this._byCid = {}; - }, - - // Prepare a model to be added to this collection - _prepareModel: function(model, options) { - if (!(model instanceof Backbone.Model)) { - var attrs = model; - model = new this.model(attrs, {collection: this}); - if (model.validate && !model._performValidation(attrs, options)) model = false; - } else if (!model.collection) { - model.collection = this; - } - return model; - }, - - // Internal implementation of adding a single model to the set, updating - // hash indexes for `id` and `cid` lookups. - // Returns the model, or 'false' if validation on a new model fails. - _add : function(model, options) { - options || (options = {}); - model = this._prepareModel(model, options); - if (!model) return false; - var already = this.getByCid(model); - if (already) throw new Error(["Can't add the same model to a set twice", already.id]); - this._byId[model.id] = model; - this._byCid[model.cid] = model; - var index = options.at != null ? options.at : - this.comparator ? this.sortedIndex(model, this.comparator) : - this.length; - this.models.splice(index, 0, model); - model.bind('all', this._onModelEvent); - this.length++; - if (!options.silent) model.trigger('add', model, this, options); - return model; - }, - - // Internal implementation of removing a single model from the set, updating - // hash indexes for `id` and `cid` lookups. - _remove : function(model, options) { - options || (options = {}); - model = this.getByCid(model) || this.get(model); - if (!model) return null; - delete this._byId[model.id]; - delete this._byCid[model.cid]; - this.models.splice(this.indexOf(model), 1); - this.length--; - if (!options.silent) model.trigger('remove', model, this, options); - this._removeReference(model); - return model; - }, - - // Internal method to remove a model's ties to a collection. - _removeReference : function(model) { - if (this == model.collection) { - delete model.collection; - } - model.unbind('all', this._onModelEvent); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent : function(ev, model, collection, options) { - if ((ev == 'add' || ev == 'remove') && collection != this) return; - if (ev == 'destroy') { - this._remove(model, options); - } - if (model && ev === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', - 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', - 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Backbone.Collection.prototype[method] = function() { - return _[method].apply(_, [this.models].concat(_.toArray(arguments))); - }; - }); - - // Backbone.Router - // ------------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var namedParam = /:([\w\d]+)/g; - var splatParam = /\*([\w\d]+)/g; - var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Backbone.Router.prototype, Backbone.Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route : function(route, name, callback) { - Backbone.history || (Backbone.history = new Backbone.History); - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - Backbone.history.route(route, _.bind(function(fragment) { - var args = this._extractParameters(route, fragment); - callback.apply(this, args); - this.trigger.apply(this, ['route:' + name].concat(args)); - }, this)); - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate : function(fragment, triggerRoute) { - Backbone.history.navigate(fragment, triggerRoute); - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes : function() { - if (!this.routes) return; - var routes = []; - for (var route in this.routes) { - routes.unshift([route, this.routes[route]]); - } - for (var i = 0, l = routes.length; i < l; i++) { - this.route(routes[i][0], routes[i][1], this[routes[i][1]]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp : function(route) { - route = route.replace(escapeRegExp, "\\$&") - .replace(namedParam, "([^\/]*)") - .replace(splatParam, "(.*?)"); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted parameters. - _extractParameters : function(route, fragment) { - return route.exec(fragment).slice(1); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on URL fragments. If the - // browser does not support `onhashchange`, falls back to polling. - Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - }; - - // Cached regex for cleaning hashes. - var hashStrip = /^#*/; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Has the history handling already been started? - var historyStarted = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(Backbone.History.prototype, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment : function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || forcePushState) { - fragment = window.location.pathname; - var search = window.location.search; - if (search) fragment += search; - if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); - } else { - fragment = window.location.hash; - } - } - return decodeURIComponent(fragment.replace(hashStrip, '')); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start : function(options) { - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - if (historyStarted) throw new Error("Backbone.history has already been started"); - this.options = _.extend({}, {root: '/'}, this.options, options); - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - if (oldIE) { - this.iframe = $('