From e73a22ccb9471db95ac28a98f32014d81b45facb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 8 Oct 2013 16:23:09 +0200 Subject: [PATCH 01/64] [WIP] point_of_sale: trying to remove all css fx to see if it's really faster bzr revid: fva@openerp.com-20131008142309-4a1hgsg8ikw8dvzw --- addons/point_of_sale/static/src/css/pos.css | 153 +------------------- 1 file changed, 1 insertion(+), 152 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index da8325cd046..e93c9774549 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -15,10 +15,10 @@ top: 0; width: 100%; height: 100%; - text-shadow: none; -webkit-user-select: none; -moz-user-select: none; user-select: none; + text-shadow: none; } .point-of-sale .oe_hidden{ @@ -37,10 +37,6 @@ left:0; right:0; height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - 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{ @@ -49,10 +45,6 @@ left:0; right:0; height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - 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 ********* */ @@ -80,12 +72,7 @@ padding: 4px 10px; font-size: 11px; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2)); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); } .point-of-sale ul, .point-of-sale ol { padding: 0; @@ -100,7 +87,6 @@ .point-of-sale .pos-right-align input { text-align: right; border: 1px solid #cecbcb; - border-radius: 4px; } .point-of-sale .pos-disc-font { font-size: 12px; @@ -121,8 +107,6 @@ padding:0; color: gray; background: #393939; - background: -moz-linear-gradient(#7b7979, #393939); - background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939)); } /* a) The left part of the top-bar */ @@ -173,10 +157,6 @@ margin-right:0px; border: 1px solid #353A7E; background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); - background: -moz-linear-gradient(#b2b3d7, #7f82ac); - background: -ms-linear-gradient(#b2b3d7, #7f82ac); - background: linear-gradient(#b2b3d7, #7f82ac); } .point-of-sale #rightheader button.square{ @@ -198,11 +178,6 @@ font-weight: 900; background: #7174A8 !important; color: rgb(236, 237, 255) !important; - text-shadow: 0px 1px rgba(0, 0, 0, 0.31); - -webkit-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - -moz-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - -ms-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; - box-shadow: 0px 1px 2px rgb(63, 66, 139) inset; } /* c) The session buttons */ @@ -228,7 +203,6 @@ } .point-of-sale #rightheader .header-button:active{ background: rgba(0,0,0,0.2); - text-shadow: #000 0px 0px 3px; color:#EEE; } @@ -325,16 +299,6 @@ border: none; color: white; background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); - text-shadow: 0px 0px 5px rgba(255,255,255,0.21); - -webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -ms-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - -o-box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); - box-shadow: 0px 0px 2px rgba(0,0,0,0.3) inset, 0px 1px 2px rgba(0,0,0,0.1); } /* ********* The Numpad ********* */ @@ -377,8 +341,6 @@ height: 32px; border-bottom: 1px solid #c7c7c7; background: #d3d3d3; - background: -moz-linear-gradient(white, #d3d3d3); - background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3)); } /* ********* The product list ********* */ @@ -419,7 +381,6 @@ display: inline-block; padding: 0 9px; vertical-align: top; - text-shadow: #f7f7f7 0 1px 1px; color: #555555; font-weight: bold; } @@ -440,7 +401,6 @@ } .point-of-sale .searchbox input { width: 130px; - border-radius: 11px; border: 1px solid #cecbcb; padding: 3px 19px; margin: 6px; @@ -460,7 +420,6 @@ .point-of-sale #categories { /*background:#f0f0f0;*/ position: relative; - background-image: url('../img/bg.png'); border-bottom: 2px solid #e0e0e0; } .point-of-sale #categories .white-gradient{ @@ -469,10 +428,6 @@ left:0; right:0; bottom:0; - background: -webkit-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -moz-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -ms-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); } .point-of-sale #categories h4 { display: inline-block; @@ -494,10 +449,6 @@ height:120px; background:#fff; border: 1px solid #fff; - border-radius: 3px; - -webkit-box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); - -moz-box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); - box-shadow: 0px 2px 0px #E9E9E9, 0px 3px 8px rgba(0, 0, 0, 0.34); cursor: pointer; } @@ -510,25 +461,13 @@ cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } .point-of-sale .category-simple-button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; @@ -560,9 +499,6 @@ 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)); - background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); /* for some reason the -90deg orientation doesn't match the -webkit-linear-gradient. It should be 180deg here. * webkit also insists on rendering *both* gradients instead of only the native one. So it doesn't looks right. ugh. background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); */ @@ -585,11 +521,7 @@ height:120px; background:#fff; border: 1px solid #fff; - border-radius: 3px; overflow: hidden; - -webkit-box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; - -moz-box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; - box-shadow: 0px 2px 0px #dad8e4, 0px 1px 8px #636480; } .point-of-sale .product .product-img { @@ -614,8 +546,6 @@ line-height: 13px; background: #7f82ac; padding: 2px 5px; - border-radius: 3px; - box-shadow: 0px 1px 0px #9A9CC5, 0px 3px 0px #7E86AC, 0px 3px 3px rgba(12, 14, 68, 0.67); } .point-of-sale .product .product-name { @@ -650,8 +580,6 @@ top:0px; bottom:0px; width:100%; - background-image: -webkit-radial-gradient(rgba(255, 255, 255, 0),rgba(51, 17, 124, 0.25)), url('../img/grid.png'); - box-shadow: 0px 36px 24px rgba(0, 0, 0, 0.14) inset; } .point-of-sale .screen header h2 { margin-top: 0px; @@ -667,9 +595,7 @@ margin-top: 50px; padding: 40px; background-color: #f8f8f8; - border-radius: 4px; text-align: center; - box-shadow: 0px 1px 0px white,0px -1px 0px white, 0px 4px 0px #DFDFDF, 0px 10px 30px rgba(0, 0, 0, 0.21); } .point-of-sale .dialog p{ font-size: 25px; @@ -724,15 +650,12 @@ padding: 0px 8px; padding-top: 8px; margin-left: 16px; - border-radius: 5px; background: white; - box-shadow: 0px -1px 0px #E2E2E2 inset,0px 1px 0px white inset, 0px 4px 0px #DDD inset, 0px 4px 8px rgba(0, 0, 0, 0.55) inset; color: #4c4c4c; -webkit-animation: all 250ms linear; } .point-of-sale .pos-step-container input:focus{ - box-shadow: 0px -1px 0px #C9CFFD inset,0px 1px 0px #B8C8FC inset, 0px 4px 0px #9FD5FF inset, 0px 4px 9px rgba(0, 31, 255, 0.55) inset; color: #5d7ad6; -webkit-animation: all 250ms linear; } @@ -743,8 +666,6 @@ margin-top: 50px; padding: 40px; background-color: #f8f8f8; - border-radius: 4px; - box-shadow: 0px 1px 0px white,0px -1px 0px white, 0px 4px 0px #DFDFDF, 0px 10px 30px rgba(0, 0, 0, 0.21); } .point-of-sale .pos-payment-container .left-block{ display: inline-block; @@ -797,8 +718,6 @@ padding-bottom:30px; display: inline-block; font-family: "Inconsolata"; - border-radius: 2px; - box-shadow: 0px 1px 0px white, 0px 3px 0px #C1B9D6, 0px 8px 16px rgba(0, 0, 0, 0.3); } .point-of-sale .pos-sale-ticket .emph{ font-size: 20px; @@ -863,8 +782,6 @@ margin-right: auto; padding: 40px; background: #f1f1f1; - box-shadow: 0px 1px 0px white, 0px -1px 0px white, 0px 4px 0px #dfdfdf, 0px 10px 30px rgba(0,0,0,0.21); - border-radius: 4px; } .point-of-sale .scale-screen .product-picture { @@ -883,8 +800,6 @@ background:#fff; border: 1px solid #fff; - border-radius: 3px; - box-shadow: 0px 2px 0px #e9e9e9, 0px 2px 8px rgba(0,0,0,0.2); } .point-of-sale .scale-screen .product-picture img{ @@ -904,7 +819,6 @@ color:white; background: #7f82ac; padding: 2px 5px; - border-radius: 3px; cursor:pointer; } @@ -931,8 +845,6 @@ padding-right: 33px; padding-top: 11px; background: white; - border-radius: 3px; - box-shadow: 0px -1px #E2E2E2 inset,0px 1px white inset, 0px 4px #E6E4E4 inset, 0px 4px 8px rgba(0, 0, 0, 0.4) inset; } .point-of-sale .scale-screen .weight p{ @@ -946,7 +858,6 @@ margin-right: 18px; font-family: "Inconsolata"; color: #6c6c6c; - text-shadow: 0px 3px 3px rgba(0,0,0, 0.2); } /* e) The Welcome Screen */ @@ -965,9 +876,6 @@ font-weight:bold; background-color: #F0EEEE; border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); z-index:1150; } @@ -989,10 +897,6 @@ .point-of-sale .order{ background: white; - background: -webkit-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -moz-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -ms-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); padding-bottom:15px; padding-top:15px; margin-left:16px; @@ -1000,10 +904,6 @@ margin-top:16px; margin-bottom:16px; font-size:16px; - border-radius: 2px; - -webkit-box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); - -moz-box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); - box-shadow: 0px 1px 0px white, 0px 3px 0px #DFDFDF, 0px 7px 16px rgba(0, 0, 0, 0.20); } @@ -1150,16 +1050,8 @@ cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } .point-of-sale .pos-actionbar .button .label{ margin-top: 37px; @@ -1171,33 +1063,18 @@ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .pos-actionbar .button.disabled *{ - opacity: 0.5; -} .point-of-sale .pos-actionbar .button.disabled:active{ border: 1px solid #cacaca; - border-radius: 4px; color: #555; cursor: default; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); } .point-of-sale .pos-actionbar .button.rightalign{ @@ -1215,9 +1092,7 @@ background: rgba(0,0,0,0.82); color: white; - text-shadow: none; padding-bottom: 10px; - box-shadow: 0px 3px 20px rgba(0,0,0,0.3); cursor:move; } .point-of-sale .debug-widget .toggle{ @@ -1302,8 +1177,6 @@ font-size:20px; font-weight:bold; background-color: #F0EEEE; - border-radius: 4px; - box-shadow: 0px -1px white, 0px 1px white, 0px 4px #949494, 0px 10px 20px rgba(0, 0, 0, 0.3); z-index:1200; } .point-of-sale .popup .footer{ @@ -1330,25 +1203,13 @@ cursor: pointer; border: 1px solid #cacaca; - border-radius: 4px; background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); } .point-of-sale .popup .button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; - background: -webkit-linear-gradient(#9d9fc5, #7f82ac); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -webkit-transition-property: background, border; -webkit-transition-duration: 0.2s; @@ -1390,7 +1251,6 @@ line-height: 38px; text-align: center; font-size:48px; - border-radius: 4px; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; @@ -1401,16 +1261,8 @@ .point-of-sale .scrollbar .button{ color:white; background: rgba(0,0,0,0.6); - -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - text-shadow: rgba(255,255,255,0.5) 0px 0px 10px; - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; } .point-of-sale .scrollbar .button:active{ - text-shadow: rgba(255,255,255,0.8) 0px 0px 15px; } .point-of-sale .scrollbar .button.disabled{ background: rgba(0,0,0,0.3); @@ -1433,7 +1285,6 @@ bottom:50%; width:100%; background: rgba(0,0,0,0.1); - border-radius: 4px; } /* ********* Unsupported Browser Page ********* */ @@ -1455,6 +1306,4 @@ } .point-of-sale .not-supported-browser img{ border-collapse: separate; - box-shadow: 0px 3px 38px rgba(0,0,0,0.3); - border-radius: 3px; } From 96d57510af665f36418325fb143ccf3f577ef791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 8 Oct 2013 16:36:39 +0200 Subject: [PATCH 02/64] [WIP] point_of_sale: removed js scrollbars bzr revid: fva@openerp.com-20131008143639-qacekusrhy6uoues --- addons/point_of_sale/static/src/css/pos.css | 2 + addons/point_of_sale/static/src/js/widgets.js | 43 ------------------- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index e93c9774549..38f61a25c29 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -357,6 +357,7 @@ width:100%; height:100%; overflow: hidden; + overflow-y: auto; } .point-of-sale .product-list-container { position:absolute; @@ -893,6 +894,7 @@ width:100%; height:100%; overflow:hidden; + overflow-y: auto; } .point-of-sale .order{ diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 291c8bfdaae..0bf94d40926 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -214,9 +214,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // freeing subwidgets - if(this.scrollbar){ - this.scrollbar.destroy(); - } for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){ this.orderlinewidgets[i].destroy(); } @@ -243,30 +240,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, this)); this.update_numpad(); this.update_summary(); - - var position = this.scrollbar ? this.scrollbar.get_position() : 0; - var at_bottom = this.scrollbar ? this.scrollbar.is_at_bottom() : false; - - this.scrollbar = new module.ScrollbarWidget(this,{ - target_widget: this, - target_selector: '.order-scroller', - name: 'order', - track_bottom: true, - on_show: function(){ - self.$('.order-scroller').css({'width':'89%'},100); - }, - on_hide: function(){ - self.$('.order-scroller').css({'width':'100%'},100); - }, - }); - - this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget')); - this.scrollbar.set_position(position); - - if( at_bottom ){ - this.scrollbar.set_position(Number.MAX_VALUE, false); - } - }, update_summary: function(){ var order = this.pos.get('selectedOrder'); @@ -609,9 +582,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var self = this; this._super(); - if(this.scrollbar){ - this.scrollbar.destroy(); - } var products = this.pos.get('products').models || []; _.each(products,function(product,i){ @@ -621,19 +591,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa $product.appendTo(self.$('.product-list')); }); - this.scrollbar = new module.ScrollbarWidget(this,{ - target_widget: this, - target_selector: '.product-list-scroller', - on_show: function(){ - self.$('.product-list-scroller').css({'padding-right':'62px'},100); - }, - on_hide: function(){ - self.$('.product-list-scroller').css({'padding-right':'0px'},100); - }, - }); - - this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget')); - }, }); From 9329c3ba9da9226247c51b402e36a7a5fbb2adf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 8 Oct 2013 17:19:40 +0200 Subject: [PATCH 03/64] [WIP] point_of_sale: add fastclick.js to remove tab delay on mobile devices bzr revid: fva@openerp.com-20131008151940-67uky6vmn52vxyun --- addons/point_of_sale/__openerp__.py | 1 + addons/point_of_sale/static/lib/fastclick.js | 777 ++++++++++++++++++ addons/point_of_sale/static/src/js/widgets.js | 2 + 3 files changed, 780 insertions(+) create mode 100644 addons/point_of_sale/static/lib/fastclick.js diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index dfdc2c57cb9..d963144ceca 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -90,6 +90,7 @@ Main Features 'application': True, 'js': [ 'static/lib/mousewheel/jquery.mousewheel-3.0.6.js', + 'static/lib/fastclick.js', 'static/src/js/db.js', 'static/src/js/models.js', 'static/src/js/widget_base.js', diff --git a/addons/point_of_sale/static/lib/fastclick.js b/addons/point_of_sale/static/lib/fastclick.js new file mode 100644 index 00000000000..a30edb3cb31 --- /dev/null +++ b/addons/point_of_sale/static/lib/fastclick.js @@ -0,0 +1,777 @@ +/** + * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. + * + * @version 0.6.11 + * @codingstandard ftlabs-jsv2 + * @copyright The Financial Times Limited [All Rights Reserved] + * @license MIT License (see LICENSE.txt) + */ + +/*jslint browser:true, node:true*/ +/*global define, Event, Node*/ + + +/** + * Instantiate fast-clicking listeners on the specificed layer. + * + * @constructor + * @param {Element} layer The layer to listen on + */ +function FastClick(layer) { + 'use strict'; + var oldOnClick, self = this; + + + /** + * Whether a click is currently being tracked. + * + * @type boolean + */ + this.trackingClick = false; + + + /** + * Timestamp for when when click tracking started. + * + * @type number + */ + this.trackingClickStart = 0; + + + /** + * The element being tracked for a click. + * + * @type EventTarget + */ + this.targetElement = null; + + + /** + * X-coordinate of touch start event. + * + * @type number + */ + this.touchStartX = 0; + + + /** + * Y-coordinate of touch start event. + * + * @type number + */ + this.touchStartY = 0; + + + /** + * ID of the last touch, retrieved from Touch.identifier. + * + * @type number + */ + this.lastTouchIdentifier = 0; + + + /** + * Touchmove boundary, beyond which a click will be cancelled. + * + * @type number + */ + this.touchBoundary = 10; + + + /** + * The FastClick layer. + * + * @type Element + */ + this.layer = layer; + + if (!layer || !layer.nodeType) { + throw new TypeError('Layer must be a document node'); + } + + /** @type function() */ + this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); }; + + /** @type function() */ + this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); }; + + /** @type function() */ + this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); }; + + /** @type function() */ + this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); }; + + /** @type function() */ + this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); }; + + /** @type function() */ + this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); }; + + if (FastClick.notNeeded(layer)) { + return; + } + + // Set up event handlers as required + if (this.deviceIsAndroid) { + layer.addEventListener('mouseover', this.onMouse, true); + layer.addEventListener('mousedown', this.onMouse, true); + layer.addEventListener('mouseup', this.onMouse, true); + } + + layer.addEventListener('click', this.onClick, true); + layer.addEventListener('touchstart', this.onTouchStart, false); + layer.addEventListener('touchmove', this.onTouchMove, false); + layer.addEventListener('touchend', this.onTouchEnd, false); + layer.addEventListener('touchcancel', this.onTouchCancel, false); + + // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick + // layer when they are cancelled. + if (!Event.prototype.stopImmediatePropagation) { + layer.removeEventListener = function(type, callback, capture) { + var rmv = Node.prototype.removeEventListener; + if (type === 'click') { + rmv.call(layer, type, callback.hijacked || callback, capture); + } else { + rmv.call(layer, type, callback, capture); + } + }; + + layer.addEventListener = function(type, callback, capture) { + var adv = Node.prototype.addEventListener; + if (type === 'click') { + adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { + if (!event.propagationStopped) { + callback(event); + } + }), capture); + } else { + adv.call(layer, type, callback, capture); + } + }; + } + + // If a handler is already declared in the element's onclick attribute, it will be fired before + // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and + // adding it as listener. + if (typeof layer.onclick === 'function') { + + // Android browser on at least 3.2 requires a new reference to the function in layer.onclick + // - the old one won't work if passed to addEventListener directly. + oldOnClick = layer.onclick; + layer.addEventListener('click', function(event) { + oldOnClick(event); + }, false); + layer.onclick = null; + } +} + + +/** + * Android requires exceptions. + * + * @type boolean + */ +FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0; + + +/** + * iOS requires exceptions. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent); + + +/** + * iOS 4 requires an exception for select elements. + * + * @type boolean + */ +FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); + + +/** + * iOS 6.0(+?) requires the target element to be manually derived + * + * @type boolean + */ +FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent); + + +/** + * Determine whether a given element requires a native click. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element needs a native click + */ +FastClick.prototype.needsClick = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + + // Don't send a synthetic click to disabled inputs (issue #62) + case 'button': + case 'select': + case 'textarea': + if (target.disabled) { + return true; + } + + break; + case 'input': + + // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) + if ((this.deviceIsIOS && target.type === 'file') || target.disabled) { + return true; + } + + break; + case 'label': + case 'video': + return true; + } + + return (/\bneedsclick\b/).test(target.className); +}; + + +/** + * Determine whether a given element requires a call to focus to simulate click into element. + * + * @param {EventTarget|Element} target Target DOM element + * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. + */ +FastClick.prototype.needsFocus = function(target) { + 'use strict'; + switch (target.nodeName.toLowerCase()) { + case 'textarea': + return true; + case 'select': + return !this.deviceIsAndroid; + case 'input': + switch (target.type) { + case 'button': + case 'checkbox': + case 'file': + case 'image': + case 'radio': + case 'submit': + return false; + } + + // No point in attempting to focus disabled inputs + return !target.disabled && !target.readOnly; + default: + return (/\bneedsfocus\b/).test(target.className); + } +}; + + +/** + * Send a click event to the specified element. + * + * @param {EventTarget|Element} targetElement + * @param {Event} event + */ +FastClick.prototype.sendClick = function(targetElement, event) { + 'use strict'; + var clickEvent, touch; + + // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) + if (document.activeElement && document.activeElement !== targetElement) { + document.activeElement.blur(); + } + + touch = event.changedTouches[0]; + + // Synthesise a click event, with an extra attribute so it can be tracked + clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + clickEvent.forwardedTouchEvent = true; + targetElement.dispatchEvent(clickEvent); +}; + +FastClick.prototype.determineEventType = function(targetElement) { + 'use strict;' + + //Issue #159: Android Chrome Select Box does not open with a synthetic click event + if (this.deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { + return 'mousedown'; + } + + return 'click'; +} + + +/** + * @param {EventTarget|Element} targetElement + */ +FastClick.prototype.focus = function(targetElement) { + 'use strict'; + var length; + + // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. + if (this.deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') { + length = targetElement.value.length; + targetElement.setSelectionRange(length, length); + } else { + targetElement.focus(); + } +}; + + +/** + * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. + * + * @param {EventTarget|Element} targetElement + */ +FastClick.prototype.updateScrollParent = function(targetElement) { + 'use strict'; + var scrollParent, parentElement; + + scrollParent = targetElement.fastClickScrollParent; + + // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the + // target element was moved to another parent. + if (!scrollParent || !scrollParent.contains(targetElement)) { + parentElement = targetElement; + do { + if (parentElement.scrollHeight > parentElement.offsetHeight) { + scrollParent = parentElement; + targetElement.fastClickScrollParent = parentElement; + break; + } + + parentElement = parentElement.parentElement; + } while (parentElement); + } + + // Always update the scroll top tracker if possible. + if (scrollParent) { + scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; + } +}; + + +/** + * @param {EventTarget} targetElement + * @returns {Element|EventTarget} + */ +FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { + 'use strict'; + + // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. + if (eventTarget.nodeType === Node.TEXT_NODE) { + return eventTarget.parentNode; + } + + return eventTarget; +}; + + +/** + * On touch start, record the position and scroll offset. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchStart = function(event) { + 'use strict'; + var targetElement, touch, selection; + + // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). + if (event.targetTouches.length > 1) { + return true; + } + + targetElement = this.getTargetElementFromEventTarget(event.target); + touch = event.targetTouches[0]; + + if (this.deviceIsIOS) { + + // Only trusted events will deselect text on iOS (issue #49) + selection = window.getSelection(); + if (selection.rangeCount && !selection.isCollapsed) { + return true; + } + + if (!this.deviceIsIOS4) { + + // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): + // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched + // with the same identifier as the touch event that previously triggered the click that triggered the alert. + // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an + // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. + if (touch.identifier === this.lastTouchIdentifier) { + event.preventDefault(); + return false; + } + + this.lastTouchIdentifier = touch.identifier; + + // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: + // 1) the user does a fling scroll on the scrollable layer + // 2) the user stops the fling scroll with another tap + // then the event.target of the last 'touchend' event will be the element that was under the user's finger + // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check + // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). + this.updateScrollParent(targetElement); + } + } + + this.trackingClick = true; + this.trackingClickStart = event.timeStamp; + this.targetElement = targetElement; + + this.touchStartX = touch.pageX; + this.touchStartY = touch.pageY; + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + event.preventDefault(); + } + + return true; +}; + + +/** + * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.touchHasMoved = function(event) { + 'use strict'; + var touch = event.changedTouches[0], boundary = this.touchBoundary; + + if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { + return true; + } + + return false; +}; + + +/** + * Update the last position. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchMove = function(event) { + 'use strict'; + if (!this.trackingClick) { + return true; + } + + // If the touch has moved, cancel the click tracking + if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { + this.trackingClick = false; + this.targetElement = null; + } + + return true; +}; + + +/** + * Attempt to find the labelled control for the given label element. + * + * @param {EventTarget|HTMLLabelElement} labelElement + * @returns {Element|null} + */ +FastClick.prototype.findControl = function(labelElement) { + 'use strict'; + + // Fast path for newer browsers supporting the HTML5 control attribute + if (labelElement.control !== undefined) { + return labelElement.control; + } + + // All browsers under test that support touch events also support the HTML5 htmlFor attribute + if (labelElement.htmlFor) { + return document.getElementById(labelElement.htmlFor); + } + + // If no for attribute exists, attempt to retrieve the first labellable descendant element + // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label + return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); +}; + + +/** + * On touch end, determine whether to send a click event at once. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onTouchEnd = function(event) { + 'use strict'; + var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; + + if (!this.trackingClick) { + return true; + } + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < 200) { + this.cancelNextClick = true; + return true; + } + + // Reset to prevent wrong click cancel on input (issue #156). + this.cancelNextClick = false; + + this.lastClickTime = event.timeStamp; + + trackingClickStart = this.trackingClickStart; + this.trackingClick = false; + this.trackingClickStart = 0; + + // On some iOS devices, the targetElement supplied with the event is invalid if the layer + // is performing a transition or scroll, and has to be re-detected manually. Note that + // for this to function correctly, it must be called *after* the event target is checked! + // See issue #57; also filed as rdar://13048589 . + if (this.deviceIsIOSWithBadTarget) { + touch = event.changedTouches[0]; + + // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null + targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; + targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; + } + + targetTagName = targetElement.tagName.toLowerCase(); + if (targetTagName === 'label') { + forElement = this.findControl(targetElement); + if (forElement) { + this.focus(targetElement); + if (this.deviceIsAndroid) { + return false; + } + + targetElement = forElement; + } + } else if (this.needsFocus(targetElement)) { + + // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. + // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). + if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) { + this.targetElement = null; + return false; + } + + this.focus(targetElement); + + // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. + if (!this.deviceIsIOS4 || targetTagName !== 'select') { + this.targetElement = null; + event.preventDefault(); + } + + return false; + } + + if (this.deviceIsIOS && !this.deviceIsIOS4) { + + // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled + // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). + scrollParent = targetElement.fastClickScrollParent; + if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { + return true; + } + } + + // Prevent the actual click from going though - unless the target node is marked as requiring + // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. + if (!this.needsClick(targetElement)) { + event.preventDefault(); + this.sendClick(targetElement, event); + } + + return false; +}; + + +/** + * On touch cancel, stop tracking the click. + * + * @returns {void} + */ +FastClick.prototype.onTouchCancel = function() { + 'use strict'; + this.trackingClick = false; + this.targetElement = null; +}; + + +/** + * Determine mouse events which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onMouse = function(event) { + 'use strict'; + + // If a target element was never set (because a touch event was never fired) allow the event + if (!this.targetElement) { + return true; + } + + if (event.forwardedTouchEvent) { + return true; + } + + // Programmatically generated events targeting a specific element should be permitted + if (!event.cancelable) { + return true; + } + + // Derive and check the target element to see whether the mouse event needs to be permitted; + // unless explicitly enabled, prevent non-touch click events from triggering actions, + // to prevent ghost/doubleclicks. + if (!this.needsClick(this.targetElement) || this.cancelNextClick) { + + // Prevent any user-added listeners declared on FastClick element from being fired. + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } else { + + // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) + event.propagationStopped = true; + } + + // Cancel the event + event.stopPropagation(); + event.preventDefault(); + + return false; + } + + // If the mouse event is permitted, return true for the action to go through. + return true; +}; + + +/** + * On actual clicks, determine whether this is a touch-generated click, a click action occurring + * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or + * an actual click which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ +FastClick.prototype.onClick = function(event) { + 'use strict'; + var permitted; + + // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. + if (this.trackingClick) { + this.targetElement = null; + this.trackingClick = false; + return true; + } + + // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. + if (event.target.type === 'submit' && event.detail === 0) { + return true; + } + + permitted = this.onMouse(event); + + // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. + if (!permitted) { + this.targetElement = null; + } + + // If clicks are permitted, return true for the action to go through. + return permitted; +}; + + +/** + * Remove all FastClick's event listeners. + * + * @returns {void} + */ +FastClick.prototype.destroy = function() { + 'use strict'; + var layer = this.layer; + + if (this.deviceIsAndroid) { + layer.removeEventListener('mouseover', this.onMouse, true); + layer.removeEventListener('mousedown', this.onMouse, true); + layer.removeEventListener('mouseup', this.onMouse, true); + } + + layer.removeEventListener('click', this.onClick, true); + layer.removeEventListener('touchstart', this.onTouchStart, false); + layer.removeEventListener('touchmove', this.onTouchMove, false); + layer.removeEventListener('touchend', this.onTouchEnd, false); + layer.removeEventListener('touchcancel', this.onTouchCancel, false); +}; + + +/** + * Check whether FastClick is needed. + * + * @param {Element} layer The layer to listen on + */ +FastClick.notNeeded = function(layer) { + 'use strict'; + var metaViewport; + + // Devices that don't support touch don't need FastClick + if (typeof window.ontouchstart === 'undefined') { + return true; + } + + if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) { + + // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) + if (FastClick.prototype.deviceIsAndroid) { + metaViewport = document.querySelector('meta[name=viewport]'); + if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) { + return true; + } + + // Chrome desktop doesn't need FastClick (issue #15) + } else { + return true; + } + } + + // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97) + if (layer.style.msTouchAction === 'none') { + return true; + } + + return false; +}; + + +/** + * Factory method for creating a FastClick object + * + * @param {Element} layer The layer to listen on + */ +FastClick.attach = function(layer) { + 'use strict'; + return new FastClick(layer); +}; + + +if (typeof define !== 'undefined' && define.amd) { + + // AMD. Register as an anonymous module. + define(function() { + 'use strict'; + return FastClick; + }); +} else if (typeof module !== 'undefined' && module.exports) { + module.exports = FastClick.attach; + module.exports.FastClick = FastClick; +} else { + window.FastClick = FastClick; +} diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 0bf94d40926..0f440dcb750 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -795,6 +795,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.cashier_controls_visible = true; this.image_cache = new module.ImageCache(); // for faster products image display + FastClick.attach(document.body); + }, start: function() { From ca28f214e48d1727a4ca988e39b0708ee058f509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 9 Oct 2013 12:13:25 +0200 Subject: [PATCH 04/64] [WIP] point_of_sale: used one jquery event handler for all products in product list in order to accelerate rerendering bzr revid: fva@openerp.com-20131009101325-d71q3ppxb7hstzvd --- addons/point_of_sale/static/src/css/pos.css | 4 ++++ addons/point_of_sale/static/src/js/widgets.js | 8 +++++--- addons/point_of_sale/static/src/xml/pos.xml | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 38f61a25c29..ff4b5d13174 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -21,6 +21,10 @@ text-shadow: none; } +.point-of-sale * { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + .point-of-sale .oe_hidden{ display: none !important; } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 0f440dcb750..476534bd3d7 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -581,15 +581,17 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa renderElement: function() { var self = this; this._super(); - + var products = this.pos.get('products').models || []; - + _.each(products,function(product,i){ var $product = $(QWeb.render('Product',{ widget:self, product: products[i] })); $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url())); - $product.find('a').click(function(){ self.click_product_action(product); }); $product.appendTo(self.$('.product-list')); }); + this.$el.delegate('a','click',function(){ + self.click_product_action(new module.Product(self.pos.db.get_product_by_id(+$(this).data('product-id')))); + }); }, }); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index d5f57e099f7..daa1164a2df 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -401,7 +401,7 @@
  • - +
    From f2aae63c61a0a60e3482031a3031e8c3c6fa2887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 9 Oct 2013 14:51:28 +0200 Subject: [PATCH 05/64] [WIP] point_of_sale: automatically scroll to bottom bzr revid: fva@openerp.com-20131009125128-yydt3l8vi8z2ik8p --- addons/point_of_sale/static/src/js/widgets.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 476534bd3d7..4ddbafd9247 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -210,6 +210,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, renderElement: function() { var self = this; + var scroller = this.$('.order-scroller')[0]; + var scrollbottom = true; + if(scroller){ + var overflow_bottom = scroller.scrollHeight - scroller.clientHeight; + var scrollTop = scroller.scrollTop; + if( scrollTop < 0.9 * overflow_bottom){ + scrollbottom = false; + } + } this._super(); // freeing subwidgets @@ -240,6 +249,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, this)); this.update_numpad(); this.update_summary(); + + scroller = this.$('.order-scroller')[0]; + if(scroller && scrollbottom){ + scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; + } }, update_summary: function(){ var order = this.pos.get('selectedOrder'); From 06aaa06d9f1aedbc3b85412440496257d7830875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 9 Oct 2013 16:57:17 +0200 Subject: [PATCH 06/64] [WIP] point_of_sale: don't auto bottom scroll if not at bottom + saner css bzr revid: fva@openerp.com-20131009145717-n42jt76nig0poc2n --- addons/point_of_sale/static/src/css/pos.css | 41 ++++++++++++++++--- addons/point_of_sale/static/src/js/widgets.js | 17 +++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index ff4b5d13174..5a2cd74e8be 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -25,6 +25,15 @@ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } +.point-of-sale button{ + box-shadow: none; + outline: none; + border: none; +} +.point-of-sale button:hover{ + background: default; +} + .point-of-sale .oe_hidden{ display: none !important; } @@ -293,7 +302,6 @@ font-weight: bold; vertical-align: middle; color: #555555; - border-top: 1px solid #efefef; font-size: 14px; } .point-of-sale #paypad button:active, @@ -319,7 +327,6 @@ font-weight: bold; vertical-align: middle; color: #555555; - border-top: 1px solid #efefef; } .point-of-sale .input-button { font-size: 24px; @@ -425,7 +432,7 @@ .point-of-sale #categories { /*background:#f0f0f0;*/ position: relative; - border-bottom: 2px solid #e0e0e0; + border-bottom: 1px solid #cecece; } .point-of-sale #categories .white-gradient{ position: absolute; @@ -453,7 +460,9 @@ width: 120px; height:120px; background:#fff; - border: 1px solid #fff; + border: 1px solid #e2e2e2; + border-radius: 3px; + border-bottom-width: 3px; cursor: pointer; } @@ -525,7 +534,9 @@ width: 120px; height:120px; background:#fff; - border: 1px solid #fff; + border: 1px solid #e2e2e2; + border-radius: 3px; + border-bottom-width: 3px; overflow: hidden; } @@ -671,6 +682,7 @@ margin-top: 50px; padding: 40px; background-color: #f8f8f8; + border: solid 1px rgb(220,220,220); } .point-of-sale .pos-payment-container .left-block{ display: inline-block; @@ -787,6 +799,7 @@ margin-right: auto; padding: 40px; background: #f1f1f1; + border-top: 1px solid #efefef; } .point-of-sale .scale-screen .product-picture { @@ -910,7 +923,8 @@ margin-top:16px; margin-bottom:16px; font-size:16px; - + border-radius: 3px; + border: solid 1px rgb(220,220,220); } .point-of-sale .order .empty{ @@ -1240,6 +1254,21 @@ line-height:180px; } +/* ********* The Webkit Scrollbar ********* */ + +.point-of-sale *::-webkit-scrollbar{ + width: 10px; + height: 10px; +} +.point-of-sale *::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: solid 1px rgb(200,200,200); +} +.point-of-sale *::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 30px; +} + /* ********* The ScrollBarWidget ********* */ .point-of-sale .scrollbar{ diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index fa455fc12e9..1abd6c3daba 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -200,7 +200,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, bind_orderline_events: function() { this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('add', this.renderElement, this); + this.currentOrderLines.bind('add', function(){ this.renderElement(true);}, this); this.currentOrderLines.bind('remove', this.renderElement, this); }, update_numpad: function() { @@ -208,14 +208,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if (this.numpadState) this.numpadState.reset(); }, - renderElement: function() { + renderElement: function(goto_bottom) { var self = this; var scroller = this.$('.order-scroller')[0]; var scrollbottom = true; + var scrollTop = 0; if(scroller){ var overflow_bottom = scroller.scrollHeight - scroller.clientHeight; - var scrollTop = scroller.scrollTop; - if( scrollTop < 0.9 * overflow_bottom){ + scrollTop = scroller.scrollTop; + if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){ scrollbottom = false; } } @@ -251,8 +252,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.update_summary(); scroller = this.$('.order-scroller')[0]; - if(scroller && scrollbottom){ - scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; + if(scroller){ + if(scrollbottom){ + scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; + }else{ + scroller.scrollTop = scrollTop; + } } }, update_summary: function(){ From d6ae4209e4ef1e248fbc15d8febae761c27a7790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 14 Oct 2013 18:34:14 +0200 Subject: [PATCH 07/64] [IMP] point_of_sale: hardware acceleration for scrolling bzr revid: fva@openerp.com-20131014163414-x3mub8q5ajsqvsdl --- addons/point_of_sale/static/src/css/pos.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 5a2cd74e8be..487a9a6c41c 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -358,6 +358,7 @@ .point-of-sale .product-list { padding:10px !important; + -webkit-transform: translate3d(0,0,0); } .point-of-sale .product-list-scroller{ @@ -368,7 +369,9 @@ width:100%; height:100%; overflow: hidden; - overflow-y: auto; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; + } .point-of-sale .product-list-container { position:absolute; @@ -911,7 +914,8 @@ width:100%; height:100%; overflow:hidden; - overflow-y: auto; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; } .point-of-sale .order{ @@ -925,6 +929,7 @@ font-size:16px; border-radius: 3px; border: solid 1px rgb(220,220,220); + -webkit-transform: translate3d(0,0,0); } .point-of-sale .order .empty{ From c7afc04be334d583510cfa66592397b8400a9190 Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Wed, 23 Oct 2013 14:53:43 +0200 Subject: [PATCH 08/64] [REF] First step, use the same vocabulary for the cr, uid, res_type and res_id => Session, Record. * a Session is a container of one cursor and one user * a Record is a container for the res_type and res_id, in this object, the right names are model and id. Examples: session = Session(cr, uid) record = Record(res_type, res_id) record.mode, record.id A new class named 'WorkflowService' is a wrapper for the instance.* functions. bzr revid: stw@openerp.com-20131023125343-uo0bq52n86ae0fb0 --- openerp/workflow/__init__.py | 66 +++++---------------- openerp/workflow/helpers.py | 15 +++++ openerp/workflow/instance.py | 87 +++++++++++++++++---------- openerp/workflow/service.py | 110 +++++++++++++++++++++++++++++++++++ openerp/workflow/wkf_expr.py | 35 +++++------ openerp/workflow/workitem.py | 108 +++++++++++++++++++++------------- 6 files changed, 280 insertions(+), 141 deletions(-) create mode 100644 openerp/workflow/helpers.py create mode 100644 openerp/workflow/service.py diff --git a/openerp/workflow/__init__.py b/openerp/workflow/__init__.py index f823a63fd42..5506e66bf18 100644 --- a/openerp/workflow/__init__.py +++ b/openerp/workflow/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # @@ -15,16 +15,17 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## -import instance +from openerp.workflow.service import WorkflowService -wkf_on_create_cache = {} +# The new API is in openerp.workflow.workflow_service +# OLD API of the Workflow def clear_cache(cr, uid): - wkf_on_create_cache[cr.dbname]={} + WorkflowService.clear_cache(cr.dbname) def trg_write(uid, res_type, res_id, cr): """ @@ -36,10 +37,7 @@ def trg_write(uid, res_type, res_id, cr): :param res_id: the model instance id the workflow belongs to :param cr: a database cursor """ - ident = (uid,res_type,res_id) - cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (res_id or None,res_type or None, 'active')) - for (id,) in cr.fetchall(): - instance.update(cr, id, ident) + return WorkflowService.new(cr, uid, res_type, res_id).write() def trg_trigger(uid, res_type, res_id, cr): """ @@ -52,12 +50,7 @@ def trg_trigger(uid, res_type, res_id, cr): :param res_id: the model instance id the workflow belongs to :param cr: a database cursor """ - cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (res_id,res_type)) - res = cr.fetchall() - for (instance_id,) in res: - cr.execute('select %s,res_type,res_id from wkf_instance where id=%s', (uid, instance_id,)) - ident = cr.fetchone() - instance.update(cr, instance_id, ident) + return WorkflowService.new(cr, uid, res_type, res_id).trigger() def trg_delete(uid, res_type, res_id, cr): """ @@ -67,8 +60,7 @@ def trg_delete(uid, res_type, res_id, cr): :param res_id: the model instance id the workflow belongs to :param cr: a database cursor """ - ident = (uid,res_type,res_id) - instance.delete(cr, ident) + return WorkflowService.new(cr, uid, res_type, res_id).delete() def trg_create(uid, res_type, res_id, cr): """ @@ -78,16 +70,7 @@ def trg_create(uid, res_type, res_id, cr): :param res_id: the model instance id to own the created worfklow instance :param cr: a database cursor """ - ident = (uid,res_type,res_id) - wkf_on_create_cache.setdefault(cr.dbname, {}) - if res_type in wkf_on_create_cache[cr.dbname]: - wkf_ids = wkf_on_create_cache[cr.dbname][res_type] - else: - cr.execute('select id from wkf where osv=%s and on_create=True', (res_type,)) - wkf_ids = cr.fetchall() - wkf_on_create_cache[cr.dbname][res_type] = wkf_ids - for (wkf_id,) in wkf_ids: - instance.create(cr, ident, wkf_id) + return WorkflowService.new(cr, uid, res_type, res_id).create() def trg_validate(uid, res_type, res_id, signal, cr): """ @@ -98,14 +81,8 @@ def trg_validate(uid, res_type, res_id, signal, cr): :signal: the signal name to be fired :param cr: a database cursor """ - result = False - ident = (uid,res_type,res_id) - # ids of all active workflow instances for a corresponding resource (id, model_nam) - cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (res_id, res_type, 'active')) - for (id,) in cr.fetchall(): - res2 = instance.validate(cr, id, ident, signal) - result = result or res2 - return result + assert isinstance(signal, basestring) + return WorkflowService.new(cr, uid, res_type, res_id).validate(signal) def trg_redirect(uid, res_type, res_id, new_rid, cr): """ @@ -120,22 +97,7 @@ def trg_redirect(uid, res_type, res_id, new_rid, cr): :param new_rid: the model instance id to own the worfklow instance :param cr: a database cursor """ - # get ids of wkf instances for the old resource (res_id) -#CHECKME: shouldn't we get only active instances? - cr.execute('select id, wkf_id from wkf_instance where res_id=%s and res_type=%s', (res_id, res_type)) - for old_inst_id, wkf_id in cr.fetchall(): - # first active instance for new resource (new_rid), using same wkf - cr.execute( - 'SELECT id '\ - 'FROM wkf_instance '\ - 'WHERE res_id=%s AND res_type=%s AND wkf_id=%s AND state=%s', - (new_rid, res_type, wkf_id, 'active')) - new_id = cr.fetchone() - if new_id: - # select all workitems which "wait" for the old instance - cr.execute('select id from wkf_workitem where subflow_id=%s', (old_inst_id,)) - for (item_id,) in cr.fetchall(): - # redirect all those workitems to the wkf instance of the new resource - cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (new_id[0], item_id)) + assert isinstance(new_rid, (long, int)) + return WorkflowService.new(cr, uid, res_type, res_id).redirect(new_rid) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/workflow/helpers.py b/openerp/workflow/helpers.py new file mode 100644 index 00000000000..980eb855461 --- /dev/null +++ b/openerp/workflow/helpers.py @@ -0,0 +1,15 @@ +import openerp.sql_db + +class Session(object): + def __init__(self, cr, uid): + assert isinstance(cr, openerp.sql_db.Cursor) + assert isinstance(uid, (int, long)) + self.cr = cr + self.uid = uid + +class Record(object): + def __init__(self, model, record_id): + assert isinstance(model, basestring) + assert isinstance(record_id, (int, long)) + self.model = model + self.id = record_id \ No newline at end of file diff --git a/openerp/workflow/instance.py b/openerp/workflow/instance.py index eed3e984f42..4db807daaac 100644 --- a/openerp/workflow/instance.py +++ b/openerp/workflow/instance.py @@ -19,57 +19,84 @@ # ############################################################################## import workitem +from helpers import Session +from helpers import Record -def create(cr, ident, wkf_id): - (uid,res_type,res_id) = ident - cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s) RETURNING id', (res_type,res_id,uid,wkf_id)) - id_new = cr.fetchone()[0] - cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (wkf_id,)) - res = cr.dictfetchall() +def create(session, record, workflow_id): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(workflow_id, (int, long)) + + cr = session.cr + cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s) RETURNING id', (record.model, record.id, session.uid, workflow_id)) + instance_id = cr.fetchone()[0] + cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (workflow_id,)) + activities = cr.dictfetchall() stack = [] - workitem.create(cr, res, id_new, ident, stack=stack) - update(cr, id_new, ident) - return id_new + workitem.create(session, record, activities, instance_id, stack=stack) + update(session, record, instance_id) + return instance_id -def delete(cr, ident): - (uid,res_type,res_id) = ident - cr.execute('delete from wkf_instance where res_id=%s and res_type=%s', (res_id,res_type)) +def delete(session, record): + assert isinstance(session, Session) + assert isinstance(record, Record) -def validate(cr, inst_id, ident, signal, force_running=False): - cr.execute("select * from wkf_workitem where inst_id=%s", (inst_id,)) + session.cr.execute('delete from wkf_instance where res_id=%s and res_type=%s', (record.id, record.model)) + +def validate(session, record, instance_id, signal, force_running=False): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(instance_id, (long, int)) + assert isinstance(signal, basestring) + assert isinstance(force_running, bool) + + cr = session.cr + cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) stack = [] - for witem in cr.dictfetchall(): + for work_item in cr.dictfetchall(): stack = [] - workitem.process(cr, witem, ident, signal, force_running, stack=stack) + workitem.process(session, record, work_item, signal, force_running, stack=stack) # An action is returned - _update_end(cr, inst_id, ident) + _update_end(session, record, instance_id) return stack and stack[0] or False -def update(cr, inst_id, ident): - cr.execute("select * from wkf_workitem where inst_id=%s", (inst_id,)) +def update(session, record, instance_id): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(instance_id, (long, int)) + + cr = session.cr + cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) for witem in cr.dictfetchall(): stack = [] - workitem.process(cr, witem, ident, stack=stack) - return _update_end(cr, inst_id, ident) + workitem.process(session, record, witem, stack=stack) + return _update_end(session, record, instance_id) -def _update_end(cr, inst_id, ident): - cr.execute('select wkf_id from wkf_instance where id=%s', (inst_id,)) +def _update_end(session, record, instance_id): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(instance_id, (long, int)) + + cr = session.cr + cr.execute('select wkf_id from wkf_instance where id=%s', (instance_id,)) wkf_id = cr.fetchone()[0] - cr.execute('select state,flow_stop from wkf_workitem w left join wkf_activity a on (a.id=w.act_id) where w.inst_id=%s', (inst_id,)) + cr.execute('select state,flow_stop from wkf_workitem w left join wkf_activity a on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) ok=True for r in cr.fetchall(): if (r[0]<>'complete') or not r[1]: ok=False break if ok: - cr.execute('select distinct a.name from wkf_activity a left join wkf_workitem w on (a.id=w.act_id) where w.inst_id=%s', (inst_id,)) + cr.execute('select distinct a.name from wkf_activity a left join wkf_workitem w on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) act_names = cr.fetchall() - cr.execute("update wkf_instance set state='complete' where id=%s", (inst_id,)) - cr.execute("update wkf_workitem set state='complete' where subflow_id=%s", (inst_id,)) - cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (inst_id,)) - for i in cr.fetchall(): + cr.execute("update wkf_instance set state='complete' where id=%s", (instance_id,)) + cr.execute("update wkf_workitem set state='complete' where subflow_id=%s", (instance_id,)) + cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (instance_id,)) + for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall(): + cur_record = Record(cur_model_name, cur_record_id) for act_name in act_names: - validate(cr, i[0], (ident[0],i[1],i[2]), 'subflow.'+act_name[0]) + validate(session, cur_record, cur_instance_id, 'subflow.%s' % act_name[0]) + return ok diff --git a/openerp/workflow/service.py b/openerp/workflow/service.py new file mode 100644 index 00000000000..08c0f61a7a2 --- /dev/null +++ b/openerp/workflow/service.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-TODAY OpenERP S.A. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from helpers import Session +from helpers import Record + +import instance + + +class WorkflowService(object): + CACHE = {} + + @classmethod + def clear_cache(cls, dbname): + cls.CACHE[dbname] = {} + + @classmethod + def new(cls, cr, uid, model_name, record_id): + return cls(Session(cr, uid), Record(model_name, record_id)) + + def __init__(self, session, record): + assert isinstance(session, Session) + assert isinstance(record, Record) + + self.session = session + self.record = record + + self.cr = self.session.cr + + def write(self): + self.cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', + (self.record.id or None, self.record.model or None, 'active') + ) + for (instance_id,) in self.cr.fetchall(): + instance.update(self.session, self.record, instance_id) + + def trigger(self): + self.cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (self.record.id, self.record.model)) + res = self.cr.fetchall() + for (instance_id,) in res: + self.cr.execute('select %s,res_type,res_id from wkf_instance where id=%s', (self.session.uid, instance_id,)) + current_uid, current_model_name, current_record_id = self.cr.fetchone() + + current_session = Session(self.session.cr, current_uid) + current_record = Record(current_model_name, current_record_id) + + instance.update(current_session, current_record, instance_id) + + def delete(self): + instance.delete(self.session, self.record) + + def create(self): + WorkflowService.CACHE.setdefault(self.cr.dbname, {}) + + wkf_ids = WorkflowService.CACHE[self.cr.dbname].get(self.record.model, None) + + if not wkf_ids: + self.cr.execute('select id from wkf where osv=%s and on_create=True', (self.record.model,)) + wkf_ids = self.cr.fetchall() + WorkflowService.CACHE[self.cr.dbname][self.record.model] = wkf_ids + + for (wkf_id, ) in wkf_ids: + instance.create(self.session, self.record, wkf_id) + + def validate(self, signal): + result = False + # ids of all active workflow instances for a corresponding resource (id, model_nam) + self.cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (self.record.id, self.record.model, 'active')) + for (instance_id,) in self.cr.fetchall(): + res2 = instance.validate(self.session, self.record, instance_id, signal) + result = result or res2 + return result + + def redirect(self, new_rid): + # get ids of wkf instances for the old resource (res_id) + # CHECKME: shouldn't we get only active instances? + self.cr.execute('select id, wkf_id from wkf_instance where res_id=%s and res_type=%s', (self.record.id, self.record.model)) + + for old_inst_id, workflow_id in self.cr.fetchall(): + # first active instance for new resource (new_rid), using same wkf + self.cr.execute( + 'SELECT id '\ + 'FROM wkf_instance '\ + 'WHERE res_id=%s AND res_type=%s AND wkf_id=%s AND state=%s', + (new_rid, self.record.model, workflow_id, 'active')) + new_id = self.cr.fetchone() + if new_id: + # select all workitems which "wait" for the old instance + self.cr.execute('select id from wkf_workitem where subflow_id=%s', (old_inst_id,)) + for (item_id,) in self.cr.fetchall(): + # redirect all those workitems to the wkf instance of the new resource + self.cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (new_id[0], item_id)) + diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py index 6f3be25802d..2c5afb0db48 100644 --- a/openerp/workflow/wkf_expr.py +++ b/openerp/workflow/wkf_expr.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # @@ -15,7 +15,7 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## @@ -51,13 +51,12 @@ class Env(dict): else: return super(Env, self).__getitem__(key) -def _eval_expr(cr, ident, workitem, lines): +def _eval_expr(session, record, workitem, lines): """ Evaluate each line of ``lines`` with the ``Env`` environment, returning the value of the last line. """ assert lines, 'You used a NULL action in a workflow, use dummy node instead.' - uid, model, id = ident result = False for line in lines.split('\n'): line = line.strip() @@ -68,30 +67,29 @@ def _eval_expr(cr, ident, workitem, lines): elif line == 'False': result = False else: - env = Env(cr, uid, model, id) + env = Env(session.cr, session.uid, record.model, record.id) result = eval(line, env, nocopy=True) return result -def execute_action(cr, ident, workitem, activity): +def execute_action(session, record, workitem, activity): """ Evaluate the ir.actions.server action specified in the activity. """ - uid, model, id = ident - ir_actions_server = openerp.registry(cr.dbname)['ir.actions.server'] - context = { 'active_model': model, 'active_id': id, 'active_ids': [id] } - result = ir_actions_server.run(cr, uid, [activity['action_id']], context) + ir_actions_server = openerp.registry(session.cr.dbname)['ir.actions.server'] + context = { 'active_model': record.model, 'active_id': record.id, 'active_ids': [record.id] } + result = ir_actions_server.run(session.cr, session.uid, [activity['action_id']], context) return result -def execute(cr, ident, workitem, activity): +def execute(session, record, workitem, activity): """ Evaluate the action specified in the activity. """ - return _eval_expr(cr, ident, workitem, activity['action']) + return _eval_expr(session, record, workitem, activity['action']) -def check(cr, workitem, ident, transition, signal): +def check(session, record, workitem, transition, signal): """ Test if a transition can be taken. The transition can be taken if: - + - the signal name matches, - the uid is SUPERUSER_ID or the user groups contains the transition's group, @@ -100,14 +98,13 @@ def check(cr, workitem, ident, transition, signal): if transition['signal'] and signal != transition['signal']: return False - uid = ident[0] - if uid != openerp.SUPERUSER_ID and transition['group_id']: - registry = openerp.registry(cr.dbname) - user_groups = registry['res.users'].read(cr, uid, [uid], ['groups_id'])[0]['groups_id'] + if session.uid != openerp.SUPERUSER_ID and transition['group_id']: + registry = openerp.registry(session.cr.dbname) + user_groups = registry['res.users'].read(session.cr, session.uid, [session.uid], ['groups_id'])[0]['groups_id'] if transition['group_id'] not in user_groups: return False - return _eval_expr(cr, ident, workitem, transition['condition']) + return _eval_expr(session, record, workitem, transition['condition']) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/workflow/workitem.py b/openerp/workflow/workitem.py index a5639fa0c7c..f6dd5128d96 100644 --- a/openerp/workflow/workitem.py +++ b/openerp/workflow/workitem.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + ############################################################################## # # OpenERP, Open Source Management Solution @@ -28,41 +28,59 @@ import logging import instance import wkf_expr +from helpers import Session +from helpers import Record logger = logging.getLogger(__name__) -def create(cr, act_datas, inst_id, ident, stack): - for act in act_datas: +def create(session, record, activities, instance_id, stack): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(activities, list) + assert isinstance(instance_id, (long, int)) + assert isinstance(stack, list) + cr = session.cr + + ident = session.uid, record.model, record.id + for activity in activities: cr.execute("select nextval('wkf_workitem_id_seq')") id_new = cr.fetchone()[0] - cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, act['id'], inst_id)) + cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id)) cr.execute('select * from wkf_workitem where id=%s',(id_new,)) - res = cr.dictfetchone() + work_item = cr.dictfetchone() logger.info('Created workflow item in activity %s', - act['id'], extra={'ident': ident}) - process(cr, res, ident, stack=stack) + activity['id'], + extra={'ident': (session.uid, record.model, record.id)}) + + process(session, record, work_item, stack=stack) + +def process(session, record, workitem, signal=None, force_running=False, stack=None): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(force_running, bool) -def process(cr, workitem, ident, signal=None, force_running=False, stack=None): assert stack is not None + cr = session.cr + cr.execute('select * from wkf_activity where id=%s', (workitem['act_id'],)) activity = cr.dictfetchone() triggers = False if workitem['state'] == 'active': triggers = True - if not _execute(cr, workitem, activity, ident, stack): + if not _execute(session, record, workitem, activity, stack): return False if force_running or workitem['state'] == 'complete': - ok = _split_test(cr, workitem, activity['split_mode'], ident, signal, stack) + ok = _split_test(session, record, workitem, activity['split_mode'], signal, stack) triggers = triggers and not ok if triggers: cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) for trans in cr.dictfetchall(): if trans['trigger_model']: - ids = wkf_expr._eval_expr(cr,ident,workitem,trans['trigger_expr_id']) + ids = wkf_expr._eval_expr(session, record, workitem, trans['trigger_expr_id']) for res_id in ids: cr.execute('select nextval(\'wkf_triggers_id_seq\')') id =cr.fetchone()[0] @@ -73,26 +91,32 @@ def process(cr, workitem, ident, signal=None, force_running=False, stack=None): # ---------------------- PRIVATE FUNCS -------------------------------- -def _state_set(cr, workitem, activity, state, ident): - cr.execute('update wkf_workitem set state=%s where id=%s', (state,workitem['id'])) - workitem['state'] = state - logger.info("Changed state of work item %s to \"%s\" in activity %s", - workitem['id'], state, activity['id'], extra={'ident': ident}) +# def new_state_set(session, record, workitem, activity, state): -def _execute(cr, workitem, activity, ident, stack): +def _state_set(session, record, workitem, activity, state): + session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, workitem['id'])) + workitem['state'] = state + logger.info('Changed state of work item %s to "%s" in activity %s', + workitem['id'], state, activity['id'], + extra={'ident': (session.uid, record.model, record.id)}) + +def _execute(session, record, workitem, activity, stack): result = True # # send a signal to parent workflow (signal: subflow.signal_name) # + cr = session.cr + ident = (session.uid, record.model, record.id) signal_todo = [] if (workitem['state']=='active') and activity['signal_send']: cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (workitem['inst_id'],)) - for i in cr.fetchall(): - signal_todo.append((i[0], (ident[0],i[1],i[2]), activity['signal_send'])) + for instance_id, model_name, record_id in cr.fetchall(): + record = Record(model_name, record_id) + signal_todo.append((instance_id, record, activity['signal_send'])) if activity['kind']=='dummy': if workitem['state']=='active': - _state_set(cr, workitem, activity, 'complete', ident) + _state_set(session, record, workitem, activity, 'complete') if activity['action_id']: res2 = wkf_expr.execute_action(cr, ident, workitem, activity) if res2: @@ -100,29 +124,29 @@ def _execute(cr, workitem, activity, ident, stack): result=res2 elif activity['kind']=='function': if workitem['state']=='active': - _state_set(cr, workitem, activity, 'running', ident) - returned_action = wkf_expr.execute(cr, ident, workitem, activity) + _state_set(session, record, workitem, activity, 'running') + returned_action = wkf_expr.execute(session, record, workitem, activity) if type(returned_action) in (dict,): stack.append(returned_action) if activity['action_id']: - res2 = wkf_expr.execute_action(cr, ident, workitem, activity) + res2 = wkf_expr.execute_action(session, record, workitem, activity) # A client action has been returned if res2: stack.append(res2) result=res2 - _state_set(cr, workitem, activity, 'complete', ident) + _state_set(session, record, workitem, activity, 'complete') elif activity['kind']=='stopall': if workitem['state']=='active': - _state_set(cr, workitem, activity, 'running', ident) + _state_set(session, record, workitem, activity, 'running') cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (workitem['inst_id'], workitem['id'])) if activity['action']: - wkf_expr.execute(cr, ident, workitem, activity) - _state_set(cr, workitem, activity, 'complete', ident) + wkf_expr.execute(session, record, workitem, activity) + _state_set(session, record, workitem, activity, 'complete') elif activity['kind']=='subflow': if workitem['state']=='active': - _state_set(cr, workitem, activity, 'running', ident) + _state_set(session, record, workitem, activity, 'running') if activity.get('action', False): - id_new = wkf_expr.execute(cr, ident, workitem, activity) + id_new = wkf_expr.execute(session, record, workitem, activity) if not id_new: cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) return False @@ -130,27 +154,29 @@ def _execute(cr, workitem, activity, ident, stack): cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new,activity['subflow_id'])) id_new = cr.fetchone()[0] else: - id_new = instance.create(cr, ident, activity['subflow_id']) + id_new = instance.create(session, record, activity['subflow_id']) cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, workitem['id'])) workitem['subflow_id'] = id_new if workitem['state']=='running': cr.execute("select state from wkf_instance where id=%s", (workitem['subflow_id'],)) state= cr.fetchone()[0] if state=='complete': - _state_set(cr, workitem, activity, 'complete', ident) - for t in signal_todo: - instance.validate(cr, t[0], t[1], t[2], force_running=True) + _state_set(session, record, workitem, activity, 'complete') + + for instance_id, record, signal_send in signal_todo: + instance.validate(session, record, signal_send, force_running=True) return result -def _split_test(cr, workitem, split_mode, ident, signal=None, stack=None): +def _split_test(session, record, workitem, split_mode, signal, stack): + cr = session.cr cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) test = False transitions = [] alltrans = cr.dictfetchall() if split_mode=='XOR' or split_mode=='OR': for transition in alltrans: - if wkf_expr.check(cr, workitem, ident, transition,signal): + if wkf_expr.check(session, record, workitem, transition,signal): test = True transitions.append((transition['id'], workitem['inst_id'])) if split_mode=='XOR': @@ -158,7 +184,7 @@ def _split_test(cr, workitem, split_mode, ident, signal=None, stack=None): else: test = True for transition in alltrans: - if not wkf_expr.check(cr, workitem, ident, transition,signal): + if not wkf_expr.check(session, record, workitem, transition,signal): test = False break cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], workitem['inst_id'])) @@ -168,15 +194,17 @@ def _split_test(cr, workitem, split_mode, ident, signal=None, stack=None): cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions) cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) for t in transitions: - _join_test(cr, t[0], t[1], ident, stack) + _join_test(session, record, t[0], t[1], stack) return True return False -def _join_test(cr, trans_id, inst_id, ident, stack): +def _join_test(session, record, trans_id, inst_id, stack): + # cr, trans_id, inst_id, ident, stack): + cr = session.cr cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,)) activity = cr.dictfetchone() if activity['join_mode']=='XOR': - create(cr,[activity], inst_id, ident, stack) + create(session, record, [activity], inst_id, stack) cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id)) else: cr.execute('select id from wkf_transition where act_to=%s', (activity['id'],)) @@ -191,7 +219,7 @@ def _join_test(cr, trans_id, inst_id, ident, stack): if ok: for (id,) in trans_ids: cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) - create(cr, [activity], inst_id, ident, stack) + create(session, record, [activity], inst_id, stack) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From fad743a9788275b5b0bdfb72875005765bd97345 Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Wed, 23 Oct 2013 14:58:18 +0200 Subject: [PATCH 09/64] [REF] Remove old code from the workflow engine bzr revid: stw@openerp.com-20131023125818-3g3fe49lfrube9kq --- openerp/workflow/workitem.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openerp/workflow/workitem.py b/openerp/workflow/workitem.py index f6dd5128d96..6ee99fd787d 100644 --- a/openerp/workflow/workitem.py +++ b/openerp/workflow/workitem.py @@ -41,7 +41,6 @@ def create(session, record, activities, instance_id, stack): assert isinstance(stack, list) cr = session.cr - ident = session.uid, record.model, record.id for activity in activities: cr.execute("select nextval('wkf_workitem_id_seq')") id_new = cr.fetchone()[0] @@ -91,8 +90,6 @@ def process(session, record, workitem, signal=None, force_running=False, stack=N # ---------------------- PRIVATE FUNCS -------------------------------- -# def new_state_set(session, record, workitem, activity, state): - def _state_set(session, record, workitem, activity, state): session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, workitem['id'])) workitem['state'] = state @@ -106,7 +103,6 @@ def _execute(session, record, workitem, activity, stack): # send a signal to parent workflow (signal: subflow.signal_name) # cr = session.cr - ident = (session.uid, record.model, record.id) signal_todo = [] if (workitem['state']=='active') and activity['signal_send']: cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (workitem['inst_id'],)) @@ -118,7 +114,7 @@ def _execute(session, record, workitem, activity, stack): if workitem['state']=='active': _state_set(session, record, workitem, activity, 'complete') if activity['action_id']: - res2 = wkf_expr.execute_action(cr, ident, workitem, activity) + res2 = wkf_expr.execute_action(session, record, workitem, activity) if res2: stack.append(res2) result=res2 @@ -199,7 +195,6 @@ def _split_test(session, record, workitem, split_mode, signal, stack): return False def _join_test(session, record, trans_id, inst_id, stack): - # cr, trans_id, inst_id, ident, stack): cr = session.cr cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,)) activity = cr.dictfetchone() From 6a3a2709efe78b2440480acb33d02e5e9f025b55 Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Wed, 23 Oct 2013 15:07:05 +0200 Subject: [PATCH 10/64] [REF] Remove the useless wkf_expr.py file and copy it into workitem.py bzr revid: stw@openerp.com-20131023130705-k4u7ssba820b7hwi --- openerp/workflow/instance.py | 4 +- openerp/workflow/wkf_expr.py | 111 -------------------------------- openerp/workflow/workitem.py | 119 ++++++++++++++++++++++++++++++----- 3 files changed, 105 insertions(+), 129 deletions(-) delete mode 100644 openerp/workflow/wkf_expr.py diff --git a/openerp/workflow/instance.py b/openerp/workflow/instance.py index 4db807daaac..3d7b8cbb04e 100644 --- a/openerp/workflow/instance.py +++ b/openerp/workflow/instance.py @@ -19,8 +19,8 @@ # ############################################################################## import workitem -from helpers import Session -from helpers import Record +from openerp.workflow.helpers import Session +from openerp.workflow.helpers import Record def create(session, record, workflow_id): assert isinstance(session, Session) diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py deleted file mode 100644 index 2c5afb0db48..00000000000 --- a/openerp/workflow/wkf_expr.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -""" -Evaluate workflow code found in activity actions and transition conditions. -""" - -import openerp -from openerp.tools.safe_eval import safe_eval as eval - -class Env(dict): - """ - Dictionary class used as an environment to evaluate workflow code (such as - the condition on transitions). - - This environment provides sybmols for cr, uid, id, model name, model - instance, column names, and all the record (the one obtained by browsing - the provided ID) attributes. - """ - def __init__(self, cr, uid, model, id): - self.cr = cr - self.uid = uid - self.model = model - self.id = id - self.ids = [id] - self.obj = openerp.registry(cr.dbname)[model] - self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys() - - def __getitem__(self, key): - if (key in self.columns) or (key in dir(self.obj)): - res = self.obj.browse(self.cr, self.uid, self.id) - return res[key] - else: - return super(Env, self).__getitem__(key) - -def _eval_expr(session, record, workitem, lines): - """ - Evaluate each line of ``lines`` with the ``Env`` environment, returning - the value of the last line. - """ - assert lines, 'You used a NULL action in a workflow, use dummy node instead.' - result = False - for line in lines.split('\n'): - line = line.strip() - if not line: - continue - if line == 'True': - result = True - elif line == 'False': - result = False - else: - env = Env(session.cr, session.uid, record.model, record.id) - result = eval(line, env, nocopy=True) - return result - -def execute_action(session, record, workitem, activity): - """ - Evaluate the ir.actions.server action specified in the activity. - """ - ir_actions_server = openerp.registry(session.cr.dbname)['ir.actions.server'] - context = { 'active_model': record.model, 'active_id': record.id, 'active_ids': [record.id] } - result = ir_actions_server.run(session.cr, session.uid, [activity['action_id']], context) - return result - -def execute(session, record, workitem, activity): - """ - Evaluate the action specified in the activity. - """ - return _eval_expr(session, record, workitem, activity['action']) - -def check(session, record, workitem, transition, signal): - """ - Test if a transition can be taken. The transition can be taken if: - - - the signal name matches, - - the uid is SUPERUSER_ID or the user groups contains the transition's - group, - - the condition evaluates to a truish value. - """ - if transition['signal'] and signal != transition['signal']: - return False - - if session.uid != openerp.SUPERUSER_ID and transition['group_id']: - registry = openerp.registry(session.cr.dbname) - user_groups = registry['res.users'].read(session.cr, session.uid, [session.uid], ['groups_id'])[0]['groups_id'] - if transition['group_id'] not in user_groups: - return False - - return _eval_expr(session, record, workitem, transition['condition']) - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/workflow/workitem.py b/openerp/workflow/workitem.py index 6ee99fd787d..5c2fc580cd0 100644 --- a/openerp/workflow/workitem.py +++ b/openerp/workflow/workitem.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). +# Copyright (C) 2004-2009 OpenERP S.A. (%s', (workitem['inst_id'], workitem['id'])) if activity['action']: - wkf_expr.execute(session, record, workitem, activity) + wkf_expr_execute(session, record, workitem, activity) _state_set(session, record, workitem, activity, 'complete') + elif activity['kind']=='subflow': if workitem['state']=='active': _state_set(session, record, workitem, activity, 'running') if activity.get('action', False): - id_new = wkf_expr.execute(session, record, workitem, activity) + id_new = wkf_expr_execute(session, record, workitem, activity) if not id_new: cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) return False @@ -172,7 +259,7 @@ def _split_test(session, record, workitem, split_mode, signal, stack): alltrans = cr.dictfetchall() if split_mode=='XOR' or split_mode=='OR': for transition in alltrans: - if wkf_expr.check(session, record, workitem, transition,signal): + if wkf_expr_check(session, record, workitem, transition,signal): test = True transitions.append((transition['id'], workitem['inst_id'])) if split_mode=='XOR': @@ -180,7 +267,7 @@ def _split_test(session, record, workitem, split_mode, signal, stack): else: test = True for transition in alltrans: - if not wkf_expr.check(session, record, workitem, transition,signal): + if not wkf_expr_check(session, record, workitem, transition,signal): test = False break cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], workitem['inst_id'])) From f6c1b6bb8638f9ea4d2c68cc73443a02a6298d7c Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Wed, 23 Oct 2013 15:58:38 +0200 Subject: [PATCH 11/64] [REF] Create a WorkflowItem object bzr revid: stw@openerp.com-20131023135838-ix7x6mzui3i3wfbi --- openerp/workflow/helpers.py | 9 +- openerp/workflow/instance.py | 14 +- openerp/workflow/workitem.py | 379 ++++++++++++++++++----------------- 3 files changed, 213 insertions(+), 189 deletions(-) diff --git a/openerp/workflow/helpers.py b/openerp/workflow/helpers.py index 980eb855461..e41d178be90 100644 --- a/openerp/workflow/helpers.py +++ b/openerp/workflow/helpers.py @@ -12,4 +12,11 @@ class Record(object): assert isinstance(model, basestring) assert isinstance(record_id, (int, long)) self.model = model - self.id = record_id \ No newline at end of file + self.id = record_id + +class WorkflowActivity(object): + KIND_FUNCTION = 'function' + KIND_DUMMY = 'dummy' + KIND_STOPALL = 'stopall' + KIND_SUBFLOW = 'subflow' + diff --git a/openerp/workflow/instance.py b/openerp/workflow/instance.py index 3d7b8cbb04e..befbd5970f7 100644 --- a/openerp/workflow/instance.py +++ b/openerp/workflow/instance.py @@ -21,6 +21,7 @@ import workitem from openerp.workflow.helpers import Session from openerp.workflow.helpers import Record +from openerp.workflow.workitem import WorkflowItem def create(session, record, workflow_id): assert isinstance(session, Session) @@ -33,7 +34,7 @@ def create(session, record, workflow_id): cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (workflow_id,)) activities = cr.dictfetchall() stack = [] - workitem.create(session, record, activities, instance_id, stack=stack) + WorkflowItem(session, record).create(activities, instance_id, stack) update(session, record, instance_id) return instance_id @@ -53,9 +54,10 @@ def validate(session, record, instance_id, signal, force_running=False): cr = session.cr cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) stack = [] + wi = WorkflowItem(session, record) for work_item in cr.dictfetchall(): - stack = [] - workitem.process(session, record, work_item, signal, force_running, stack=stack) + # stack = [] + wi.process(work_item, signal, force_running, stack=stack) # An action is returned _update_end(session, record, instance_id) return stack and stack[0] or False @@ -67,9 +69,11 @@ def update(session, record, instance_id): cr = session.cr cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) - for witem in cr.dictfetchall(): + wi = WorkflowItem(session, record) + + for work_item in cr.dictfetchall(): stack = [] - workitem.process(session, record, witem, stack=stack) + wi.process(work_item, stack=stack) return _update_end(session, record, instance_id) def _update_end(session, record, instance_id): diff --git a/openerp/workflow/workitem.py b/openerp/workflow/workitem.py index 5c2fc580cd0..7d0afcb519f 100644 --- a/openerp/workflow/workitem.py +++ b/openerp/workflow/workitem.py @@ -28,13 +28,14 @@ import instance from openerp.workflow.helpers import Session from openerp.workflow.helpers import Record +from openerp.workflow.helpers import WorkflowActivity logger = logging.getLogger(__name__) import openerp from openerp.tools.safe_eval import safe_eval as eval -class Env(dict): +class Environment(dict): """ Dictionary class used as an environment to evaluate workflow code (such as the condition on transitions). @@ -43,13 +44,13 @@ class Env(dict): instance, column names, and all the record (the one obtained by browsing the provided ID) attributes. """ - def __init__(self, cr, uid, model, id): - self.cr = cr - self.uid = uid - self.model = model - self.id = id - self.ids = [id] - self.obj = openerp.registry(cr.dbname)[model] + def __init__(self, session, record): + self.cr = session.cr + self.uid = session.uid + self.model = record.model + self.id = record.id + self.ids = [record.id] + self.obj = openerp.registry(self.cr.dbname)[self.model] self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys() def __getitem__(self, key): @@ -57,11 +58,11 @@ class Env(dict): res = self.obj.browse(self.cr, self.uid, self.id) return res[key] else: - return super(Env, self).__getitem__(key) + return super(Environment, self).__getitem__(key) def wkf_expr_eval_expr(session, record, workitem, lines): """ - Evaluate each line of ``lines`` with the ``Env`` environment, returning + Evaluate each line of ``lines`` with the ``Environment`` environment, returning the value of the last line. """ assert lines, 'You used a NULL action in a workflow, use dummy node instead.' @@ -75,7 +76,7 @@ def wkf_expr_eval_expr(session, record, workitem, lines): elif line == 'False': result = False else: - env = Env(session.cr, session.uid, record.model, record.id) + env = Environment(session, record) result = eval(line, env, nocopy=True) return result @@ -115,193 +116,205 @@ def wkf_expr_check(session, record, workitem, transition, signal): return wkf_expr_eval_expr(session, record, workitem, transition['condition']) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +class WorkflowItem(object): + def __init__(self, session, record): + assert isinstance(session, Session) + assert isinstance(record, Record) + self.session = session + self.record = record + def create(self, activities, instance_id, stack): + assert isinstance(activities, list) + assert isinstance(instance_id, (long, int)) + assert isinstance(stack, list) + cr = self.session.cr + for activity in activities: + cr.execute("select nextval('wkf_workitem_id_seq')") + id_new = cr.fetchone()[0] + cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id)) + cr.execute('select * from wkf_workitem where id=%s',(id_new,)) + work_item = cr.dictfetchone() + logger.info('Created workflow item in activity %s', + activity['id'], + extra={'ident': (self.session.uid, self.record.model, self.record.id)}) -def create(session, record, activities, instance_id, stack): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(activities, list) - assert isinstance(instance_id, (long, int)) - assert isinstance(stack, list) - cr = session.cr + self.process(work_item, stack=stack) - for activity in activities: - cr.execute("select nextval('wkf_workitem_id_seq')") - id_new = cr.fetchone()[0] - cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id)) - cr.execute('select * from wkf_workitem where id=%s',(id_new,)) - work_item = cr.dictfetchone() - logger.info('Created workflow item in activity %s', - activity['id'], - extra={'ident': (session.uid, record.model, record.id)}) + def process(self, workitem, signal=None, force_running=False, stack=None): + assert isinstance(workitem, dict) + assert isinstance(force_running, bool) - process(session, record, work_item, stack=stack) + # return _process(self.session, self.record, work_item, signal=signal, force_running=force_running, stack=stack) + # assert isinstance(force_running, bool) -def process(session, record, workitem, signal=None, force_running=False, stack=None): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(force_running, bool) + assert stack is not None - assert stack is not None + cr = self.session.cr - cr = session.cr + cr.execute('select * from wkf_activity where id=%s', (workitem['act_id'],)) + activity = cr.dictfetchone() - cr.execute('select * from wkf_activity where id=%s', (workitem['act_id'],)) - activity = cr.dictfetchone() + triggers = False + if workitem['state'] == 'active': + triggers = True + if not self._execute(workitem, activity, stack): + return False - triggers = False - if workitem['state'] == 'active': - triggers = True - if not _execute(session, record, workitem, activity, stack): - return False + if force_running or workitem['state'] == 'complete': + ok = self._split_test(workitem, activity['split_mode'], signal, stack) + triggers = triggers and not ok - if force_running or workitem['state'] == 'complete': - ok = _split_test(session, record, workitem, activity['split_mode'], signal, stack) - triggers = triggers and not ok + if triggers: + cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) + for trans in cr.dictfetchall(): + if trans['trigger_model']: + ids = wkf_expr_eval_expr(self.session, self.record, workitem, trans['trigger_expr_id']) + for res_id in ids: + cr.execute('select nextval(\'wkf_triggers_id_seq\')') + id =cr.fetchone()[0] + cr.execute('insert into wkf_triggers (model,res_id,instance_id,workitem_id,id) values (%s,%s,%s,%s,%s)', (trans['trigger_model'],res_id,workitem['inst_id'], workitem['id'], id)) - if triggers: - cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) - for trans in cr.dictfetchall(): - if trans['trigger_model']: - ids = wkf_expr_eval_expr(session, record, workitem, trans['trigger_expr_id']) - for res_id in ids: - cr.execute('select nextval(\'wkf_triggers_id_seq\')') - id =cr.fetchone()[0] - cr.execute('insert into wkf_triggers (model,res_id,instance_id,workitem_id,id) values (%s,%s,%s,%s,%s)', (trans['trigger_model'],res_id,workitem['inst_id'], workitem['id'], id)) - - return True - - -# ---------------------- PRIVATE FUNCS -------------------------------- - -def _state_set(session, record, workitem, activity, state): - session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, workitem['id'])) - workitem['state'] = state - logger.info('Changed state of work item %s to "%s" in activity %s', - workitem['id'], state, activity['id'], - extra={'ident': (session.uid, record.model, record.id)}) - -def _execute(session, record, workitem, activity, stack): - """Send a signal to parenrt workflow (signal: subflow.signal_name)""" - result = True - cr = session.cr - signal_todo = [] - if (workitem['state']=='active') and activity['signal_send']: - cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (workitem['inst_id'],)) - for instance_id, model_name, record_id in cr.fetchall(): - record = Record(model_name, record_id) - signal_todo.append((instance_id, record, activity['signal_send'])) - - if activity['kind']=='dummy': - if workitem['state']=='active': - _state_set(session, record, workitem, activity, 'complete') - if activity['action_id']: - res2 = wkf_expr_execute_action(session, record, workitem, activity) - if res2: - stack.append(res2) - result=res2 - - elif activity['kind']=='function': - if workitem['state']=='active': - _state_set(session, record, workitem, activity, 'running') - returned_action = wkf_expr_execute(session, record, workitem, activity) - if type(returned_action) in (dict,): - stack.append(returned_action) - if activity['action_id']: - res2 = wkf_expr_execute_action(session, record, workitem, activity) - # A client action has been returned - if res2: - stack.append(res2) - result=res2 - _state_set(session, record, workitem, activity, 'complete') - - elif activity['kind']=='stopall': - if workitem['state']=='active': - _state_set(session, record, workitem, activity, 'running') - cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (workitem['inst_id'], workitem['id'])) - if activity['action']: - wkf_expr_execute(session, record, workitem, activity) - _state_set(session, record, workitem, activity, 'complete') - - elif activity['kind']=='subflow': - if workitem['state']=='active': - _state_set(session, record, workitem, activity, 'running') - if activity.get('action', False): - id_new = wkf_expr_execute(session, record, workitem, activity) - if not id_new: - cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) - return False - assert type(id_new)==type(1) or type(id_new)==type(1L), 'Wrong return value: '+str(id_new)+' '+str(type(id_new)) - cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new,activity['subflow_id'])) - id_new = cr.fetchone()[0] - else: - id_new = instance.create(session, record, activity['subflow_id']) - cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, workitem['id'])) - workitem['subflow_id'] = id_new - if workitem['state']=='running': - cr.execute("select state from wkf_instance where id=%s", (workitem['subflow_id'],)) - state= cr.fetchone()[0] - if state=='complete': - _state_set(session, record, workitem, activity, 'complete') - - for instance_id, record, signal_send in signal_todo: - instance.validate(session, record, signal_send, force_running=True) - - return result - -def _split_test(session, record, workitem, split_mode, signal, stack): - cr = session.cr - cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) - test = False - transitions = [] - alltrans = cr.dictfetchall() - if split_mode=='XOR' or split_mode=='OR': - for transition in alltrans: - if wkf_expr_check(session, record, workitem, transition,signal): - test = True - transitions.append((transition['id'], workitem['inst_id'])) - if split_mode=='XOR': - break - else: - test = True - for transition in alltrans: - if not wkf_expr_check(session, record, workitem, transition,signal): - test = False - break - cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], workitem['inst_id'])) - if not cr.fetchone()[0]: - transitions.append((transition['id'], workitem['inst_id'])) - if test and len(transitions): - cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions) - cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) - for t in transitions: - _join_test(session, record, t[0], t[1], stack) return True - return False -def _join_test(session, record, trans_id, inst_id, stack): - cr = session.cr - cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,)) - activity = cr.dictfetchone() - if activity['join_mode']=='XOR': - create(session, record, [activity], inst_id, stack) - cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id)) - else: - cr.execute('select id from wkf_transition where act_to=%s', (activity['id'],)) - trans_ids = cr.fetchall() - ok = True - for (id,) in trans_ids: - cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) - res = cr.fetchone()[0] - if not res: - ok = False - break - if ok: + def _execute(self, workitem, activity, stack): + """Send a signal to parenrt workflow (signal: subflow.signal_name)""" + result = True + cr = self.session.cr + signal_todo = [] + + if (workitem['state']=='active') and activity['signal_send']: + # signal_send']: + cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (workitem['inst_id'],)) + for instance_id, model_name, record_id in cr.fetchall(): + record = Record(model_name, record_id) + signal_todo.append((instance_id, record, activity['signal_send'])) + + + if activity['kind'] == WorkflowActivity.KIND_DUMMY: + if workitem['state']=='active': + self._state_set(workitem, activity, 'complete') + if activity['action_id']: + res2 = wkf_expr_execute_action(self.session, self.record, workitem, activity) + if res2: + stack.append(res2) + result=res2 + + elif activity['kind'] == WorkflowActivity.KIND_FUNCTION: + + if workitem['state']=='active': + self._state_set(workitem, activity, 'running') + returned_action = wkf_expr_execute(self.session, self.record, workitem, activity) + if type(returned_action) in (dict,): + stack.append(returned_action) + if activity['action_id']: + res2 = wkf_expr_execute_action(self.session, self.record, workitem, activity) + # A client action has been returned + if res2: + stack.append(res2) + result=res2 + self._state_set(workitem, activity, 'complete') + + elif activity['kind'] == WorkflowActivity.KIND_STOPALL: + if workitem['state']=='active': + self._state_set(workitem, activity, 'running') + cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (workitem['inst_id'], workitem['id'])) + if activity['action']: + wkf_expr_execute(self.session, self.record, workitem, activity) + self._state_set(workitem, activity, 'complete') + + elif activity['kind'] == WorkflowActivity.KIND_SUBFLOW: + + if workitem['state']=='active': + + self._state_set(workitem, activity, 'running') + if activity.get('action', False): + id_new = wkf_expr_execute(self.session, self.record, workitem, activity) + if not id_new: + cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) + return False + assert type(id_new)==type(1) or type(id_new)==type(1L), 'Wrong return value: '+str(id_new)+' '+str(type(id_new)) + cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new,activity['subflow_id'])) + id_new = cr.fetchone()[0] + else: + id_new = instance.create(self.session, self.record, activity['subflow_id']) + cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, workitem['id'])) + workitem['subflow_id'] = id_new + + if workitem['state']=='running': + cr.execute("select state from wkf_instance where id=%s", (workitem['subflow_id'],)) + state= cr.fetchone()[0] + if state=='complete': + self._state_set(workitem, activity, 'complete') + + for instance_id, record, signal_send in signal_todo: + instance.validate(self.session, self.record, signal_send, force_running=True) + + return result + + def _state_set(self, workitem, activity, state): + self.session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, workitem['id'])) + workitem['state'] = state + logger.info('Changed state of work item %s to "%s" in activity %s', + workitem['id'], state, activity['id'], + extra={'ident': (self.session.uid, self.record.model, self.record.id)}) + + + + + def _split_test(self, workitem, split_mode, signal, stack): + cr = self.session.cr + cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) + test = False + transitions = [] + alltrans = cr.dictfetchall() + + if split_mode in ('XOR', 'OR'): + for transition in alltrans: + if wkf_expr_check(self.session, self.record, workitem, transition,signal): + test = True + transitions.append((transition['id'], workitem['inst_id'])) + if split_mode=='XOR': + break + else: + test = True + for transition in alltrans: + if not wkf_expr_check(self.session, self.record, workitem, transition,signal): + test = False + break + cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], workitem['inst_id'])) + if not cr.fetchone()[0]: + transitions.append((transition['id'], workitem['inst_id'])) + + if test and transitions: + cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions) + cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) + for t in transitions: + self._join_test(t[0], t[1], stack) + return True + return False + + def _join_test(self, trans_id, inst_id, stack): + cr = self.session.cr + cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,)) + activity = cr.dictfetchone() + if activity['join_mode']=='XOR': + self.create([activity], inst_id, stack) + cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id)) + else: + cr.execute('select id from wkf_transition where act_to=%s', (activity['id'],)) + trans_ids = cr.fetchall() + ok = True for (id,) in trans_ids: - cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) - create(session, record, [activity], inst_id, stack) + cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) + res = cr.fetchone()[0] + if not res: + ok = False + break + if ok: + for (id,) in trans_ids: + cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) + self.create([activity], inst_id, stack) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 1c398cd6f89a4979eade863f8aa89ccaabf52132 Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Thu, 24 Oct 2013 09:57:07 +0200 Subject: [PATCH 12/64] [REF] Start to abstract the Workflow engine bzr revid: stw@openerp.com-20131024075707-k1092bspi6ebixdw --- openerp/workflow/instance.py | 166 ++++++++++++--------- openerp/workflow/service.py | 17 ++- openerp/workflow/workitem.py | 275 +++++++++++++++++++---------------- 3 files changed, 253 insertions(+), 205 deletions(-) diff --git a/openerp/workflow/instance.py b/openerp/workflow/instance.py index befbd5970f7..223e90cca31 100644 --- a/openerp/workflow/instance.py +++ b/openerp/workflow/instance.py @@ -23,85 +23,111 @@ from openerp.workflow.helpers import Session from openerp.workflow.helpers import Record from openerp.workflow.workitem import WorkflowItem -def create(session, record, workflow_id): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(workflow_id, (int, long)) +class WorkflowInstance(object): + def __init__(self, session, record, values): + assert isinstance(session, Session) + assert isinstance(record, Record) + self.session = session + self.record = record - cr = session.cr - cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s) RETURNING id', (record.model, record.id, session.uid, workflow_id)) - instance_id = cr.fetchone()[0] - cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (workflow_id,)) - activities = cr.dictfetchall() - stack = [] - WorkflowItem(session, record).create(activities, instance_id, stack) - update(session, record, instance_id) - return instance_id + if not values: + values = {} + + assert isinstance(values, dict) + self.instance = values + + @classmethod + def create(cls, session, record, workflow_id): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(workflow_id, (int, long)) + + cr = session.cr + cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s) RETURNING id', (record.model, record.id, session.uid, workflow_id)) + instance_id = cr.fetchone()[0] + + cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (workflow_id,)) + stack = [] + + activities = cr.dictfetchall() + for activity in activities: + WorkflowItem.create(session, record, activity, instance_id, stack) + + cr.execute('SELECT * FROM wkf_instance WHERE id = %s', (instance_id,)) + values = cr.dictfetchone() + wi = WorkflowInstance(session, record, values) + wi.update() + + return wi + + def delete(self): + self.session.cr.execute('delete from wkf_instance where res_id=%s and res_type=%s', (self.record.id, self.record.model)) + + def validate(self, signal, force_running=False): + assert isinstance(signal, basestring) + assert isinstance(force_running, bool) + + cr = self.session.cr + cr.execute("select * from wkf_workitem where inst_id=%s", (self.instance['id'],)) + stack = [] + for work_item_values in cr.dictfetchall(): + wi = WorkflowItem(self.session, self.record, work_item_values) + wi.process(signal=signal, force_running=force_running, stack=stack) + # An action is returned + self._update_end() + return stack and stack[0] or False + + def update(self): + cr = self.session.cr + + cr.execute("select * from wkf_workitem where inst_id=%s", (self.instance['id'],)) + for work_item_values in cr.dictfetchall(): + stack = [] + WorkflowItem(self.session, self.record, work_item_values).process(stack=stack) + return self._update_end() + + def _update_end(self): + cr = self.session.cr + instance_id = self.instance['id'] + cr.execute('select wkf_id from wkf_instance where id=%s', (instance_id,)) + wkf_id = cr.fetchone()[0] + cr.execute('select state,flow_stop from wkf_workitem w left join wkf_activity a on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) + ok=True + for r in cr.fetchall(): + if (r[0]<>'complete') or not r[1]: + ok=False + break + if ok: + cr.execute('select distinct a.name from wkf_activity a left join wkf_workitem w on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) + act_names = cr.fetchall() + cr.execute("update wkf_instance set state='complete' where id=%s", (instance_id,)) + cr.execute("update wkf_workitem set state='complete' where subflow_id=%s", (instance_id,)) + cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (instance_id,)) + for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall(): + cur_record = Record(cur_model_name, cur_record_id) + for act_name in act_names: + WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.{}'.format(act_name[0])) + + return ok + + + + + +def create(session, record, workflow_id): + return WorkflowInstance(session, record).create(workflow_id) def delete(session, record): - assert isinstance(session, Session) - assert isinstance(record, Record) - - session.cr.execute('delete from wkf_instance where res_id=%s and res_type=%s', (record.id, record.model)) + return WorkflowInstance(session, record).delete() def validate(session, record, instance_id, signal, force_running=False): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(instance_id, (long, int)) - assert isinstance(signal, basestring) - assert isinstance(force_running, bool) - - cr = session.cr - cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) - stack = [] - wi = WorkflowItem(session, record) - for work_item in cr.dictfetchall(): - # stack = [] - wi.process(work_item, signal, force_running, stack=stack) - # An action is returned - _update_end(session, record, instance_id) - return stack and stack[0] or False + return WorkflowInstance(session, record).validate(instance_id, signal, force_running) def update(session, record, instance_id): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(instance_id, (long, int)) - - cr = session.cr - cr.execute("select * from wkf_workitem where inst_id=%s", (instance_id,)) - wi = WorkflowItem(session, record) - - for work_item in cr.dictfetchall(): - stack = [] - wi.process(work_item, stack=stack) - return _update_end(session, record, instance_id) + return WorkflowInstance(session, record).update(instance_id) def _update_end(session, record, instance_id): - assert isinstance(session, Session) - assert isinstance(record, Record) - assert isinstance(instance_id, (long, int)) - - cr = session.cr - cr.execute('select wkf_id from wkf_instance where id=%s', (instance_id,)) - wkf_id = cr.fetchone()[0] - cr.execute('select state,flow_stop from wkf_workitem w left join wkf_activity a on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) - ok=True - for r in cr.fetchall(): - if (r[0]<>'complete') or not r[1]: - ok=False - break - if ok: - cr.execute('select distinct a.name from wkf_activity a left join wkf_workitem w on (a.id=w.act_id) where w.inst_id=%s', (instance_id,)) - act_names = cr.fetchall() - cr.execute("update wkf_instance set state='complete' where id=%s", (instance_id,)) - cr.execute("update wkf_workitem set state='complete' where subflow_id=%s", (instance_id,)) - cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (instance_id,)) - for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall(): - cur_record = Record(cur_model_name, cur_record_id) - for act_name in act_names: - validate(session, cur_record, cur_instance_id, 'subflow.%s' % act_name[0]) - - return ok + return WorkflowInstance(session, record)._update_end(instance_id) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/workflow/service.py b/openerp/workflow/service.py index 08c0f61a7a2..cf8a5449b07 100644 --- a/openerp/workflow/service.py +++ b/openerp/workflow/service.py @@ -21,7 +21,8 @@ from helpers import Session from helpers import Record -import instance +from openerp.workflow.instance import WorkflowInstance +# import instance class WorkflowService(object): @@ -49,7 +50,7 @@ class WorkflowService(object): (self.record.id or None, self.record.model or None, 'active') ) for (instance_id,) in self.cr.fetchall(): - instance.update(self.session, self.record, instance_id) + WorkflowInstance(self.session, self.record, {'id': instance_id}).update() def trigger(self): self.cr.execute('select instance_id from wkf_triggers where res_id=%s and model=%s', (self.record.id, self.record.model)) @@ -61,10 +62,10 @@ class WorkflowService(object): current_session = Session(self.session.cr, current_uid) current_record = Record(current_model_name, current_record_id) - instance.update(current_session, current_record, instance_id) + WorkflowInstance(current_session, current_record, {'id': instance_id}).update() def delete(self): - instance.delete(self.session, self.record) + WorkflowInstance(self.session, self.record, {}).delete() def create(self): WorkflowService.CACHE.setdefault(self.cr.dbname, {}) @@ -77,14 +78,18 @@ class WorkflowService(object): WorkflowService.CACHE[self.cr.dbname][self.record.model] = wkf_ids for (wkf_id, ) in wkf_ids: - instance.create(self.session, self.record, wkf_id) + WorkflowInstance.create(self.session, self.record, wkf_id) def validate(self, signal): result = False # ids of all active workflow instances for a corresponding resource (id, model_nam) self.cr.execute('select id from wkf_instance where res_id=%s and res_type=%s and state=%s', (self.record.id, self.record.model, 'active')) + # TODO: Refactor the workflow instance object for (instance_id,) in self.cr.fetchall(): - res2 = instance.validate(self.session, self.record, instance_id, signal) + wi = WorkflowInstance(self.session, self.record, {'id': instance_id}) + + res2 = wi.validate(signal) + result = result or res2 return result diff --git a/openerp/workflow/workitem.py b/openerp/workflow/workitem.py index 7d0afcb519f..2c71aaaf7d8 100644 --- a/openerp/workflow/workitem.py +++ b/openerp/workflow/workitem.py @@ -60,235 +60,191 @@ class Environment(dict): else: return super(Environment, self).__getitem__(key) -def wkf_expr_eval_expr(session, record, workitem, lines): - """ - Evaluate each line of ``lines`` with the ``Environment`` environment, returning - the value of the last line. - """ - assert lines, 'You used a NULL action in a workflow, use dummy node instead.' - result = False - for line in lines.split('\n'): - line = line.strip() - if not line: - continue - if line == 'True': - result = True - elif line == 'False': - result = False - else: - env = Environment(session, record) - result = eval(line, env, nocopy=True) - return result - -def wkf_expr_execute_action(session, record, workitem, activity): - """ - Evaluate the ir.actions.server action specified in the activity. - """ - ir_actions_server = openerp.registry(session.cr.dbname)['ir.actions.server'] - context = { 'active_model': record.model, 'active_id': record.id, 'active_ids': [record.id] } - result = ir_actions_server.run(session.cr, session.uid, [activity['action_id']], context) - return result - -def wkf_expr_execute(session, record, workitem, activity): - """ - Evaluate the action specified in the activity. - """ - return wkf_expr_eval_expr(session, record, workitem, activity['action']) - -def wkf_expr_check(session, record, workitem, transition, signal): - """ - Test if a transition can be taken. The transition can be taken if: - - - the signal name matches, - - the uid is SUPERUSER_ID or the user groups contains the transition's - group, - - the condition evaluates to a truish value. - """ - if transition['signal'] and signal != transition['signal']: - return False - - if session.uid != openerp.SUPERUSER_ID and transition['group_id']: - registry = openerp.registry(session.cr.dbname) - user_groups = registry['res.users'].read(session.cr, session.uid, [session.uid], ['groups_id'])[0]['groups_id'] - if transition['group_id'] not in user_groups: - return False - - return wkf_expr_eval_expr(session, record, workitem, transition['condition']) - class WorkflowItem(object): - def __init__(self, session, record): + def __init__(self, session, record, work_item_values): assert isinstance(session, Session) assert isinstance(record, Record) self.session = session self.record = record - def create(self, activities, instance_id, stack): - assert isinstance(activities, list) + if not work_item_values: + work_item_values = {} + + assert isinstance(work_item_values, dict) + self.workitem = work_item_values + + @classmethod + def create(cls, session, record, activity, instance_id, stack): + assert isinstance(session, Session) + assert isinstance(record, Record) + assert isinstance(activity, dict) assert isinstance(instance_id, (long, int)) assert isinstance(stack, list) - cr = self.session.cr + cr = session.cr + cr.execute("select nextval('wkf_workitem_id_seq')") + id_new = cr.fetchone()[0] + cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id)) + cr.execute('select * from wkf_workitem where id=%s',(id_new,)) + work_item_values = cr.dictfetchone() + logger.info('Created workflow item in activity %s', + activity['id'], + extra={'ident': (session.uid, record.model, record.id)}) + + workflow_item = WorkflowItem(session, record, work_item_values) + workflow_item.process(stack=stack) + + @classmethod + def create_all(cls, session, record, activities, instance_id, stack): + assert isinstance(activities, list) + for activity in activities: - cr.execute("select nextval('wkf_workitem_id_seq')") - id_new = cr.fetchone()[0] - cr.execute("insert into wkf_workitem (id,act_id,inst_id,state) values (%s,%s,%s,'active')", (id_new, activity['id'], instance_id)) - cr.execute('select * from wkf_workitem where id=%s',(id_new,)) - work_item = cr.dictfetchone() - logger.info('Created workflow item in activity %s', - activity['id'], - extra={'ident': (self.session.uid, self.record.model, self.record.id)}) + cls.create(session, record, activity, instance_id, stack) - self.process(work_item, stack=stack) - - def process(self, workitem, signal=None, force_running=False, stack=None): - assert isinstance(workitem, dict) + def process(self, signal=None, force_running=False, stack=None): assert isinstance(force_running, bool) - - # return _process(self.session, self.record, work_item, signal=signal, force_running=force_running, stack=stack) - # assert isinstance(force_running, bool) - assert stack is not None cr = self.session.cr - cr.execute('select * from wkf_activity where id=%s', (workitem['act_id'],)) + cr.execute('select * from wkf_activity where id=%s', (self.workitem['act_id'],)) activity = cr.dictfetchone() triggers = False - if workitem['state'] == 'active': + if self.workitem['state'] == 'active': triggers = True - if not self._execute(workitem, activity, stack): + if not self._execute(activity, stack): return False - if force_running or workitem['state'] == 'complete': - ok = self._split_test(workitem, activity['split_mode'], signal, stack) + if force_running or self.workitem['state'] == 'complete': + ok = self._split_test(activity['split_mode'], signal, stack) triggers = triggers and not ok if triggers: - cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) + cr.execute('select * from wkf_transition where act_from=%s', (self.workitem['act_id'],)) for trans in cr.dictfetchall(): if trans['trigger_model']: - ids = wkf_expr_eval_expr(self.session, self.record, workitem, trans['trigger_expr_id']) + ids = self.wkf_expr_eval_expr(trans['trigger_expr_id']) for res_id in ids: cr.execute('select nextval(\'wkf_triggers_id_seq\')') id =cr.fetchone()[0] - cr.execute('insert into wkf_triggers (model,res_id,instance_id,workitem_id,id) values (%s,%s,%s,%s,%s)', (trans['trigger_model'],res_id,workitem['inst_id'], workitem['id'], id)) + cr.execute('insert into wkf_triggers (model,res_id,instance_id,workitem_id,id) values (%s,%s,%s,%s,%s)', (trans['trigger_model'],res_id, self.workitem['inst_id'], self.workitem['id'], id)) return True - def _execute(self, workitem, activity, stack): + def _execute(self, activity, stack): """Send a signal to parenrt workflow (signal: subflow.signal_name)""" result = True cr = self.session.cr signal_todo = [] - if (workitem['state']=='active') and activity['signal_send']: + if (self.workitem['state']=='active') and activity['signal_send']: # signal_send']: - cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (workitem['inst_id'],)) + cr.execute("select i.id,w.osv,i.res_id from wkf_instance i left join wkf w on (i.wkf_id=w.id) where i.id IN (select inst_id from wkf_workitem where subflow_id=%s)", (self.workitem['inst_id'],)) for instance_id, model_name, record_id in cr.fetchall(): record = Record(model_name, record_id) signal_todo.append((instance_id, record, activity['signal_send'])) if activity['kind'] == WorkflowActivity.KIND_DUMMY: - if workitem['state']=='active': - self._state_set(workitem, activity, 'complete') + if self.workitem['state']=='active': + self._state_set(activity, 'complete') if activity['action_id']: - res2 = wkf_expr_execute_action(self.session, self.record, workitem, activity) + res2 = self.wkf_expr_execute_action(activity) if res2: stack.append(res2) result=res2 elif activity['kind'] == WorkflowActivity.KIND_FUNCTION: - if workitem['state']=='active': - self._state_set(workitem, activity, 'running') - returned_action = wkf_expr_execute(self.session, self.record, workitem, activity) + if self.workitem['state']=='active': + self._state_set(activity, 'running') + returned_action = self.wkf_expr_execute(activity) if type(returned_action) in (dict,): stack.append(returned_action) if activity['action_id']: - res2 = wkf_expr_execute_action(self.session, self.record, workitem, activity) + res2 = self.wkf_expr_execute_action(activity) # A client action has been returned if res2: stack.append(res2) result=res2 - self._state_set(workitem, activity, 'complete') + self._state_set(activity, 'complete') elif activity['kind'] == WorkflowActivity.KIND_STOPALL: - if workitem['state']=='active': - self._state_set(workitem, activity, 'running') - cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (workitem['inst_id'], workitem['id'])) + if self.workitem['state']=='active': + self._state_set(activity, 'running') + cr.execute('delete from wkf_workitem where inst_id=%s and id<>%s', (self.workitem['inst_id'], self.workitem['id'])) if activity['action']: - wkf_expr_execute(self.session, self.record, workitem, activity) - self._state_set(workitem, activity, 'complete') + self.wkf_expr_execute(activity) + self._state_set(activity, 'complete') elif activity['kind'] == WorkflowActivity.KIND_SUBFLOW: - if workitem['state']=='active': + if self.workitem['state']=='active': - self._state_set(workitem, activity, 'running') + self._state_set(activity, 'running') if activity.get('action', False): - id_new = wkf_expr_execute(self.session, self.record, workitem, activity) + id_new = self.wkf_expr_execute(activity) if not id_new: - cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) + cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],)) return False assert type(id_new)==type(1) or type(id_new)==type(1L), 'Wrong return value: '+str(id_new)+' '+str(type(id_new)) - cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new,activity['subflow_id'])) + cr.execute('select id from wkf_instance where res_id=%s and wkf_id=%s', (id_new, activity['subflow_id'])) id_new = cr.fetchone()[0] else: - id_new = instance.create(self.session, self.record, activity['subflow_id']) - cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, workitem['id'])) - workitem['subflow_id'] = id_new + inst = instance.WorkflowInstance(self.session, self.record) + id_new = inst.create(activity['subflow_id']) - if workitem['state']=='running': - cr.execute("select state from wkf_instance where id=%s", (workitem['subflow_id'],)) - state= cr.fetchone()[0] + cr.execute('update wkf_workitem set subflow_id=%s where id=%s', (id_new, self.workitem['id'])) + self.workitem['subflow_id'] = id_new + + if self.workitem['state']=='running': + cr.execute("select state from wkf_instance where id=%s", (self.workitem['subflow_id'],)) + state = cr.fetchone()[0] if state=='complete': - self._state_set(workitem, activity, 'complete') + self._state_set(activity, 'complete') for instance_id, record, signal_send in signal_todo: - instance.validate(self.session, self.record, signal_send, force_running=True) + wi = instance.WorkflowInstance(self.session, record, {'id': instance_id}) + wi.validate(signal_send, force_running=True) return result - def _state_set(self, workitem, activity, state): - self.session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, workitem['id'])) - workitem['state'] = state + def _state_set(self, activity, state): + self.session.cr.execute('update wkf_workitem set state=%s where id=%s', (state, self.workitem['id'])) + self.workitem['state'] = state logger.info('Changed state of work item %s to "%s" in activity %s', - workitem['id'], state, activity['id'], + self.workitem['id'], state, activity['id'], extra={'ident': (self.session.uid, self.record.model, self.record.id)}) - - - - def _split_test(self, workitem, split_mode, signal, stack): + def _split_test(self, split_mode, signal, stack): cr = self.session.cr - cr.execute('select * from wkf_transition where act_from=%s', (workitem['act_id'],)) + cr.execute('select * from wkf_transition where act_from=%s', (self.workitem['act_id'],)) test = False transitions = [] alltrans = cr.dictfetchall() if split_mode in ('XOR', 'OR'): for transition in alltrans: - if wkf_expr_check(self.session, self.record, workitem, transition,signal): + if self.wkf_expr_check(transition,signal): test = True - transitions.append((transition['id'], workitem['inst_id'])) + transitions.append((transition['id'], self.workitem['inst_id'])) if split_mode=='XOR': break else: test = True for transition in alltrans: - if not wkf_expr_check(self.session, self.record, workitem, transition,signal): + if not self.wkf_expr_check(transition, signal): test = False break - cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], workitem['inst_id'])) + cr.execute('select count(*) from wkf_witm_trans where trans_id=%s and inst_id=%s', (transition['id'], self.workitem['inst_id'])) if not cr.fetchone()[0]: - transitions.append((transition['id'], workitem['inst_id'])) + transitions.append((transition['id'], self.workitem['inst_id'])) if test and transitions: cr.executemany('insert into wkf_witm_trans (trans_id,inst_id) values (%s,%s)', transitions) - cr.execute('delete from wkf_workitem where id=%s', (workitem['id'],)) + cr.execute('delete from wkf_workitem where id=%s', (self.workitem['id'],)) for t in transitions: self._join_test(t[0], t[1], stack) return True @@ -299,7 +255,7 @@ class WorkflowItem(object): cr.execute('select * from wkf_activity where id=(select act_to from wkf_transition where id=%s)', (trans_id,)) activity = cr.dictfetchone() if activity['join_mode']=='XOR': - self.create([activity], inst_id, stack) + WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack) cr.execute('delete from wkf_witm_trans where inst_id=%s and trans_id=%s', (inst_id,trans_id)) else: cr.execute('select id from wkf_transition where act_to=%s', (activity['id'],)) @@ -314,7 +270,68 @@ class WorkflowItem(object): if ok: for (id,) in trans_ids: cr.execute('delete from wkf_witm_trans where trans_id=%s and inst_id=%s', (id,inst_id)) - self.create([activity], inst_id, stack) + WorkflowItem.create(self.session, self.record, activity, inst_id, stack=stack) + + def wkf_expr_eval_expr(self, lines): + """ + Evaluate each line of ``lines`` with the ``Environment`` environment, returning + the value of the last line. + """ + assert lines, 'You used a NULL action in a workflow, use dummy node instead.' + result = False + for line in lines.split('\n'): + line = line.strip() + if not line: + continue + if line == 'True': + result = True + elif line == 'False': + result = False + else: + env = Environment(self.session, self.record) + result = eval(line, env, nocopy=True) + return result + + def wkf_expr_execute_action(self, activity): + """ + Evaluate the ir.actions.server action specified in the activity. + """ + context = { + 'active_model': self.record.model, + 'active_id': self.record.id, + 'active_ids': [self.record.id] + } + + ir_actions_server = openerp.registry(self.session.cr.dbname)['ir.actions.server'] + result = ir_actions_server.run(self.session.cr, self.session.uid, [activity['action_id']], context) + + return result + + def wkf_expr_execute(self, activity): + """ + Evaluate the action specified in the activity. + """ + return self.wkf_expr_eval_expr(activity['action']) + + def wkf_expr_check(self, transition, signal): + """ + Test if a transition can be taken. The transition can be taken if: + + - the signal name matches, + - the uid is SUPERUSER_ID or the user groups contains the transition's + group, + - the condition evaluates to a truish value. + """ + if transition['signal'] and signal != transition['signal']: + return False + + if self.session.uid != openerp.SUPERUSER_ID and transition['group_id']: + registry = openerp.registry(self.session.cr.dbname) + user_groups = registry['res.users'].read(self.session.cr, self.session.uid, [self.session.uid], ['groups_id'])[0]['groups_id'] + if transition['group_id'] not in user_groups: + return False + + return self.wkf_expr_eval_expr(transition['condition']) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From d4e8041601680f2a944b01a3ba6c22bb16132212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 5 Nov 2013 15:19:03 +0100 Subject: [PATCH 13/64] [IMP] point_of_sale: removed all ids from templates and css bzr revid: fva@openerp.com-20131105141903-3n8iq4geiwhq4nav --- addons/point_of_sale/static/src/css/pos.css | 76 +++++++++---------- addons/point_of_sale/static/src/js/screens.js | 12 +-- addons/point_of_sale/static/src/js/widgets.js | 52 ++++++------- addons/point_of_sale/static/src/xml/pos.xml | 56 ++++++-------- 4 files changed, 88 insertions(+), 108 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 487a9a6c41c..8e2234b6e8c 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -110,7 +110,7 @@ /* ********* The black header bar ********* */ -.point-of-sale #topheader { +.point-of-sale .pos-topheader { position:absolute; left:0; top:0; @@ -124,7 +124,7 @@ /* a) The left part of the top-bar */ -.point-of-sale #branding{ +.point-of-sale .pos-branding{ position: absolute; display: table-cell; left:0; @@ -138,13 +138,13 @@ line-height:100%; vertical-align: middle; } -.point-of-sale #branding img { +.point-of-sale .pos-branding img { height: 32px; width: 116px; margin-left:5px; vertical-align:middle; } -.point-of-sale #branding .username{ +.point-of-sale .pos-branding .username{ float:right; color:#DDD; font-size:16px; @@ -155,7 +155,7 @@ /* b) The right part of the top-bar */ -.point-of-sale #rightheader { +.point-of-sale .pos-rightheader { position: absolute; left:440px; right:0; @@ -163,7 +163,7 @@ height:100%; } -.point-of-sale #rightheader button { +.point-of-sale .pos-rightheader button { color: #273072; height:27px; margin:3px; @@ -172,22 +172,22 @@ background: #7f82ac; } -.point-of-sale #rightheader button.square{ +.point-of-sale .pos-rightheader button.square{ width: 32px; margin-left:4px; } -.point-of-sale div#order-selector { +.point-of-sale .order-selector { display: inline; } -.point-of-sale ol#orders { +.point-of-sale .orders { display: inline; margin-left: 8px; } -.point-of-sale li.order-selector-button { +.point-of-sale .order-selector-button { display: inline; } -.point-of-sale li.selected-order button { +.point-of-sale .selected-order button { font-weight: 900; background: #7174A8 !important; color: rgb(236, 237, 255) !important; @@ -195,7 +195,7 @@ /* c) The session buttons */ -.point-of-sale #rightheader .header-button{ +.point-of-sale .pos-rightheader .header-button{ float:right; height:32px; padding-left:10px; @@ -211,10 +211,10 @@ -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale #rightheader .header-button:last-child{ +.point-of-sale .pos-rightheader .header-button:last-child{ border-left: 1px solid #3a3a3a; } -.point-of-sale #rightheader .header-button:active{ +.point-of-sale .pos-rightheader .header-button:active{ background: rgba(0,0,0,0.2); color:#EEE; } @@ -249,7 +249,7 @@ /* ********* Contains everything below the bar ********* */ -.point-of-sale #content { +.point-of-sale .pos-content { width: 100%; position: absolute; top: 34px; @@ -259,7 +259,7 @@ /* ********* The leftpane contains the order, numpad and paypad ********* */ -.point-of-sale #leftpane { +.point-of-sale .pos-leftpane { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; @@ -273,7 +273,7 @@ border-right: solid 1px #CECBCB; background-color: white; } -.point-of-sale #leftpane footer { +.point-of-sale .pos-leftpane footer { position: absolute; bottom: 0; left: 0; @@ -284,7 +284,7 @@ /* ********* The paypad contains the payment buttons ********* */ -.point-of-sale #paypad { +.point-of-sale .paypad { padding: 8px 4px 8px 8px; display: inline-block; text-align: center; @@ -294,7 +294,7 @@ overflow-y: auto; overflow-x: hidden; } -.point-of-sale #paypad button { +.point-of-sale .paypad button { height: 50px; display: block; width: 100%; @@ -304,9 +304,9 @@ color: #555555; font-size: 14px; } -.point-of-sale #paypad button:active, -.point-of-sale #numpad button:active, -.point-of-sale #numpad .selected-mode, +.point-of-sale .paypad button:active, +.point-of-sale .numpad button:active, +.point-of-sale .numpad .selected-mode, .point-of-sale .popup button:active{ border: none; color: white; @@ -315,12 +315,12 @@ /* ********* The Numpad ********* */ -.point-of-sale #numpad { +.point-of-sale .numpad { padding: 8px 8px 8px 4px; display: inline-block; text-align: center; } -.point-of-sale #numpad button { +.point-of-sale .numpad button { height: 50px; width: 50px; margin: 0px 0px 4px 0px; @@ -331,13 +331,13 @@ .point-of-sale .input-button { font-size: 24px; } -.point-of-sale .mode-button, .point-of-sale #numpad-delete, .point-of-sale #numpad-minus { +.point-of-sale .mode-button { font-size: 14px; } /* ********* The right pane contains the screens and headers ********* */ -.point-of-sale #rightpane { +.point-of-sale .pos-rightpane { position: absolute; top: 0; /*bottom: 105px;*/ @@ -347,7 +347,7 @@ vertical-align: top; } -.point-of-sale #rightpane header { +.point-of-sale .pos-rightpane header { padding: 0; height: 32px; border-bottom: 1px solid #c7c7c7; @@ -432,19 +432,11 @@ /* c) the categories list */ -.point-of-sale #categories { - /*background:#f0f0f0;*/ +.point-of-sale .categories { position: relative; border-bottom: 1px solid #cecece; } -.point-of-sale #categories .white-gradient{ - position: absolute; - top:50%; - left:0; - right:0; - bottom:0; -} -.point-of-sale #categories h4 { +.point-of-sale .categories h4 { display: inline-block; margin: 9px 5px; } @@ -754,20 +746,20 @@ } @media print { - .point-of-sale #topheader, .point-of-sale #leftpane { + .point-of-sale .pos-topheader, .point-of-sale .pos-leftpane { display: none !important; } - .point-of-sale #content { + .point-of-sale .pos-content { top: 0px !important; } - .point-of-sale #rightpane { + .point-of-sale .pos-rightpane { left: 0px !important; background-color: white; } - #receipt-screen header { + .point-of-sale .receipt-screen header { display: none !important; } - #receipt-screen { + .point-of-sale .receipt-screen { text-align: left; } .pos-actionbar { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 77a542ff93c..c6261477fe6 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -998,7 +998,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa l.on('delete_payment_line', self, function(r) { self.deleteLine(r); }); - l.appendTo(this.$('#paymentlines')); + l.appendTo(this.$('.paymentlines')); this.paymentlinewidgets.push(l); if(this.numpadState){ this.numpadState.resetValue(); @@ -1007,7 +1007,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, renderElement: function() { this._super(); - this.$('#paymentlines').empty(); + this.$('.paymentlines').empty(); for(var i = 0, len = this.paymentlinewidgets.length; i < len; i++){ this.paymentlinewidgets[i].destroy(); } @@ -1029,10 +1029,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; - this.$('#payment-due-total').html(this.format_currency(dueTotal)); - this.$('#payment-paid-total').html(this.format_currency(paidTotal)); - this.$('#payment-remaining').html(this.format_currency(remaining)); - this.$('#payment-change').html(this.format_currency(change)); + this.$('.payment-due-total').html(this.format_currency(dueTotal)); + this.$('.payment-paid-total').html(this.format_currency(paidTotal)); + this.$('.payment-remaining').html(this.format_currency(remaining)); + this.$('.payment-change').html(this.format_currency(change)); if(currentOrder.selected_orderline === undefined){ remaining = 1; // What is this ? } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 1abd6c3daba..d237b552d17 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -842,7 +842,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa order: new_order, pos: self.pos }); - new_order_button.appendTo($('#orders')); + new_order_button.appendTo(this.$('.orders')); new_order_button.selectOrder(); }, self); @@ -889,25 +889,25 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // -------- Screens --------- this.product_screen = new module.ProductScreenWidget(this,{}); - this.product_screen.appendTo($('#rightpane')); + this.product_screen.appendTo(this.$('.pos-rightpane')); this.receipt_screen = new module.ReceiptScreenWidget(this, {}); - this.receipt_screen.appendTo($('#rightpane')); + this.receipt_screen.appendTo(this.$('.pos-rightpane')); this.payment_screen = new module.PaymentScreenWidget(this, {}); - this.payment_screen.appendTo($('#rightpane')); + this.payment_screen.appendTo(this.$('.pos-rightpane')); this.welcome_screen = new module.WelcomeScreenWidget(this,{}); - this.welcome_screen.appendTo($('#rightpane')); + this.welcome_screen.appendTo(this.$('.pos-rightpane')); this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {}); - this.client_payment_screen.appendTo($('#rightpane')); + this.client_payment_screen.appendTo(this.$('.pos-rightpane')); this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {}); - this.scale_invite_screen.appendTo($('#rightpane')); + this.scale_invite_screen.appendTo(this.$('.pos-rightpane')); this.scale_screen = new module.ScaleScreenWidget(this,{}); - this.scale_screen.appendTo($('#rightpane')); + this.scale_screen.appendTo(this.$('.pos-rightpane')); // -------- Popups --------- @@ -938,42 +938,42 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // -------- Misc --------- this.notification = new module.SynchNotificationWidget(this,{}); - this.notification.appendTo(this.$('#rightheader')); + this.notification.appendTo(this.$('.pos-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 #rightpane")); + this.action_bar.appendTo(this.$(".pos-rightpane")); this.left_action_bar = new module.ActionBarWidget(this); - this.left_action_bar.appendTo($(".point-of-sale #leftpane")); + this.left_action_bar.appendTo(this.$(".pos-leftpane")); this.paypad = new module.PaypadWidget(this, {}); - this.paypad.replace($('#placeholder-PaypadWidget')); + this.paypad.replace(this.$('.placeholder-PaypadWidget')); this.numpad = new module.NumpadWidget(this); - this.numpad.replace($('#placeholder-NumpadWidget')); + this.numpad.replace(this.$('.placeholder-NumpadWidget')); this.order_widget = new module.OrderWidget(this, {}); - this.order_widget.replace($('#placeholder-OrderWidget')); + this.order_widget.replace(this.$('.placeholder-OrderWidget')); this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, { 'keyboard_model': 'simple' }); - this.onscreen_keyboard.appendTo($(".point-of-sale #content")); + this.onscreen_keyboard.appendTo(this.$(".pos-content")); this.close_button = new module.HeaderButtonWidget(this,{ label: _t('Close'), action: function(){ self.close(); }, }); - this.close_button.appendTo(this.$('#rightheader')); + this.close_button.appendTo(this.$('.pos-rightheader')); this.client_button = new module.HeaderButtonWidget(this,{ label: _t('Self-Checkout'), action: function(){ self.screen_selector.set_user_mode('client'); }, }); - this.client_button.appendTo(this.$('#rightheader')); + this.client_button.appendTo(this.$('.pos-rightheader')); // -------- Screen Selector --------- @@ -1006,7 +1006,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(this.pos.debug){ this.debug_widget = new module.DebugWidget(this); - this.debug_widget.appendTo(this.$('#content')); + this.debug_widget.appendTo(this.$('.pos-content')); } }, @@ -1053,12 +1053,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(visible !== this.leftpane_visible){ this.leftpane_visible = visible; if(visible){ - $('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing'); - $('#rightpane').animate({'left':this.leftpane_width},500,'swing'); + this.$('.pos-leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing'); + this.$('.pos-rightpane').animate({'left':this.leftpane_width},500,'swing'); }else{ - var leftpane = $('#leftpane'); - $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); }); - $('#rightpane').animate({'left':'0px'},500,'swing'); + var leftpane = this.$('.pos-leftpane'); + leftpane.animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); }); + this.$('.pos-rightpane').animate({'left':'0px'},500,'swing'); } } }, @@ -1067,11 +1067,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(visible !== this.cashier_controls_visible){ this.cashier_controls_visible = visible; if(visible){ - $('#loggedas').removeClass('oe_hidden'); - $('#rightheader').removeClass('oe_hidden'); + this.$('.pos-rightheader').removeClass('oe_hidden'); }else{ - $('#loggedas').addClass('oe_hidden'); - $('#rightheader').addClass('oe_hidden'); + this.$('.pos-rightheader').addClass('oe_hidden'); } } }, diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index daa1164a2df..3d3f921f07c 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -5,30 +5,30 @@
    -
    -
    +
    +
    -
    -
    +
    +
    -
      +
        -
        +
        -
        -
        +
        +
        -
        -
        +
        +
        -
        +
        @@ -73,12 +73,12 @@ -
        +
        -
        +
        @@ -144,11 +144,9 @@
        -
        -
        +
        -
        @@ -156,16 +154,15 @@
        -
          +
        -
        -
        +
        @@ -213,7 +210,7 @@ -
        +

        Payment

        @@ -222,28 +219,28 @@ Total: - +
        -
        +
        @@ -252,7 +249,7 @@ -
        +

        Receipt

        @@ -300,13 +297,6 @@
        - -
        - - -
        -
        -
        From fcbe2e3a97b4cee5787d76394031b4eec1089e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 5 Nov 2013 16:15:48 +0100 Subject: [PATCH 16/64] [IMP] point_of_sale: renamed point-of-sale class + new subwindow layouting system bzr revid: fva@openerp.com-20131105151548-vpsg2npnp0bx7pwd --- addons/point_of_sale/static/src/css/pos.css | 478 ++++++++++-------- addons/point_of_sale/static/src/js/widgets.js | 4 +- addons/point_of_sale/static/src/xml/pos.xml | 2 +- 3 files changed, 263 insertions(+), 221 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 6532a3436e5..742f38257e8 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -3,7 +3,7 @@ src: url(../fonts/Inconsolata.otf); } -.point-of-sale { +.pos { padding: 0; margin: 0; background-color: #f0eeee; @@ -21,24 +21,24 @@ text-shadow: none; } -.point-of-sale * { +.pos * { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } -.point-of-sale button{ +.pos button{ box-shadow: none; outline: none; border: none; } -.point-of-sale button:hover{ +.pos button:hover{ background: default; } -.point-of-sale .oe_hidden{ +.pos .oe_hidden{ display: none !important; } -.point-of-sale ul, .point-of-sale li { +.pos ul, .pos li { margin: 0; padding: 0; list-style-type: none; @@ -46,7 +46,7 @@ /* ********* The black loading screen ********* */ -.point-of-sale .loader{ +.pos .loader{ background-color: #222; position:absolute; left:0px; @@ -57,13 +57,108 @@ text-align: center; } +/* ********* Generic Layout Constructs ********* */ + +.pos .window{ + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + display: table; + border: none; +} +.pos .window .subwindow{ + display: table-row; + width: 100%; + height: 100%; +} +.pos .window .subwindow.collapsed{ + height: 0px; +} +.pos .subwindow .subwindow-container{ + display: table-cell; + position: relative; +} + +/* ---- Scrollers ----- */ + +.pos .scroller-container{ + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; +} +.pos .scroller{ + width: 100%; + height: 100%; + overflow: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.pos .scroller.horizontal{ + overflow-y: hidden; + overflow-x: auto; +} +.pos .scroller-content{ + -webkit-transform: translate3d(0,0,0); +} +.pos .scroller-container ::-webkit-scrollbar{ + width: 10px; + height: 10px; +} +.pos .scroller-container ::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: solid 1px rgb(200,200,200); + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pos .scroller-container ::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 30px; +} + +/* ---- Panes ----- */ + +/* +.pos .headerpane{ + position: absolute; + left: 0px; + right: 0px; + top: 0px; + height: 40px; + background: rgb(100,100,100) +} +.pos .mainpane{ + position: absolute; + left: 0px; + right: 0px; + top: 40px; + bottom: 0px; +} +.pos .leftpane{ + position: absolute; + left: 0px; + width: 400px; + top: 0px; + bottom: 0px; +} +.pos .rightpane{ + position: absolute; + left: 400px; + right: 0px; + top: 0px; + bottom: 0px; +}*/ + /* ********* Generic element styling ********* */ -.point-of-sale a { +.pos a { text-decoration: none; color: #555555; } -.point-of-sale button, .point-of-sale a.button { +.pos button, .pos a.button { display: inline-block; cursor: pointer; padding: 4px 10px; @@ -71,21 +166,21 @@ border: 1px solid #cacaca; background: #e2e2e2; } -.point-of-sale ul, .point-of-sale ol { +.pos ul, .pos ol { padding: 0; margin: 0; } -.point-of-sale li { +.pos li { list-style-type: none; } -.point-of-sale .pos-right-align { +.pos .pos-right-align { text-align: right; } -.point-of-sale .pos-right-align input { +.pos .pos-right-align input { text-align: right; border: 1px solid #cecbcb; } -.point-of-sale .pos-disc-font { +.pos .pos-disc-font { font-size: 12px; font-style:italic; color: #808080; @@ -94,7 +189,7 @@ /* ********* The black header bar ********* */ -.point-of-sale .pos-topheader { +.pos .pos-topheader { position:absolute; left:0; top:0; @@ -108,7 +203,7 @@ /* a) The left part of the top-bar */ -.point-of-sale .pos-branding{ +.pos .pos-branding{ position: absolute; display: table-cell; left:0; @@ -122,13 +217,13 @@ line-height:100%; vertical-align: middle; } -.point-of-sale .pos-branding img { +.pos .pos-branding img { height: 32px; width: 116px; margin-left:5px; vertical-align:middle; } -.point-of-sale .pos-branding .username{ +.pos .pos-branding .username{ float:right; color:#DDD; font-size:16px; @@ -139,7 +234,7 @@ /* b) The right part of the top-bar */ -.point-of-sale .pos-rightheader { +.pos .pos-rightheader { position: absolute; left:440px; right:0; @@ -147,7 +242,7 @@ height:100%; } -.point-of-sale .pos-rightheader button { +.pos .pos-rightheader button { color: #273072; height:27px; margin:3px; @@ -156,22 +251,22 @@ background: #7f82ac; } -.point-of-sale .pos-rightheader button.square{ +.pos .pos-rightheader button.square{ width: 32px; margin-left:4px; } -.point-of-sale .order-selector { +.pos .order-selector { display: inline; } -.point-of-sale .orders { +.pos .orders { display: inline; margin-left: 8px; } -.point-of-sale .order-selector-button { +.pos .order-selector-button { display: inline; } -.point-of-sale .selected-order button { +.pos .selected-order button { font-weight: 900; background: #7174A8 !important; color: rgb(236, 237, 255) !important; @@ -179,7 +274,7 @@ /* c) The session buttons */ -.point-of-sale .pos-rightheader .header-button{ +.pos .pos-rightheader .header-button{ float:right; height:32px; padding-left:10px; @@ -195,17 +290,17 @@ -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .pos-rightheader .header-button:last-child{ +.pos .pos-rightheader .header-button:last-child{ border-left: 1px solid #3a3a3a; } -.point-of-sale .pos-rightheader .header-button:active{ +.pos .pos-rightheader .header-button:active{ background: rgba(0,0,0,0.2); color:#EEE; } /* c) The notifications indicator */ -.point-of-sale .oe_pos_synch-notification{ +.pos .oe_pos_synch-notification{ float:right; color: rgba(255,255,255,0.4); padding: 8px; @@ -215,17 +310,17 @@ font-style: italic; cursor:pointer; } -.point-of-sale .oe_pos_synch-notification.oe_inactive{ +.pos .oe_pos_synch-notification.oe_inactive{ cursor: default; } -.point-of-sale .oe_pos_synch-notification .oe_status_red{ +.pos .oe_pos_synch-notification .oe_status_red{ display:inline-block; cursor:pointer; width:16px; height:16px; background: url("../img/gtk-no.png") no-repeat ; } -.point-of-sale .oe_pos_synch-notification .oe_status_green{ +.pos .oe_pos_synch-notification .oe_status_green{ display:inline-block; width:16px; height:16px; background: url("../img/gtk-yes.png") no-repeat; @@ -233,7 +328,7 @@ /* ********* Contains everything below the bar ********* */ -.point-of-sale .pos-content { +.pos .pos-content { width: 100%; position: absolute; top: 34px; @@ -243,7 +338,7 @@ /* ********* The leftpane contains the order, numpad and paypad ********* */ -.point-of-sale .pos-leftpane { +.pos .pos-leftpane { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; @@ -257,7 +352,7 @@ border-right: solid 1px #CECBCB; background-color: white; } -.point-of-sale .pos-leftpane footer { +.pos .pos-leftpane footer { position: absolute; bottom: 0; left: 0; @@ -268,7 +363,7 @@ /* ********* The paypad contains the payment buttons ********* */ -.point-of-sale .paypad { +.pos .paypad { padding: 8px 4px 8px 8px; display: inline-block; text-align: center; @@ -278,7 +373,7 @@ overflow-y: auto; overflow-x: hidden; } -.point-of-sale .paypad button { +.pos .paypad button { height: 50px; display: block; width: 100%; @@ -288,10 +383,10 @@ color: #555555; font-size: 14px; } -.point-of-sale .paypad button:active, -.point-of-sale .numpad button:active, -.point-of-sale .numpad .selected-mode, -.point-of-sale .popup button:active{ +.pos .paypad button:active, +.pos .numpad button:active, +.pos .numpad .selected-mode, +.pos .popup button:active{ border: none; color: white; background: #7f82ac; @@ -299,12 +394,12 @@ /* ********* The Numpad ********* */ -.point-of-sale .numpad { +.pos .numpad { padding: 8px 8px 8px 4px; display: inline-block; text-align: center; } -.point-of-sale .numpad button { +.pos .numpad button { height: 50px; width: 50px; margin: 0px 0px 4px 0px; @@ -312,16 +407,16 @@ vertical-align: middle; color: #555555; } -.point-of-sale .input-button { +.pos .input-button { font-size: 24px; } -.point-of-sale .mode-button { +.pos .mode-button { font-size: 14px; } /* ********* The right pane contains the screens and headers ********* */ -.point-of-sale .pos-rightpane { +.pos .pos-rightpane { position: absolute; top: 0; /*bottom: 105px;*/ @@ -331,7 +426,7 @@ vertical-align: top; } -.point-of-sale .pos-rightpane header { +.pos .pos-rightpane header { padding: 0; height: 32px; border-bottom: 1px solid #c7c7c7; @@ -340,12 +435,12 @@ /* ********* The product list ********* */ -.point-of-sale .product-list { +.pos .product-list { padding:10px !important; -webkit-transform: translate3d(0,0,0); } -.point-of-sale .product-list-scroller{ +.pos .product-list-scroller{ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; @@ -357,7 +452,7 @@ -webkit-overflow-scrolling: touch; } -.point-of-sale .product-list-container { +.pos .product-list-container { position:absolute; top:0px; bottom:0px; @@ -367,26 +462,26 @@ /* a) the product list navigation bar */ -.point-of-sale .breadcrumb li { +.pos .breadcrumb li { float: left; line-height: 32px; height: 32px; } -.point-of-sale .breadcrumb li:last-child { +.pos .breadcrumb li:last-child { padding-right: 3px; border-right: 1px solid #c5c5c5; } -.point-of-sale .breadcrumb a { +.pos .breadcrumb a { display: inline-block; padding: 0 9px; vertical-align: top; color: #555555; font-weight: bold; } -.point-of-sale .bc-arrow { +.pos .bc-arrow { height: 33px; } -.point-of-sale .homeimg { +.pos .homeimg { width: 19px; height: 19px; margin: 6px 0; @@ -394,11 +489,11 @@ /* b) the search box */ -.point-of-sale .searchbox { +.pos .searchbox { position: absolute; right: 2px; } -.point-of-sale .searchbox input { +.pos .searchbox input { width: 130px; border: 1px solid #cecbcb; padding: 3px 19px; @@ -406,7 +501,7 @@ background: url("../img/search.png") no-repeat 5px; background-color: white; } -.point-of-sale .search-clear { +.pos .search-clear { position: absolute; top: 11px; right: 11px; @@ -416,21 +511,21 @@ /* c) the categories list */ -.point-of-sale .categories { +.pos .categories { position: relative; border-bottom: 1px solid #cecece; } -.point-of-sale .categories h4 { +.pos .categories h4 { display: inline-block; margin: 9px 5px; } -.point-of-sale .category-list{ +.pos .category-list{ padding:10px !important; } /* d) the category button */ -.point-of-sale .category-button { +.pos .category-button { position: relative; vertical-align: top; display: inline-block; @@ -445,7 +540,7 @@ cursor: pointer; } -.point-of-sale .category-simple-button{ +.pos .category-simple-button{ position: relative; display: inline-block; font-size: 14px; @@ -457,7 +552,7 @@ background: #e2e2e2; } -.point-of-sale .category-simple-button:active{ +.pos .category-simple-button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; @@ -469,7 +564,7 @@ -.point-of-sale .category-button .category-img { +.pos .category-button .category-img { position: relative; width: 120px; height: 100px; @@ -477,12 +572,12 @@ cursor: pointer; } -.point-of-sale .category-button .category-img img { +.pos .category-button .category-img img { max-height: 100px; max-width: 120px; } -.point-of-sale .category-button .category-name { +.pos .category-button .category-name { position: absolute; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -503,7 +598,7 @@ /* e) the product */ -.point-of-sale .product { +.pos .product { position:relative; vertical-align: top; display: inline-block; @@ -519,7 +614,7 @@ overflow: hidden; } -.point-of-sale .product .product-img { +.pos .product .product-img { position: relative; width: 120px; height: 100px; @@ -527,12 +622,12 @@ text-align: center; } -.point-of-sale .product .product-img img { +.pos .product .product-img img { max-height: 100px; max-width: 120px; } -.point-of-sale .product .price-tag { +.pos .product .price-tag { position: absolute; top: 2px; right: 2px; @@ -543,7 +638,7 @@ padding: 2px 5px; } -.point-of-sale .product .product-name { +.pos .product .product-name { position: absolute; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -569,21 +664,21 @@ /* ********* The Screens ********* */ -.point-of-sale .screen { +.pos .screen { position:absolute; text-align: center; top:0px; bottom:0px; width:100%; } -.point-of-sale .screen header h2 { +.pos .screen header h2 { margin-top: 0px; padding-top: 7px; } -.point-of-sale .screen p{ +.pos .screen p{ font-size: 18px; } -.point-of-sale .dialog{ +.pos .dialog{ width: 500px; margin-left: auto; margin-right: auto; @@ -592,7 +687,7 @@ background-color: #f8f8f8; text-align: center; } -.point-of-sale .dialog p{ +.pos .dialog p{ font-size: 25px; margin-top: 10px; color: #5a5a5a; @@ -600,45 +695,45 @@ /* a) Layout for the Product Screen */ -.point-of-sale .screen .layout-table { +.pos .screen .layout-table { border:none; width:100%; height:100%; } -.point-of-sale .screen .header-row { +.pos .screen .header-row { border:none; width:100%; height:0px; } -.point-of-sale .screen .header-cell{ +.pos .screen .header-cell{ border:none; width:100%; height:0px; } -.point-of-sale .screen .content-row { +.pos .screen .content-row { width:100%; height:100%; } -.point-of-sale .screen .content-cell{ +.pos .screen .content-cell{ width:100%; } -.point-of-sale .screen .content-cell .content-container{ +.pos .screen .content-cell .content-container{ height:100%; position:relative; } /* b) The payment screen */ -.point-of-sale .pos-step-container { +.pos .pos-step-container { display: inline-block; font-size: 1.5em; } -.point-of-sale .greyed-out{ +.pos .greyed-out{ color: #AAA; } -.point-of-sale .pos-step-container input{ +.pos .pos-step-container input{ font-size: 1em; outline: none; border: none; @@ -650,12 +745,12 @@ -webkit-animation: all 250ms linear; } -.point-of-sale .pos-step-container input:focus{ +.pos .pos-step-container input:focus{ color: #5d7ad6; -webkit-animation: all 250ms linear; } -.point-of-sale .pos-payment-container { +.pos .pos-payment-container { text-align: left; min-width: 500px; margin-top: 50px; @@ -663,37 +758,37 @@ background-color: #f8f8f8; border: solid 1px rgb(220,220,220); } -.point-of-sale .pos-payment-container .left-block{ +.pos .pos-payment-container .left-block{ display: inline-block; width:49%; margin:0; padding:0; text-align:left; } -.point-of-sale .pos-payment-container .header{ +.pos .pos-payment-container .header{ margin-top: 0px; margin-bottom:20px; font-weight: bold; } -.point-of-sale .pos-payment-container .infoline{ +.pos .pos-payment-container .infoline{ margin-top:5px; margin-bottom:5px; } -.point-of-sale .pos-payment-container .right-block{ +.pos .pos-payment-container .right-block{ display: inline-block; width:49%; margin:0; padding:0; text-align:right; } -.point-of-sale .pos-payment-container table { +.pos .pos-payment-container table { width: 100%; margin-bottom: 20px; } -.point-of-sale .pos-payment-container td { +.pos .pos-payment-container td { vertical-align: middle; } -.point-of-sale .pos-payment-container .paymentline-type { +.pos .pos-payment-container .paymentline-type { font-size: 1em; font-weight: bold; margin-right:10px; @@ -701,11 +796,11 @@ /* c) The receipt screen */ -.point-of-sale .pos-receipt-container { +.pos .pos-receipt-container { font-size: 0.75em; } -.point-of-sale .pos-sale-ticket { +.pos .pos-sale-ticket { text-align: left; width: 300px; background-color: white; @@ -715,35 +810,35 @@ display: inline-block; font-family: "Inconsolata"; } -.point-of-sale .pos-sale-ticket .emph{ +.pos .pos-sale-ticket .emph{ font-size: 20px; margin:5px; } -.point-of-sale .pos-sale-ticket table { +.pos .pos-sale-ticket table { width: 100%; border: 0; table-layout: fixed; } -.point-of-sale .pos-sale-ticket table td { +.pos .pos-sale-ticket table td { border: 0; word-wrap: break-word; } @media print { - .point-of-sale .pos-topheader, .point-of-sale .pos-leftpane { + .pos .pos-topheader, .pos .pos-leftpane { display: none !important; } - .point-of-sale .pos-content { + .pos .pos-content { top: 0px !important; } - .point-of-sale .pos-rightpane { + .pos .pos-rightpane { left: 0px !important; background-color: white; } - .point-of-sale .receipt-screen header { + .pos .receipt-screen header { display: none !important; } - .point-of-sale .receipt-screen { + .pos .receipt-screen { text-align: left; } .pos-actionbar { @@ -755,12 +850,12 @@ .debug-widget{ display: none !important; } - .point-of-sale *{ + .pos *{ text-shadow: none !important; box-shadow: none !important; background: transparent !important; } - .point-of-sale .pos-sale-ticket{ + .pos .pos-sale-ticket{ margin-left: auto !important; margin-right: auto !important; border: solid 1px black !important; @@ -769,7 +864,7 @@ /* d) The Scale screen */ -.point-of-sale .scale-screen .display{ +.pos .scale-screen .display{ position:relative; width:600px; height:190px; @@ -781,7 +876,7 @@ border-top: 1px solid #efefef; } -.point-of-sale .scale-screen .product-picture { +.pos .scale-screen .product-picture { position: relative; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -799,14 +894,14 @@ border: 1px solid #fff; } -.point-of-sale .scale-screen .product-picture img{ +.pos .scale-screen .product-picture img{ max-width: 178px; max-height:178px; vertical-align: middle; cursor:pointer; } -.point-of-sale .scale-screen .product-picture .product-price{ +.pos .scale-screen .product-picture .product-price{ position: absolute; top:8px; right:8px; @@ -819,7 +914,7 @@ cursor:pointer; } -.point-of-sale .scale-screen .product-name { +.pos .scale-screen .product-name { position: absolute; left:40px; top:50px; @@ -832,7 +927,7 @@ overflow: hidden; text-overflow: ellipsis; } -.point-of-sale .scale-screen .weight{ +.pos .scale-screen .weight{ position: absolute; left: 40px; height: 90px; @@ -844,7 +939,7 @@ background: white; } -.point-of-sale .scale-screen .weight p{ +.pos .scale-screen .weight p{ display: inline-block; text-align:right; line-height: 90px; @@ -858,7 +953,7 @@ } /* e) The Welcome Screen */ -.point-of-sale .goodbye-message{ +.pos .goodbye-message{ position: absolute; left:50%; top:40%; @@ -878,7 +973,7 @@ /* ********* The OrderWidget ********* */ -.point-of-sale .order-container{ +.pos .order-container{ position: absolute; top: 0px; bottom: 232px; @@ -886,7 +981,7 @@ background: #F0EEEE; } -.point-of-sale .order-scroller{ +.pos .order-scroller{ width:100%; height:100%; overflow:hidden; @@ -894,7 +989,7 @@ -webkit-overflow-scrolling: touch; } -.point-of-sale .order{ +.pos .order{ background: white; padding-bottom:15px; padding-top:15px; @@ -908,7 +1003,7 @@ -webkit-transform: translate3d(0,0,0); } -.point-of-sale .order .empty{ +.pos .order .empty{ text-align:center; margin-top: 15px; margin-bottom: 5px; @@ -916,14 +1011,14 @@ font-weight: normal; } -.point-of-sale .order .summary{ +.pos .order .summary{ width:100%; text-align:right; font-weight: bold; margin-top:20px; margin-bottom:10px; } -.point-of-sale .order .summary .line{ +.pos .order .summary .line{ float: right; margin-right:15px; margin-left: 15px; @@ -931,12 +1026,12 @@ border-top: solid 2px; border-color:#777; } -.point-of-sale .order .summary .line .subentry{ +.pos .order .summary .line .subentry{ font-size: 10px; font-weight: normal; text-align: center; } -.point-of-sale .order .summary .line.empty{ +.pos .order .summary .line.empty{ text-align: right; border-color:#BBB; color:#999; @@ -944,7 +1039,7 @@ /* ********* The OrderLineWidget ********* */ -.point-of-sale .order .orderline{ +.pos .order .orderline{ width:100%; margin:0px; padding-top:3px; @@ -960,25 +1055,25 @@ -moz-transition: background 250ms ease-in-out; transition: background 250ms ease-in-out; } -.point-of-sale .order .orderline:active{ +.pos .order .orderline:active{ background: rgba(140,143,183,0.05); -webkit-transition: background 50ms ease-in-out; -moz-transition: background 50ms ease-in-out; transition: background 50ms ease-in-out; } -.point-of-sale .order .orderline.empty:active{ +.pos .order .orderline.empty:active{ background: transparent; cursor: default; } -.point-of-sale .order .orderline.selected{ +.pos .order .orderline.selected{ background: rgba(140,143,183,0.2); -webkit-transition: background 250ms ease-in-out; -moz-transition: background 250ms ease-in-out; transition: background 250ms ease-in-out; cursor: default; } -.point-of-sale .order .orderline .product-name{ +.pos .order .orderline .product-name{ padding:0; display:inline-block; font-weight: bold; @@ -986,16 +1081,16 @@ overflow:hidden; text-overflow: ellipsis; } -.point-of-sale .order .orderline .price{ +.pos .order .orderline .price{ padding:0; font-weight: bold; float:right; } -.point-of-sale .order .orderline .info-list{ +.pos .order .orderline .info-list{ color: #888; margin-left:10px; } -.point-of-sale .order .orderline .info-list em{ +.pos .order .orderline .info-list em{ color: #777; font-weight: bold; font-style:normal; @@ -1003,7 +1098,7 @@ /* ********* The ActionBarWidget ********* */ -.point-of-sale .pos-actionbar{ +.pos .pos-actionbar{ position:absolute; left: 0; bottom: 0px; @@ -1015,11 +1110,11 @@ z-index:900; } -.point-of-sale .pos-actionbar ul{ +.pos .pos-actionbar ul{ list-style: none; } -.point-of-sale .pos-actionbar-left-pane{ +.pos .pos-actionbar-left-pane{ height: 100%; width: 434px; margin: 0px; @@ -1029,7 +1124,7 @@ float: left; } -.point-of-sale .pos-actionbar-button-list{ +.pos .pos-actionbar-button-list{ height: 100%; margin: 0px; padding-left:3px; @@ -1037,7 +1132,7 @@ overflow:hidden; } -.point-of-sale .pos-actionbar .button{ +.pos .pos-actionbar .button{ width: 90px; height: 90px; text-align:center; @@ -1054,13 +1149,13 @@ background: #e2e2e2; } -.point-of-sale .pos-actionbar .button .label{ +.pos .pos-actionbar .button .label{ margin-top: 37px; } -.point-of-sale .pos-actionbar .button .icon{ +.pos .pos-actionbar .button .icon{ margin-top: 10px; } -.point-of-sale .pos-actionbar .button:active{ +.pos .pos-actionbar .button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; @@ -1070,7 +1165,7 @@ -webkit-transition-timing-function: ease-out; } -.point-of-sale .pos-actionbar .button.disabled:active{ +.pos .pos-actionbar .button.disabled:active{ border: 1px solid #cacaca; color: #555; cursor: default; @@ -1078,12 +1173,12 @@ background: #e2e2e2; } -.point-of-sale .pos-actionbar .button.rightalign{ +.pos .pos-actionbar .button.rightalign{ float:right; } /* ********* The Debug Widget ********* */ -.point-of-sale .debug-widget{ +.pos .debug-widget{ z-index:100000; position: absolute; right: 10px; @@ -1096,7 +1191,7 @@ padding-bottom: 10px; cursor:move; } -.point-of-sale .debug-widget .toggle{ +.pos .debug-widget .toggle{ position: absolute; font-size: 16px; cursor:pointer; @@ -1105,17 +1200,17 @@ padding:10px; padding-right:15px; } -.point-of-sale .debug-widget .content{ +.pos .debug-widget .content{ overflow: hidden; } -.point-of-sale .debug-widget h1{ +.pos .debug-widget h1{ background:black; padding-top: 10px; padding-left: 10px; margin-top:0; margin-bottom:0; } -.point-of-sale .debug-widget .category{ +.pos .debug-widget .category{ background: black; padding-left: 10px; margin: 0px; @@ -1123,29 +1218,29 @@ padding-top:3px; padding-bottom:3px; } -.point-of-sale .debug-widget .button{ +.pos .debug-widget .button{ padding: 5px; padding-left: 15px; display: block; cursor:pointer; } -.point-of-sale .debug-widget .button:active{ +.pos .debug-widget .button:active{ background: rgba(96,21,177,0.45); } -.point-of-sale .debug-widget input{ +.pos .debug-widget input{ margin-left:10px; margin-top:7px; } -.point-of-sale .debug-widget .status{ +.pos .debug-widget .status{ padding: 5px; padding-left: 15px; display: block; cursor:default; } -.point-of-sale .debug-widget .status.on{ +.pos .debug-widget .status.on{ background-color: #6cd11d; } -.point-of-sale .debug-widget .event{ +.pos .debug-widget .event{ padding: 5px; padding-left: 15px; display: block; @@ -1155,7 +1250,7 @@ /* ********* The PopupWidgets ********* */ -.point-of-sale .modal-dialog{ +.pos .modal-dialog{ position: absolute; left: 0; top: 0; @@ -1164,7 +1259,7 @@ background-color: rgba(0,0,0,0.5); z-index:1000; } -.point-of-sale .modal-dialog .popup{ +.pos .modal-dialog .popup{ position: absolute; left:50%; top:50%; @@ -1180,7 +1275,7 @@ background-color: #F0EEEE; z-index:1200; } -.point-of-sale .popup .footer{ +.pos .popup .footer{ position:absolute; bottom:0; left:0; @@ -1188,7 +1283,7 @@ height:60px; border-top: 1px solid #E0DDDD; } -.point-of-sale .popup .button{ +.pos .popup .button{ float:right; width: 110px; height: 40px; @@ -1207,7 +1302,7 @@ background: #e2e2e2; } -.point-of-sale .popup .button:active{ +.pos .popup .button:active{ color: white; background: #7f82ac; border: 1px solid #7f82ac; @@ -1217,7 +1312,7 @@ -webkit-transition-timing-function: ease-out; } -.point-of-sale .popup .button.big-left{ +.pos .popup .button.big-left{ position:absolute; top: 120px; left:40px; @@ -1226,7 +1321,7 @@ line-height:180px; } -.point-of-sale .popup .button.big-right{ +.pos .popup .button.big-right{ position:absolute; top: 120px; right:40px; @@ -1237,81 +1332,28 @@ /* ********* The Webkit Scrollbar ********* */ -.point-of-sale *::-webkit-scrollbar{ +.pos *::-webkit-scrollbar{ width: 10px; height: 10px; } -.point-of-sale *::-webkit-scrollbar-track{ +.pos *::-webkit-scrollbar-track{ background: rgb(224,224,224); border-left: solid 1px rgb(200,200,200); } -.point-of-sale *::-webkit-scrollbar-thumb{ +.pos *::-webkit-scrollbar-thumb{ background: rgb(168,168,168); min-height: 30px; } -/* ********* The ScrollBarWidget ********* */ - -.point-of-sale .scrollbar{ - position:absolute; - top:7px; - right:7px; - width:48px; - bottom:7px; - background: rgba(0,0,0,0.1); - -} -.point-of-sale .scrollbar .button{ - width:100%; - height: 48px; - line-height: 38px; - text-align: center; - font-size:48px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .button{ - color:white; - background: rgba(0,0,0,0.6); -} -.point-of-sale .scrollbar .button:active{ -} -.point-of-sale .scrollbar .button.disabled{ - background: rgba(0,0,0,0.3); - color:rgba(255,255,255,0.5); - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .down-button{ - position:absolute; - bottom:0px; -} -.point-of-sale .scrollbar .up-button{ - position:absolute; - top:0px; -} -.point-of-sale .scrollbar .scroller{ - position:absolute; - top:33%; - bottom:50%; - width:100%; - background: rgba(0,0,0,0.1); -} - /* ********* Unsupported Browser Page ********* */ -.point-of-sale .not-supported-browser{ +.pos .not-supported-browser{ position: absolute; z-index: 100000; top: 0; bottom: 0; left: 0; right: 0; background: #2C2C2C; } -.point-of-sale .not-supported-browser .message{ +.pos .not-supported-browser .message{ width:600px; margin-top: 100px; margin-left: auto; @@ -1320,6 +1362,6 @@ color: #d3d3d3; font-size: 14px; } -.point-of-sale .not-supported-browser img{ +.pos .not-supported-browser img{ border-collapse: separate; } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index c98e760641a..32c5515b5ae 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -230,9 +230,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.orderlinewidgets = []; if(this.display_mode === 'maximized'){ - $('.point-of-sale .order-container').css({'bottom':'0px'}); + $('.pos .order-container').css({'bottom':'0px'}); }else if(this.display_mode === 'actionbar'){ - $('.point-of-sale .order-container').css({'bottom':'105px'}); + $('.pos .order-container').css({'bottom':'105px'}); }else if(this.display_mode !== 'numpad'){ console.error('ERROR: OrderWidget renderElement(): wrong display_mode:',this.display_mode); } diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 363c475151c..e17764cecde 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -4,7 +4,7 @@ -
        +
        From 6582780d3b12dacaa40e86e4bd958d3aed011663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 6 Nov 2013 11:32:55 +0100 Subject: [PATCH 17/64] [IMP] point_of_sale: restructured the main widget to use a generic scrollable subwindow system bzr revid: fva@openerp.com-20131106103255-3ym4kh9wom5t1vra --- .../point_of_sale/static/src/css/keyboard.css | 51 ++++++------- addons/point_of_sale/static/src/css/pos.css | 73 +++---------------- addons/point_of_sale/static/src/js/screens.js | 2 - addons/point_of_sale/static/src/js/widgets.js | 24 ++---- addons/point_of_sale/static/src/xml/pos.xml | 45 ++++++++++-- 5 files changed, 79 insertions(+), 116 deletions(-) diff --git a/addons/point_of_sale/static/src/css/keyboard.css b/addons/point_of_sale/static/src/css/keyboard.css index d5ce468d7be..d060735be8d 100644 --- a/addons/point_of_sale/static/src/css/keyboard.css +++ b/addons/point_of_sale/static/src/css/keyboard.css @@ -1,6 +1,6 @@ /* Onscreen Keyboard http://net.tutsplus.com/tutorials/javascript-ajax/creating-a-keyboard-with-css-and-jquery/ */ -.point-of-sale .keyboard_frame{ +.pos .keyboard_frame{ display: none; position:absolute; left: 0; @@ -12,7 +12,6 @@ height: 0px; /* 235px, animated via jquery */ background-color: #BBB; overflow:hidden; - -webkit-box-shadow: 0px 0px 10px rgba(0,0,0, 0.3); -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -22,7 +21,7 @@ z-index:10000; } -.point-of-sale .keyboard_frame .close_button{ +.pos .keyboard_frame .close_button{ height:40px; width:60px; text-align:center; @@ -31,13 +30,12 @@ line-height:40px; border: 1px solid #CCC; -webkit-border-radius: 5px; - -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2); position:absolute; top:0; right:15px; cursor: pointer; } -.point-of-sale .keyboard li { +.pos .keyboard li { float: left; text-align: center; background-color: #fff; @@ -45,12 +43,11 @@ top:0; -moz-border-radius: 5px; -webkit-border-radius: 5px; - -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2); -webkit-transition-property: top, background-color; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: linear; } -.point-of-sale .keyboard li:hover { +.pos .keyboard li:hover { position: relative; top: 2px; left: 0px; @@ -61,22 +58,22 @@ -webkit-transition-duration: 0.1s; -webkit-transition-timing-function: ease-out; } -.point-of-sale .uppercase { +.pos .uppercase { text-transform: uppercase; } -.point-of-sale .on { +.pos .on { display: none; } -.point-of-sale .firstitem{ +.pos .firstitem{ clear: left; } -.point-of-sale .keyboard .lastitem { +.pos .keyboard .lastitem { margin-right: 0 !important; } /* ---- full sized keyboard ---- */ -.point-of-sale .full_keyboard { +.pos .full_keyboard { list-style: none; font-size: 14px; width: 685px; @@ -84,35 +81,35 @@ margin-left: auto !important; margin-right: auto !important; } -.point-of-sale .full_keyboard li{ +.pos .full_keyboard li{ margin: 0 5px 5px 0 !important; width: 40px; height: 40px; line-height: 40px; } -.point-of-sale .full_keyboard .tab, .point-of-sale .full_keyboard .delete { +.pos .full_keyboard .tab, .pos .full_keyboard .delete { width: 70px; } -.point-of-sale .full_keyboard .capslock { +.pos .full_keyboard .capslock { width: 80px; } -.point-of-sale .full_keyboard .return { +.pos .full_keyboard .return { width: 77px; } -.point-of-sale .full_keyboard .left-shift { +.pos .full_keyboard .left-shift { width: 95px; } -.point-of-sale .full_keyboard .right-shift { +.pos .full_keyboard .right-shift { width: 109px; } -.point-of-sale .full_keyboard .space { +.pos .full_keyboard .space { clear: left; width: 673px; } /* ---- simplified keyboard ---- */ -.point-of-sale .simple_keyboard { +.pos .simple_keyboard { list-style: none; font-size: 16px; width: 555px; @@ -120,27 +117,27 @@ margin-left: auto !important; margin-right: auto !important; } -.point-of-sale .simple_keyboard li{ +.pos .simple_keyboard li{ margin: 0 5px 5px 0 !important; width: 49px; height: 49px; line-height: 49px; } -.point-of-sale .simple_keyboard .firstitem.row_asdf{ +.pos .simple_keyboard .firstitem.row_asdf{ margin-left:25px !important; } -.point-of-sale .simple_keyboard .firstitem.row_zxcv{ +.pos .simple_keyboard .firstitem.row_zxcv{ margin-left:55px !important; } -.point-of-sale .simple_keyboard .delete{ +.pos .simple_keyboard .delete{ width: 103px; } -.point-of-sale .simple_keyboard .return{ +.pos .simple_keyboard .return{ width: 103px; } -.point-of-sale .simple_keyboard .space{ +.pos .simple_keyboard .space{ width:273px; } -.point-of-sale .simple_keyboard .numlock{ +.pos .simple_keyboard .numlock{ width:103px; } diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 742f38257e8..8183132ee35 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -19,6 +19,7 @@ -moz-user-select: none; user-select: none; text-shadow: none; + overflow: hidden; } .pos * { @@ -119,39 +120,6 @@ min-height: 30px; } -/* ---- Panes ----- */ - -/* -.pos .headerpane{ - position: absolute; - left: 0px; - right: 0px; - top: 0px; - height: 40px; - background: rgb(100,100,100) -} -.pos .mainpane{ - position: absolute; - left: 0px; - right: 0px; - top: 40px; - bottom: 0px; -} -.pos .leftpane{ - position: absolute; - left: 0px; - width: 400px; - top: 0px; - bottom: 0px; -} -.pos .rightpane{ - position: absolute; - left: 400px; - right: 0px; - top: 0px; - bottom: 0px; -}*/ - /* ********* Generic element styling ********* */ .pos a { @@ -347,16 +315,10 @@ left:0; width:440px; top:0px; - /*bottom:105px;*/ bottom:0; border-right: solid 1px #CECBCB; - background-color: white; } -.pos .pos-leftpane footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; +.pos .pos-leftpane .pads { background: #F0EEEE; white-space: nowrap; } @@ -670,6 +632,7 @@ top:0px; bottom:0px; width:100%; + overflow: auto; } .pos .screen header h2 { margin-top: 0px; @@ -742,21 +705,20 @@ margin-left: 16px; background: white; color: #4c4c4c; + box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset; -webkit-animation: all 250ms linear; } .pos .pos-step-container input:focus{ - color: #5d7ad6; + color: rgb(130, 124, 255); + box-shadow: 0px 2px rgba(130, 124, 255, 0.3) inset; -webkit-animation: all 250ms linear; } .pos .pos-payment-container { text-align: left; - min-width: 500px; - margin-top: 50px; - padding: 40px; - background-color: #f8f8f8; - border: solid 1px rgb(220,220,220); + min-width: 320px; + margin-top: 40px; } .pos .pos-payment-container .left-block{ display: inline-block; @@ -976,8 +938,8 @@ .pos .order-container{ position: absolute; top: 0px; - bottom: 232px; width:100%; + height: 100%; background: #F0EEEE; } @@ -1099,14 +1061,9 @@ /* ********* The ActionBarWidget ********* */ .pos .pos-actionbar{ - position:absolute; - left: 0; - bottom: 0px; height: 105px; - width: 100%; - margin: 0; background: #f5f5f5; /*#ebebeb;*/ - border-top: solid 1px #afafb6; + border-top: solid 1px #cecece; z-index:900; } @@ -1114,16 +1071,6 @@ list-style: none; } -.pos .pos-actionbar-left-pane{ - height: 100%; - width: 434px; - margin: 0px; - padding-left:3px; - padding-right:3px; - border-right: solid 1px #dfdfdf; - float: left; -} - .pos .pos-actionbar-button-list{ height: 100%; margin: 0px; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index c6261477fe6..ae848ac79b3 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -227,13 +227,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // with add_action_button() show_action_bar: function(){ this.pos_widget.action_bar.show(); - this.$el.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.$el.css({'bottom':'0px'}); }, // adds a new button to the action bar. The button definition takes three parameters, all optional : diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 32c5515b5ae..0b3c1bc344f 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -229,14 +229,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } this.orderlinewidgets = []; - if(this.display_mode === 'maximized'){ - $('.pos .order-container').css({'bottom':'0px'}); - }else if(this.display_mode === 'actionbar'){ - $('.pos .order-container').css({'bottom':'105px'}); - }else if(this.display_mode !== 'numpad'){ - console.error('ERROR: OrderWidget renderElement(): wrong display_mode:',this.display_mode); - } - var $content = this.$('.orderlines'); this.currentOrderLines.each(_.bind( function(orderLine) { var line = new module.OrderlineWidget(this, { @@ -889,25 +881,25 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // -------- Screens --------- this.product_screen = new module.ProductScreenWidget(this,{}); - this.product_screen.appendTo(this.$('.pos-rightpane')); + this.product_screen.appendTo(this.$('.screens')); this.receipt_screen = new module.ReceiptScreenWidget(this, {}); - this.receipt_screen.appendTo(this.$('.pos-rightpane')); + this.receipt_screen.appendTo(this.$('.screens')); this.payment_screen = new module.PaymentScreenWidget(this, {}); - this.payment_screen.appendTo(this.$('.pos-rightpane')); + this.payment_screen.appendTo(this.$('.screens')); this.welcome_screen = new module.WelcomeScreenWidget(this,{}); - this.welcome_screen.appendTo(this.$('.pos-rightpane')); + this.welcome_screen.appendTo(this.$('.screens')); this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {}); - this.client_payment_screen.appendTo(this.$('.pos-rightpane')); + this.client_payment_screen.appendTo(this.$('.screens')); this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {}); - this.scale_invite_screen.appendTo(this.$('.pos-rightpane')); + this.scale_invite_screen.appendTo(this.$('.screens')); this.scale_screen = new module.ScaleScreenWidget(this,{}); - this.scale_screen.appendTo(this.$('.pos-rightpane')); + this.scale_screen.appendTo(this.$('.screens')); // -------- Popups --------- @@ -944,7 +936,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.username.replace(this.$('.placeholder-UsernameWidget')); this.action_bar = new module.ActionBarWidget(this); - this.action_bar.appendTo(this.$(".pos-rightpane")); + this.action_bar.replace(this.$(".placeholder-RightActionBar")); this.left_action_bar = new module.ActionBarWidget(this); this.left_action_bar.replace(this.$('.placeholder-LeftActionBar')); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index e17764cecde..3d398f22d4c 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -19,23 +19,52 @@
        +
        -
        -
        -
        -
        -
        -
        +
        +
        +
        +
        +
        +
        + + + + +
        +
        +
        +
        +
        +
        +
        + + +
        -
        -
        + +
        + <!--[if IE]>
        From 7f0e721cc105edcdc1c4f81981adca7a6bb1322f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 6 Nov 2013 17:17:23 +0100 Subject: [PATCH 18/64] [IMP] point_of_sale: more css adaptations for iPad / android + a few bug correction along the way bzr revid: fva@openerp.com-20131106161723-vus7amgc6cfw3bzr --- addons/point_of_sale/point_of_sale.py | 1 + addons/point_of_sale/point_of_sale_view.xml | 1 + .../point_of_sale/static/src/css/keyboard.css | 4 +- addons/point_of_sale/static/src/css/pos.css | 98 ++++++++++++------ .../static/src/img/scroll-down.png | Bin 0 -> 357 bytes .../static/src/img/scroll-up.png | Bin 0 -> 303 bytes addons/point_of_sale/static/src/js/models.js | 4 +- addons/point_of_sale/static/src/js/screens.js | 49 +++++++-- .../static/src/js/widget_keyboard.js | 17 +-- addons/point_of_sale/static/src/js/widgets.js | 5 +- 10 files changed, 131 insertions(+), 48 deletions(-) create mode 100644 addons/point_of_sale/static/src/img/scroll-down.png create mode 100644 addons/point_of_sale/static/src/img/scroll-up.png diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index d32b8e91d3a..344eddb5f63 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -64,6 +64,7 @@ class pos_config(osv.osv): 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'), 'iface_print_via_proxy' : fields.boolean('Print via Proxy'), 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), + 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True), 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 7202b57a1eb..11893bd966e 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -796,6 +796,7 @@ + diff --git a/addons/point_of_sale/static/src/css/keyboard.css b/addons/point_of_sale/static/src/css/keyboard.css index d060735be8d..36cbea09368 100644 --- a/addons/point_of_sale/static/src/css/keyboard.css +++ b/addons/point_of_sale/static/src/css/keyboard.css @@ -28,7 +28,6 @@ background-color: #DDD; font-size: 12px; line-height:40px; - border: 1px solid #CCC; -webkit-border-radius: 5px; position:absolute; top:0; @@ -41,13 +40,14 @@ background-color: #fff; border: 1px solid #f0f0f0; top:0; + cursor: pointer; -moz-border-radius: 5px; -webkit-border-radius: 5px; -webkit-transition-property: top, background-color; -webkit-transition-duration: 0.2s; -webkit-transition-timing-function: linear; } -.pos .keyboard li:hover { +.pos .keyboard li:active{ position: relative; top: 2px; left: 0px; diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 8183132ee35..f1a0ea84b46 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -217,6 +217,7 @@ margin-right:0px; border: 1px solid #353A7E; background: #7f82ac; + text-shadow: none; } .pos .pos-rightheader button.square{ @@ -410,7 +411,7 @@ width:100%; height:100%; overflow: hidden; - overflow-y: scroll; + overflow-y: auto; -webkit-overflow-scrolling: touch; } @@ -598,6 +599,7 @@ line-height: 13px; background: #7f82ac; padding: 2px 5px; + border-radius: 2px; } .pos .product .product-name { @@ -633,6 +635,7 @@ bottom:0px; width:100%; overflow: auto; + -webkit-overflow-scrolling: touch; } .pos .screen header h2 { margin-top: 0px; @@ -646,8 +649,6 @@ margin-left: auto; margin-right: auto; margin-top: 50px; - padding: 40px; - background-color: #f8f8f8; text-align: center; } .pos .dialog p{ @@ -828,14 +829,11 @@ .pos .scale-screen .display{ position:relative; - width:600px; + width: 550px; height:190px; margin-top: 100px; margin-left: auto; margin-right: auto; - padding: 40px; - background: #f1f1f1; - border-top: 1px solid #efefef; } .pos .scale-screen .product-picture { @@ -854,11 +852,13 @@ background:#fff; border: 1px solid #fff; + border-radius: 3px; + box-shadow: 0px 2px rgb(228, 228, 228); } .pos .scale-screen .product-picture img{ - max-width: 178px; - max-height:178px; + max-width: 178px; + max-height: 178px; vertical-align: middle; cursor:pointer; } @@ -872,40 +872,43 @@ line-height:1; color:white; background: #7f82ac; - padding: 2px 5px; + padding: 4px 5px; + border-radius: 3px; cursor:pointer; } .pos .scale-screen .product-name { position: absolute; - left:40px; - top:50px; - height:50px; - font-size:36px; - line-height:50px; - text-align:right; - right:275px; + left: 0px; + right: 210px; + top: 10px; + height: 50px; + font-size: 36px; + line-height: 50px; + text-align: right; color: #8d8d8d; overflow: hidden; text-overflow: ellipsis; } .pos .scale-screen .weight{ - position: absolute; - left: 40px; - height: 90px; - bottom: 55px; - right: 260px; - padding: 6px; - padding-right: 33px; - padding-top: 11px; - background: white; - + position: absolute; + left: 0; + top: 80px; + height: 90px; + bottom: 55px; + right: 210px; + padding: 6px; + padding-right: 33px; + padding-top: 11px; + background: white; + border-radius: 3px; + box-shadow: 0px 2px rgb(228, 228, 228) inset; } .pos .scale-screen .weight p{ display: inline-block; text-align:right; line-height: 90px; - font-size: 70px; + font-size: 56px; width: 100%; height: 100%; margin: 0; @@ -930,6 +933,8 @@ font-weight:bold; background-color: #F0EEEE; border: 1px solid #E0DDDD; + border-radius: 3px; + box-shadow: 0px 10px 20px rgba(0,0,0,0.4); z-index:1150; } @@ -947,7 +952,7 @@ width:100%; height:100%; overflow:hidden; - overflow-y: scroll; + overflow-y: auto; -webkit-overflow-scrolling: touch; } @@ -989,7 +994,7 @@ border-color:#777; } .pos .order .summary .line .subentry{ - font-size: 10px; + font-size: 14px; font-weight: normal; text-align: center; } @@ -1112,6 +1117,9 @@ -webkit-transition-timing-function: ease-out; } +.pos .pos-actionbar .button.disabled{ + opacity: 0.5; +} .pos .pos-actionbar .button.disabled:active{ border: 1px solid #cacaca; color: #555; @@ -1220,6 +1228,8 @@ font-size:20px; font-weight:bold; background-color: #F0EEEE; + border-radius: 3px; + box-shadow: 0px 10px 20px rgba(0,0,0,0.4); z-index:1200; } .pos .popup .footer{ @@ -1292,6 +1302,34 @@ min-height: 30px; } +.pos.big-scrollbars *::-webkit-scrollbar{ + width: 40px; + height: 40px; +} +.pos.big-scrollbars *::-webkit-scrollbar-track{ + background: rgb(224,224,224); + border-left: none; +} +.pos.big-scrollbars *::-webkit-scrollbar-thumb{ + background: rgb(168,168,168); + min-height: 40px; + border-radius: 3px; +} +.pos.big-scrollbars *::-webkit-scrollbar-button{ + width: 40px; + height: 40px; + border-radius: 3px; + background: rgb(210,210,210); + background-size: cover; +} +.pos.big-scrollbars *::-webkit-scrollbar-button:decrement{ + background-image: url('../img/scroll-up.png'); +} +.pos.big-scrollbars *::-webkit-scrollbar-button:increment{ + background-image: url('../img/scroll-down.png'); +} + + /* ********* Unsupported Browser Page ********* */ .pos .not-supported-browser{ diff --git a/addons/point_of_sale/static/src/img/scroll-down.png b/addons/point_of_sale/static/src/img/scroll-down.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd075891572fd17bde6f2abeecba83ed8a81625 GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8!3Q zuY)k7lg8`{prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt+!{T^vI) z8sFa7=zBOo!u8=}6ns~o@SJxN$lar)xe=kn0 z4BGij>?Pw`QH5B>OBlr6RS$1TKj7TKs`fALSV@tNp3QyP_s05LpGK^nf9`!`?6#Z? z)&=*@yX@K0_nvh|^Np_(_k|mF1@70A_muGdjyXwCB zzVKM_RAJ27xmq*k%(uRnEmJ0x1)z& z7b)&Dw7d4qVM$+nuZ;Zd Date: Mon, 25 Nov 2013 18:27:26 +0100 Subject: [PATCH 19/64] [WIP] point_of_sale: mobile ui optimisations bzr revid: fva@openerp.com-20131125172726-xp81rn938rtfwlbs --- addons/point_of_sale/static/src/css/pos.css | 5 ++++ addons/point_of_sale/static/src/js/screens.js | 12 ++++++-- addons/point_of_sale/static/src/js/widgets.js | 30 ++++++++++++++++--- addons/point_of_sale/static/src/xml/pos.xml | 2 +- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index d5fcd663fc3..8e1fcb8c28e 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -3,6 +3,10 @@ src: url(../fonts/Inconsolata.otf); } +.oe_hidden{ + display: none !important; +} + .pos { padding: 0; margin: 0; @@ -416,6 +420,7 @@ overflow: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; + -webkit-transform: translate3d(0,0,0); } .pos .product-list-container { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index a133587fcca..4c5f2245e0d 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -970,7 +970,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var currentOrder = this.pos.get('selectedOrder'); - if(currentOrder.getPaidTotal() + 0.000001 < currentOrder.getTotalTaxIncluded()){ + if(!this.isPaymentPaid()){ return; } @@ -1066,6 +1066,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.currentPaymentLines.remove([lineWidget.payment_line]); lineWidget.destroy(); }, + isPaymentPaid: function(){ + var currentOrder = this.pos.get('selectedOrder'); + return (currentOrder.getTotalTaxIncluded < 0.000001 + && currentOrder.getPaidTotal() + 0.000001 < currentOrder.getTotalTaxIncluded()); + + }, updatePaymentSummary: function() { var currentOrder = this.pos.get('selectedOrder'); var paidTotal = currentOrder.getPaidTotal(); @@ -1082,8 +1088,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } if(this.pos_widget.action_bar){ - this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0.000001); - this.pos_widget.action_bar.set_button_disabled('invoice', remaining > 0.000001); + this.pos_widget.action_bar.set_button_disabled('validation', this.isPaymentPaid()); + this.pos_widget.action_bar.set_button_disabled('invoice', this.isPaymentPaid()); } }, set_numpad_state: function(numpadState) { diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 6c65528f402..6a222cd1ca4 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -213,13 +213,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var scroller = this.$('.order-scroller')[0]; var scrollbottom = true; var scrollTop = 0; - if(scroller){ + /*if(scroller){ var overflow_bottom = scroller.scrollHeight - scroller.clientHeight; scrollTop = scroller.scrollTop; if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){ scrollbottom = false; } - } + }*/ this._super(); // freeing subwidgets @@ -245,11 +245,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa scroller = this.$('.order-scroller')[0]; if(scroller){ + //scroller.scrollTop = 1000000; + /* if(scrollbottom){ scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; }else{ scroller.scrollTop = scrollTop; - } + }*/ } }, update_summary: function(){ @@ -835,6 +837,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.pos.delete_current_order(); }); + $('body').on('keyup',function(event){ + if(event.which === 13){ + self.set_fullscreen(); + } + }); + //when a new order is created, add an order button widget self.pos.get('orders').bind('add', function(new_order){ var new_order_button = new module.OrderButtonWidget(null, { @@ -858,6 +866,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.pos.barcode_reader.connect(); + if(!$('#oe-fullscreenwidget-viewport').length){ + $('head').append(''); + $('head').append(''); + } + + $('.oe_leftbar').addClass('oe_hidden'); + instance.webclient.set_content_full_screen(true); if (!self.pos.get('pos_session')) { @@ -1050,7 +1065,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } } }, - + set_fullscreen: function(){ + if(this.el.webkitRequestFullscreen){ + this.el.webkitRequestFullscreen(); + } + }, //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.) set_leftpane_visible: function(visible){ if(visible !== this.leftpane_visible){ @@ -1105,6 +1124,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa destroy: function() { this.pos.destroy(); instance.webclient.set_content_full_screen(false); + $('.oe_leftbar').removeClass('oe_hidden'); + $('#oe-pos-viewport').remove(); + $('#oe-pos-apple-mobile').remove(); this._super(); } }); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 6686015df5c..42e5b7ef44d 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -580,7 +580,7 @@ - + From 165af24f49573bfc31e990f2ca3642a960136f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 27 Nov 2013 15:44:27 +0100 Subject: [PATCH 20/64] [WIP] point_of_sale: more progress on ipadification: the app now is in a separate controller, with proper meta tags for standalone apps, pixel ratio, etc bzr revid: fva@openerp.com-20131127144427-odf37v9dthuw4yrm --- addons/point_of_sale/controllers/main.py | 227 +++++------------- addons/point_of_sale/static/src/css/pos.css | 6 + .../static/src/img/touch-icon-ipad-retina.png | Bin 0 -> 3441 bytes .../static/src/img/touch-icon-ipad.png | Bin 0 -> 4098 bytes .../src/img/touch-icon-iphone-retina.png | Bin 0 -> 8403 bytes .../static/src/img/touch-icon-iphone.png | Bin 0 -> 3745 bytes .../static/src/img/touch-icon.svg | 199 +++++++++++++++ addons/point_of_sale/static/src/js/models.js | 2 +- addons/point_of_sale/static/src/js/screens.js | 19 +- addons/point_of_sale/static/src/js/widgets.js | 29 +-- addons/point_of_sale/static/src/xml/pos.xml | 2 +- 11 files changed, 291 insertions(+), 193 deletions(-) create mode 100644 addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png create mode 100644 addons/point_of_sale/static/src/img/touch-icon-ipad.png create mode 100644 addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png create mode 100644 addons/point_of_sale/static/src/img/touch-icon-iphone.png create mode 100644 addons/point_of_sale/static/src/img/touch-icon.svg diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 2461fe90b79..76dd705cbfc 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -8,182 +8,73 @@ import random from openerp import http from openerp.http import request -from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template +from openerp.addons.web.controllers.main import manifest_list, module_boot _logger = logging.getLogger(__name__) -class PointOfSaleController(http.Controller): - def __init__(self): - self.scale = 'closed' - self.scale_weight = 0.0 +html_template = """ + + + OpenERP Point of Sale - @http.route('/pos/app', type='http', auth='admin') - def app(self): - js = "\n ".join('' % i for i in manifest_list('js',db=request.db)) - css = "\n ".join('' % i for i in manifest_list('css',db=request.db)) + + - cookie = request.httprequest.cookies.get("instance0|session_id") - session_id = cookie.replace("%22","") - template = html_template.replace(' + - r = template % { + + + + + + + + %(css)s + %(js)s + + + + + + +""" + +class PosController(http.Controller): + + @http.route('/pos/web', type='http', auth='none') + def a(self, debug=False, **k): + js_list = manifest_list('js',db=request.db, debug=debug) + css_list = manifest_list('css',db=request.db, debug=debug) + + print css_list + print js_list + + js = "\n".join('' % i for i in js_list) + css = "\n".join('' % i for i in css_list) + r = html_template % { 'js': js, 'css': css, - 'modules': simplejson.dumps(module_boot(request)), - 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));' + 'modules': simplejson.dumps(module_boot(request.db)), + 'init': """ + window.navigator.standalone = true; + var wc = new s.web.WebClient(); + wc.appendTo($(document.body)); + wc.show_application = function(){ + wc.action_manager.do_action("pos.ui"); + }; + wc.show_login = function(){ + window.location.href = '/'; + } + """ } return r - @http.route('/pos/manifest',type='http', auth='admin') - def manifest(self): - """ This generates a HTML5 cache manifest files that preloads the categories and products thumbnails - and other ressources necessary for the point of sale to work offline """ - - ml = ["CACHE MANIFEST"] - - # loading all the images in the static/src/img/* directories - def load_css_img(srcdir,dstdir): - for f in os.listdir(srcdir): - path = os.path.join(srcdir,f) - dstpath = os.path.join(dstdir,f) - if os.path.isdir(path) : - load_css_img(path,dstpath) - elif f.endswith(('.png','.PNG','.jpg','.JPG','.jpeg','.JPEG','.gif','.GIF')): - ml.append(dstpath) - - imgdir = openerp.modules.get_module_resource('point_of_sale','static/src/img'); - load_css_img(imgdir,'/point_of_sale/static/src/img') - - products = request.registry.get('product.product') - for p in products.search_read(request.cr, request.uid, [('pos_categ_id','!=',False)], ['name']): - product_id = p['id'] - url = "/web/binary/image?session_id=%s&model=product.product&field=image&id=%s" % (request.session_id, product_id) - ml.append(url) - - categories = request.registry.get('pos.category') - for c in categories.search_read(request.cr, request.uid, [], ['name']): - category_id = c['id'] - url = "/web/binary/image?session_id=%s&model=pos.category&field=image&id=%s" % (request.session_id, category_id) - ml.append(url) - - ml += ["NETWORK:","*"] - m = "\n".join(ml) - - return m - - @http.route('/pos/test_connection', type='json', auth='admin') - def test_connection(self): - _logger.info('Received Connection Test from the Point of Sale'); - - @http.route('/pos/scan_item_success', type='json', auth='admin') - def scan_item_success(self, ean): - """ - A product has been scanned with success - """ - print 'scan_item_success: ' + str(ean) - - @http.route('/pos/scan_item_error_unrecognized', type='json', auth='admin') - def scan_item_error_unrecognized(self, ean): - """ - A product has been scanned without success - """ - print 'scan_item_error_unrecognized: ' + str(ean) - - @http.route('/pos/help_needed', type='json', auth='admin') - def help_needed(self): - """ - The user wants an help (ex: light is on) - """ - print "help_needed" - - @http.route('/pos/help_canceled', type='json', auth='admin') - def help_canceled(self): - """ - The user stops the help request - """ - print "help_canceled" - - @http.route('/pos/weighting_start', type='json', auth='admin') - def weighting_start(self): - if self.scale == 'closed': - print "Opening (Fake) Connection to Scale..." - self.scale = 'open' - self.scale_weight = 0.0 - time.sleep(0.1) - print "... Scale Open." - else: - print "WARNING: Scale already Connected !!!" - - @http.route('/pos/weighting_read_kg', type='json', auth='admin') - def weighting_read_kg(self): - if self.scale == 'open': - print "Reading Scale..." - time.sleep(0.025) - self.scale_weight += 0.01 - print "... Done." - return self.scale_weight - else: - print "WARNING: Reading closed scale !!!" - return 0.0 - - @http.route('/pos/weighting_end', type='json', auth='admin') - def weighting_end(self): - if self.scale == 'open': - print "Closing Connection to Scale ..." - self.scale = 'closed' - self.scale_weight = 0.0 - time.sleep(0.1) - print "... Scale Closed." - else: - print "WARNING: Scale already Closed !!!" - - @http.route('/pos/payment_request', type='json', auth='admin') - def payment_request(self, price): - """ - The PoS will activate the method payment - """ - print "payment_request: price:"+str(price) - return 'ok' - - @http.route('/pos/payment_status', type='json', auth='admin') - def payment_status(self): - print "payment_status" - return { 'status':'waiting' } - - @http.route('/pos/payment_cancel', type='json', auth='admin') - def payment_cancel(self): - print "payment_cancel" - - @http.route('/pos/transaction_start', type='json', auth='admin') - def transaction_start(self): - print 'transaction_start' - - @http.route('/pos/transaction_end', type='json', auth='admin') - def transaction_end(self): - print 'transaction_end' - - @http.route('/pos/cashier_mode_activated', type='json', auth='admin') - def cashier_mode_activated(self): - print 'cashier_mode_activated' - - @http.route('/pos/cashier_mode_deactivated', type='json', auth='admin') - def cashier_mode_deactivated(self): - print 'cashier_mode_deactivated' - - @http.route('/pos/open_cashbox', type='json', auth='admin') - def open_cashbox(self): - print 'open_cashbox' - - @http.route('/pos/print_receipt', type='json', auth='admin') - def print_receipt(self, receipt): - print 'print_receipt' + str(receipt) - - @http.route('/pos/log', type='json', auth='admin') - def log(self, arguments): - _logger.info(' '.join(str(v) for v in arguments)) - - @http.route('/pos/print_pdf_invoice', type='json', auth='admin') - def print_pdf_invoice(self, pdfinvoice): - print 'print_pdf_invoice' + str(pdfinvoice) - - diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 8e1fcb8c28e..8935e6d2dc5 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -30,6 +30,12 @@ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } +.pos input::-webkit-outer-spin-button, +.pos input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + .pos button{ box-shadow: none; outline: none; diff --git a/addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png b/addons/point_of_sale/static/src/img/touch-icon-ipad-retina.png new file mode 100644 index 0000000000000000000000000000000000000000..4f1e1db5c98573b2eb39abc8ad74fd8327de5b86 GIT binary patch literal 3441 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 TGU-1d00000NkvXXu0mjfNvD=t literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/touch-icon-ipad.png b/addons/point_of_sale/static/src/img/touch-icon-ipad.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8c111406f6470bdd6868bc74a80b647efa73de GIT binary patch literal 4098 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 Ar~m)} literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png b/addons/point_of_sale/static/src/img/touch-icon-iphone-retina.png new file mode 100644 index 0000000000000000000000000000000000000000..593c1506061954de69358c3f7009bacc68f928d0 GIT binary patch literal 8403 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 literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/touch-icon-iphone.png b/addons/point_of_sale/static/src/img/touch-icon-iphone.png new file mode 100644 index 0000000000000000000000000000000000000000..4122e61f9c0af572442fdd0eb60e8fe1a2dbce25 GIT binary patch literal 3745 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 LNkvXXu0mjfr8rij literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/touch-icon.svg b/addons/point_of_sale/static/src/img/touch-icon.svg new file mode 100644 index 00000000000..8ce5f30e13e --- /dev/null +++ b/addons/point_of_sale/static/src/img/touch-icon.svg @@ -0,0 +1,199 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 3de24b22fbb..68acd4b874e 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -378,7 +378,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal _flush_order: function(order_id, options){ var self = this; options = options || {}; - timeout = typeof options.timeout === 'number' ? options.timeout : 5000; + timeout = typeof options.timeout === 'number' ? options.timeout : 7500; var order = this.db.get_order(order_id); order.to_invoice = options.to_invoice || false; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 4c5f2245e0d..88a83fc52e1 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -812,6 +812,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(); var self = this; + this.pos_widget.set_numpad_visible(false); + var print_button = this.add_action_button({ label: _t('Print'), icon: '/point_of_sale/static/src/img/icons/png48/printer.png', @@ -872,6 +874,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.currentOrder = this.pos.get('selectedOrder'); $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this})); }, + close: function(){ + this._super(); + this.pos_widget.set_numpad_visible(true); + } }); module.PaymentScreenWidget = module.ScreenWidget.extend({ @@ -1006,6 +1012,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.screen_selector.set_current_screen(this.next_screen); } } + // hide onscreen (iOS) keyboard + setTimeout(function(){ + document.activeElement.blur(); + $("input").blur(); + },250); }, bindPaymentLineEvents: function() { this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); @@ -1068,8 +1079,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, isPaymentPaid: function(){ var currentOrder = this.pos.get('selectedOrder'); - return (currentOrder.getTotalTaxIncluded < 0.000001 - && currentOrder.getPaidTotal() + 0.000001 < currentOrder.getTotalTaxIncluded()); + return (currentOrder.getTotalTaxIncluded() >= 0.000001 + && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); }, updatePaymentSummary: function() { @@ -1088,8 +1099,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } if(this.pos_widget.action_bar){ - this.pos_widget.action_bar.set_button_disabled('validation', this.isPaymentPaid()); - this.pos_widget.action_bar.set_button_disabled('invoice', this.isPaymentPaid()); + this.pos_widget.action_bar.set_button_disabled('validation', !this.isPaymentPaid()); + this.pos_widget.action_bar.set_button_disabled('invoice', !this.isPaymentPaid()); } }, set_numpad_state: function(numpadState) { diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 6a222cd1ca4..2939eb39390 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -213,13 +213,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var scroller = this.$('.order-scroller')[0]; var scrollbottom = true; var scrollTop = 0; - /*if(scroller){ + if(scroller){ var overflow_bottom = scroller.scrollHeight - scroller.clientHeight; scrollTop = scroller.scrollTop; if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){ scrollbottom = false; } - }*/ + } this._super(); // freeing subwidgets @@ -244,14 +244,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.update_summary(); scroller = this.$('.order-scroller')[0]; + if(scroller){ - //scroller.scrollTop = 1000000; - /* if(scrollbottom){ scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; }else{ scroller.scrollTop = scrollTop; - }*/ + } } }, update_summary: function(){ @@ -313,8 +312,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa focus: function(){ var val = this.$('input')[0].value; this.$('input')[0].focus(); - this.$('input')[0].value = val; - this.$('input')[0].select(); + if(Number(val) === 0){ + this.$('input')[0].value = ''; + }else{ + this.$('input')[0].value = val; + this.$('input')[0].select(); + } }, }); @@ -863,16 +866,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.screen_selector.set_default_screen(); - self.pos.barcode_reader.connect(); - if(!$('#oe-fullscreenwidget-viewport').length){ - $('head').append(''); - $('head').append(''); - } - - $('.oe_leftbar').addClass('oe_hidden'); - instance.webclient.set_content_full_screen(true); if (!self.pos.get('pos_session')) { @@ -1104,7 +1099,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) { var action = result; action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}}); - //self.destroy(); this.do_action(action); }, this)); }, self)); @@ -1124,9 +1118,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa destroy: function() { this.pos.destroy(); instance.webclient.set_content_full_screen(false); - $('.oe_leftbar').removeClass('oe_hidden'); - $('#oe-pos-viewport').remove(); - $('#oe-pos-apple-mobile').remove(); this._super(); } }); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 42e5b7ef44d..4eaf9e53604 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -580,7 +580,7 @@ - + From 14e995b4525eda9fb7fd550a54d0b6190e66471a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 27 Nov 2013 20:17:50 +0100 Subject: [PATCH 21/64] [WIP] point_of_sale: improving performance on mobile devices bzr revid: fva@openerp.com-20131127191750-ob13h0s99b9amsa5 --- addons/point_of_sale/controllers/main.py | 10 ++++- addons/point_of_sale/static/lib/fastclick.js | 2 +- addons/point_of_sale/static/src/css/pos.css | 1 + .../static/src/img/touch-icon-128.png | Bin 0 -> 7118 bytes .../static/src/img/touch-icon-196.png | Bin 0 -> 6442 bytes addons/point_of_sale/static/src/js/widgets.js | 13 ++++++- addons/point_of_sale/static/src/xml/pos.xml | 36 +++++++++--------- 7 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 addons/point_of_sale/static/src/img/touch-icon-128.png create mode 100644 addons/point_of_sale/static/src/img/touch-icon-196.png diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 76dd705cbfc..87244a9c90c 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -15,14 +15,17 @@ _logger = logging.getLogger(__name__) html_template = """ - OpenERP Point of Sale + OpenERP POS + + + @@ -52,12 +55,17 @@ class PosController(http.Controller): @http.route('/pos/web', type='http', auth='none') def a(self, debug=False, **k): + + print '\nDEBUG',debug,'\n' + js_list = manifest_list('js',db=request.db, debug=debug) css_list = manifest_list('css',db=request.db, debug=debug) print css_list print js_list + js_list = [ js for js in js_list if 'select2' not in js ] + js = "\n".join('' % i for i in js_list) css = "\n".join('' % i for i in css_list) r = html_template % { diff --git a/addons/point_of_sale/static/lib/fastclick.js b/addons/point_of_sale/static/lib/fastclick.js index a30edb3cb31..a64edbe1a72 100644 --- a/addons/point_of_sale/static/lib/fastclick.js +++ b/addons/point_of_sale/static/lib/fastclick.js @@ -733,7 +733,7 @@ FastClick.notNeeded = function(layer) { if (FastClick.prototype.deviceIsAndroid) { metaViewport = document.querySelector('meta[name=viewport]'); if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) { - return true; + return false; //WIP FIX } // Chrome desktop doesn't need FastClick (issue #15) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 8935e6d2dc5..1d0cbb41589 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -589,6 +589,7 @@ border-radius: 3px; border-bottom-width: 3px; overflow: hidden; + cursor: pointer; } .pos .product .product-img { diff --git a/addons/point_of_sale/static/src/img/touch-icon-128.png b/addons/point_of_sale/static/src/img/touch-icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..5bbf31d645768d4ef77e15dac09066b1a7cd2493 GIT binary patch literal 7118 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 Ef+4UmTmS$7 literal 0 HcmV?d00001 diff --git a/addons/point_of_sale/static/src/img/touch-icon-196.png b/addons/point_of_sale/static/src/img/touch-icon-196.png new file mode 100644 index 0000000000000000000000000000000000000000..dbde2d99447f856abaaa9337d20ad2b4031f25ef GIT binary patch literal 6442 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 -
      1. - -
        - - - - - - - - - - - -
        -
        - -
        -
        +
      2. +
        + + + + + + + + + + + +
        +
        + +
      3. From 7d062a0b45a806a926f23d2e9a6ff6f0cec5e371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 2 Dec 2013 17:21:44 +0100 Subject: [PATCH 22/64] [WIP] point_of_sale: improved performance of the product screen & order widget + ongoing cleanup bzr revid: fva@openerp.com-20131202162144-6se3s0wjwz0jdfih --- addons/point_of_sale/controllers/main.py | 10 +- addons/point_of_sale/static/src/css/pos.css | 3 +- .../static/src/css/pos_nohover.css | 1168 ----------------- addons/point_of_sale/static/src/js/TODO.txt | 63 - addons/point_of_sale/static/src/js/models.js | 51 +- addons/point_of_sale/static/src/js/screens.js | 100 +- .../static/src/js/widget_scrollbar.js | 265 ---- addons/point_of_sale/static/src/js/widgets.js | 410 +++--- addons/point_of_sale/static/src/xml/pos.xml | 66 +- 9 files changed, 339 insertions(+), 1797 deletions(-) delete mode 100644 addons/point_of_sale/static/src/css/pos_nohover.css delete mode 100644 addons/point_of_sale/static/src/js/TODO.txt delete mode 100644 addons/point_of_sale/static/src/js/widget_scrollbar.js diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 87244a9c90c..705fd853ec1 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -56,7 +56,6 @@ class PosController(http.Controller): @http.route('/pos/web', type='http', auth='none') def a(self, debug=False, **k): - print '\nDEBUG',debug,'\n' js_list = manifest_list('js',db=request.db, debug=debug) css_list = manifest_list('css',db=request.db, debug=debug) @@ -64,8 +63,6 @@ class PosController(http.Controller): print css_list print js_list - js_list = [ js for js in js_list if 'select2' not in js ] - js = "\n".join('' % i for i in js_list) css = "\n".join('' % i for i in css_list) r = html_template % { @@ -73,15 +70,18 @@ class PosController(http.Controller): 'css': css, 'modules': simplejson.dumps(module_boot(request.db)), 'init': """ - window.navigator.standalone = true; + console.log('LOADED'); var wc = new s.web.WebClient(); - wc.appendTo($(document.body)); wc.show_application = function(){ + console.log('SHOW APPLICATION'); wc.action_manager.do_action("pos.ui"); }; wc.show_login = function(){ + console.log('SHOW LOGIN'); window.location.href = '/'; } + wc.appendTo($(document.body)); + console.log('APPENDED'); """ } return r diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 1d0cbb41589..9d728f8c7da 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1163,7 +1163,8 @@ background: rgba(0,0,0,0.82); color: white; padding-bottom: 10px; - cursor:move; + cursor: move; + -webkit-transform: translate3d(0,0,0); } .pos .debug-widget .toggle{ position: absolute; diff --git a/addons/point_of_sale/static/src/css/pos_nohover.css b/addons/point_of_sale/static/src/css/pos_nohover.css deleted file mode 100644 index 6f24abda53b..00000000000 --- a/addons/point_of_sale/static/src/css/pos_nohover.css +++ /dev/null @@ -1,1168 +0,0 @@ -@font-face{ - font-family: 'Inconsolata'; - src: url(../fonts/Inconsolata.otf); -} - -.point-of-sale { - padding: 0; - margin: 0; - background-color: #f0eeee; - font-family: "Lucida Grande", Helvetica, Verdana, Arial; - color: #555555; - font-size: 12px; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.point-of-sale .shadow-top{ - position: absolute; - top:0; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0)); - 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; - left:0; - right:0; - height:10px; - background: -webkit-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - background: -moz-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0)); - 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{ - background-color: #222; - position:absolute; - left:0px; - top:0px; - width:100%; - height:100%; - z-index: 999; - text-align: center; -} -.point-of-sale .loader img{ - position:absolute; - top:50%; - left:50%; -} - -/* ********* Generic element styling ********* */ - -.point-of-sale a { - text-decoration: none; - color: #555555; -} -.point-of-sale button, .point-of-sale a.button { - display: inline-block; - cursor: pointer; - padding: 4px 10px; - font-size: 11px; - border: 1px solid #cacaca; - border-radius: 4px; - background: #e2e2e2; - background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2)); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); -} -.point-of-sale ul, .point-of-sale ol { - padding: 0; - margin: 0; -} -.point-of-sale li { - list-style-type: none; -} -.point-of-sale .pos-right-align { - text-align: right; -} -.point-of-sale .pos-right-align input { - text-align: right; - border: 1px solid #cecbcb; - border-radius: 4px; -} - -/* ********* The black header bar ********* */ - - -.point-of-sale #topheader { - position:absolute; - left:0; - top:0; - width: 100%; - height: 33px; - margin:0; - padding:0; - color: gray; - border-top: solid 1px #d3d3d3; - border-bottom: solid 1px black; - background: #393939; - background: -moz-linear-gradient(#7b7979, #393939); - background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939)); -} - -/* a) The left part of the top-bar */ - -.point-of-sale #branding{ - position: absolute; - display: table-cell; - left:0; - top:0; - width:439px; - height:100%; - margin:0; - padding:0; - border-right: 1px solid #373737; - text-align:left; - line-height:100%; - vertical-align: middle; -} -.point-of-sale #branding img { - height: 32px; - width: 116px; - margin-left:5px; - vertical-align:middle; -} -.point-of-sale #branding .username{ - float:right; - color:#DDD; - font-size:16px; - margin-right:32px; - margin-top:10px; - font-style:italic; -} - -/* b) The right part of the top-bar */ - -.point-of-sale #rightheader { - position: absolute; - left:440px; - right:0; - top:0; - height:100%; -} - -.point-of-sale #rightheader button { - color: #273072; - height:27px; - margin:3px; - margin-right:0px; - border: 1px solid #353A7E; - background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac)); - background: -moz-linear-gradient(#b2b3d7, #7f82ac); - background: -ms-linear-gradient(#b2b3d7, #7f82ac); - background: linear-gradient(#b2b3d7, #7f82ac); -} - -.point-of-sale #rightheader button.neworder-button { - width: 32px; - margin-left:4px; - margin-right:4px; -} - -.point-of-sale div#order-selector { - display: inline; -} -.point-of-sale ol#orders { - display: inline; -} -.point-of-sale li.order-selector-button { - display: inline; -} -.point-of-sale li.selected-order button { - 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; -} - -/* c) The notifications indicator */ - -.point-of-sale .oe_pos_synch-notification{ - float:right; - color: rgba(255,255,255,0.4); - padding: 8px; - line-height:16px; - font-size:16px; - vertical-align:middle; - font-style: italic; - cursor:pointer; -} - -.point-of-sale .oe_pos_synch-notification .oe_status_red{ - display:inline-block; - cursor:pointer; - width:16px; height:16px; - background: url("../img/gtk-no.png") no-repeat ; -} - -.point-of-sale .oe_pos_synch-notification .oe_status_green{ - display:inline-block; - width:16px; height:16px; - background: url("../img/gtk-yes.png") no-repeat; -} - -/* ********* Contains everything below the bar ********* */ - -.point-of-sale #content { - width: 100%; - position: absolute; - top: 35px; - bottom: 0; - background: #F0EEEE; -} - -/* ********* The leftpane contains the order, numpad and paypad ********* */ - -.point-of-sale #leftpane { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - position:absolute; - left:0; - width:440px; - top:0px; - /*bottom:105px;*/ - bottom:0; - border-right: solid 1px #CECBCB; - background-color: white; -} -.point-of-sale #leftpane footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - background: #F0EEEE; - white-space: nowrap; -} - -/* ********* The paypad contains the payment buttons ********* */ - -.point-of-sale #paypad { - padding: 8px 4px 8px 8px; - display: inline-block; - text-align: center; - vertical-align: top; -} -.point-of-sale #paypad button { - height: 54px; - width: 208px; - margin: 0 -3px; - font-weight: bold; - vertical-align: middle; - color: #555555; - border-top: 1px solid #efefef; - font-size: 14px; -} - -/* ********* The Numpad ********* */ - -.point-of-sale #numpad { - padding: 8px 8px 8px 4px; - display: inline-block; - text-align: center; -} -.point-of-sale #numpad button { - height: 54px; - width: 54px; - margin: 0 -3px; - font-weight: bold; - vertical-align: middle; - color: #555555; - border-top: 1px solid #efefef; -} -.point-of-sale #numpad .selected-mode { - color: white; - background: #7f82ac; - background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac)); - background: -moz-linear-gradient(#9d9fc5, #7f82ac); - background: -ms-linear-gradient(#9d9fc5, #7f82ac); - background: linear-gradient(#9d9fc5, #7f82ac); -} -.point-of-sale .input-button { - font-size: 24px; -} -.point-of-sale .mode-button, .point-of-sale #numpad-delete, .point-of-sale #numpad-minus { - font-size: 14px; -} - -/* ********* The right pane contains the screens and headers ********* */ - -.point-of-sale #rightpane { - position: absolute; - top: 0; - /*bottom: 105px;*/ - bottom:0; - left: 440px; - right: 0; - vertical-align: top; -} - -.point-of-sale #rightpane header { - padding: 0; - height: 32px; - border-bottom: 1px solid #cecbcb; - background: #d3d3d3; - background: -moz-linear-gradient(white, #d3d3d3); - background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3)); -} - -/* ********* The product list ********* */ - -.point-of-sale .product-list { - padding:10px; -} - -.point-of-sale .product-list-scroller{ - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - width:100%; - height:100%; - overflow: hidden; -} -.point-of-sale .product-list-container { - position:absolute; - top:0px; - bottom:0px; - left:0px; - right:0px; - background: #eeedff; -} - -/* a) the product list navigation bar */ - -.point-of-sale .breadcrumb li { - float: left; - line-height: 32px; - height: 32px; -} -.point-of-sale .breadcrumb li:last-child { - padding-right: 3px; - border-right: 1px solid #c5c5c5; -} -.point-of-sale .breadcrumb a { - display: inline-block; - padding: 0 9px; - vertical-align: top; - text-shadow: #f7f7f7 0 1px 1px; - color: #555555; - font-weight: bold; -} -.point-of-sale .bc-arrow { - height: 33px; -} -.point-of-sale .homeimg { - width: 19px; - height: 19px; - margin: 6px 0; -} - -/* b) the search box */ - -.point-of-sale .searchbox { - position: absolute; - right: 2px; -} -.point-of-sale .searchbox input { - width: 130px; - border-radius: 11px; - border: 1px solid #cecbcb; - padding: 3px 19px; - margin: 6px; - background: url("../img/search.png") no-repeat 5px; - background-color: white; -} -.point-of-sale .search-clear { - position: absolute; - top: 11px; - right: 11px; - cursor: pointer; - display: none; -} - -/* c) the categories list */ - -.point-of-sale #categories { - /*background:#f0f0f0;*/ - position: relative; - background-image: url('../img/bg.png'); - border-bottom: 1px solid #cecbcb; -} -.point-of-sale #categories .white-gradient{ - position: absolute; - top:50%; - left:0; - right:0; - bottom:0; - background: -webkit-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -moz-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: -ms-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); - background: linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0)); -} -.point-of-sale #categories h4 { - display: inline-block; - margin: 9px 5px; -} - -.point-of-sale .category-list{ - padding:10px; -} -/* d) the category button */ - -.point-of-sale .category-button { - position: relative; - vertical-align: top; - display: inline-block; - font-size: 11px; - margin: 5px; - width: 120px; - height:120px; - 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); - cursor: pointer; -} - -.point-of-sale .category-simple-button{ - position: relative; - display: inline-block; - font-size: 14px; - margin-right:10px; - padding:5px; - - cursor: pointer; - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); -} - -.point-of-sale .category-button .category-img { - position: relative; - width: 120px; - height: 100px; - text-align: center; - cursor: pointer; -} - -.point-of-sale .category-button .category-img img { - max-height: 100px; - max-width: 120px; -} - -.point-of-sale .category-button .category-name { - position: absolute; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - top:auto; - left: 2px; - right: 2px; - bottom: 2px; - background: #8a89ba; /*-webkit-linear-gradient(-90deg,rgba(138,137,186,0),rgba(138,137,186,1), rgba(138,137,186,1));*/ - padding: 3px; - /*color:#8a89ba;*/ - color: #FFF; - cursor: pointer; - border-radius: 3px; -} - -/* e) the product */ - -.point-of-sale .product { - position:relative; - vertical-align: top; - display: inline-block; - line-height: 100px; - font-size: 11px; - margin: 5px; - width: 120px; - height:120px; - background:#fff; - border: 1px solid #fff; - border-radius: 3px; - -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 { - position: relative; - width: 120px; - height: 100px; - background: white; - text-align: center; -} - -.point-of-sale .product .product-img img { - max-height: 100px; - max-width: 120px; -} - -.point-of-sale .product .price-tag { - position: absolute; - top: 2px; - right: 2px; - vertical-align: top; - color: white; - line-height: 14px; - background: #7f82ac; - padding: 2px 5px; - border-radius: 3px; -} - -.point-of-sale .product .product-name { - position: absolute; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - 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)); - background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - /*background:#FFF;*/ - padding: 3px; - padding-top:15px; -} - - -/* ********* The Screens ********* */ - -.point-of-sale .screen { - position:absolute; - text-align: center; - top:0px; - bottom:0px; - width:100%; -} -.point-of-sale .screen header h2 { - margin-top: 0px; - padding-top: 7px; -} -.point-of-sale .screen p{ - font-size: 18px; -} - -/* a) Layout for the Product Screen */ - -.point-of-sale .screen .layout-table { - border:none; - width:100%; - height:100%; -} - -.point-of-sale .screen .header-row { - border:none; - width:100%; - height:0px; -} - -.point-of-sale .screen .header-cell{ - border:none; - width:100%; - height:0px; -} -.point-of-sale .screen .content-row { - width:100%; - height:100%; -} -.point-of-sale .screen .content-cell{ - width:100%; -} -.point-of-sale .screen .content-cell .content-container{ - height:100%; - position:relative; -} - -/* b) The payment screen */ - -.point-of-sale .pos-step-container { - display: inline-block; - font-size: 1.5em; -} -.point-of-sale .greyed-out{ - color: #AAA; -} -.point-of-sale .pos-step-container input{ - font-size: 1em; -} - -.point-of-sale .pos-payment-container { - text-align: left; - min-width: 500px; -} -.point-of-sale .pos-payment-container .left-block{ - display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:left; -} -.point-of-sale .pos-payment-container .header{ - margin-top: 50px; - margin-bottom:20px; - font-weight: bold; -} -.point-of-sale .pos-payment-container .infoline{ - margin-top:5px; - margin-bottom:5px; -} -.point-of-sale .pos-payment-container .right-block{ - display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:right; -} -.point-of-sale .pos-payment-container table { - width: 100%; - margin-bottom: 20px; -} -.point-of-sale .pos-payment-container td { - vertical-align: middle; -} -.point-of-sale .pos-payment-container .paymentline-type { - font-size: 1em; - font-weight: bold; - margin-right:10px; -} - -/* c) The receipt screen */ - -.point-of-sale .pos-receipt-container { - font-size: 0.75em; -} - -.point-of-sale .pos-sale-ticket { - text-align: left; - width: 300px; - background-color: white; - margin: 20px; - padding: 15px; - padding-bottom:30px; - display: inline-block; - font-family: "Inconsolata"; - -webkit-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); -} -.point-of-sale .pos-sale-ticket .emph{ - font-size: 20px; - margin:5px; -} -.point-of-sale .pos-sale-ticket table { - width: 100%; - border: 0; -} -.point-of-sale .pos-sale-ticket table td { - border: 0; -} - -@media print { - .point-of-sale #topheader, .point-of-sale #leftpane { - display: none !important; - } - .point-of-sale #content { - top: 0px !important; - } - .point-of-sale #rightpane { - left: 0px !important; - background-color: white; - } - #receipt-screen header { - display: none !important; - } - #receipt-screen { - text-align: left; - } - .pos-sale-ticket { - margin: 0; - } -} - -/* d) The Scale screen */ - -.point-of-sale .scale-screen .display{ - position:relative; - width:600px; - height:190px; - margin-top: 100px; - margin-left: auto; - margin-right: auto; -} - -.point-of-sale .scale-screen .product-picture { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - display: float; - float: right; - margin: 5px; - width: 180px; - height:180px; - line-height:180px; - cursor:pointer; - - 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); -} - -.point-of-sale .scale-screen .product-picture img{ - vertical-align: middle; - cursor:pointer; -} - -.point-of-sale .scale-screen .product-picture .product-price{ - position: absolute; - top:8px; - right:8px; - width:auto; - height:auto; - line-height:1; - color:white; - background: #7f82ac; - padding: 2px 5px; - border-radius: 3px; - cursor:pointer; -} - -.point-of-sale .scale-screen .product-name { - position: absolute; - left:0; - top:20px; - height:50px; - font-size:40px; - line-height:50px; - text-align:right; - right:225px; -} -.point-of-sale .scale-screen .weight{ - position: absolute; - left:0; - height:90px; - bottom:15px; - right:220px; - padding:5px; -} -.point-of-sale .scale-screen .weight p{ - display: inline-block; - text-align:right; - line-height: 90px; - font-size: 80px; - width:100%; - height:100%; - margin:0; - font-family: "Inconsolata"; -} - -/* e) The Welcome Screen */ -.point-of-sale .goodbye-message{ - position: absolute; - left:50%; - top:30%; - width:500px; - height:400px; - margin-left: -250px; - margin-top: -200px; - padding:10px; - padding-top:20px; - text-align:center; - font-size:20px; - font-weight:bold; - background-color: #F0EEEE; - border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - z-index:1150; -} - -/* ********* The OrderWidget ********* */ - -.point-of-sale .order-container{ - position: absolute; - top: 0px; - bottom: 232px; - width:100%; - background: #F0EEEE; -} - -.point-of-sale .order-scroller{ - width:100%; - height:100%; - overflow:hidden; -} - -.point-of-sale .order{ - background: #F00; - background: -webkit-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -moz-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: -ms-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - background: linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1)); - padding-bottom:15px; - padding-top:15px; - margin-left:16px; - margin-right:16px; - margin-top:16px; - margin-bottom:16px; - font-size:16px; - -webkit-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - box-shadow: 0px 5px 16px rgba(0,0,0, 0.3); - -} - -.point-of-sale .order .empty{ - text-align:center; - margin-top: 15px; - margin-bottom: 5px; - color:#999; - font-weight: normal; -} - -.point-of-sale .order .summary{ - width:100%; - text-align:right; - font-weight: bold; - margin-top:20px; - margin-bottom:10px; -} -.point-of-sale .order .summary .line{ - margin-right:15px; - padding-top:5px; - border-top: solid 2px; - border-color:#777; -} -.point-of-sale .order .summary .line.empty{ - border-color:#BBB; - color:#999; -} - -/* ********* The OrderLineWidget ********* */ - -.point-of-sale .order .orderline{ - width:100%; - margin:0px; - padding-top:3px; - padding-bottom:10px; - padding-left:15px; - padding-right:15px; - cursor: pointer; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: background 250ms ease-in-out; - -moz-transition: background 250ms ease-in-out; - transition: background 250ms ease-in-out; -} - -.point-of-sale .order .orderline.selected{ - background: rgba(140,143,183,0.2); - -webkit-transition: background 250ms ease-in-out; - -moz-transition: background 250ms ease-in-out; - transition: background 250ms ease-in-out; - cursor: default; -} -.point-of-sale .order .orderline .product-name{ - padding:0; - display:inline-block; - font-weight: bold; - width:80%; - overflow:hidden; -} -.point-of-sale .order .orderline .price{ - padding:0; - font-weight: bold; - float:right; -} -.point-of-sale .order .orderline .info-list{ - color: #888; - margin-left:10px; -} -.point-of-sale .order .orderline .info-list em{ - color: #777; - font-weight: bold; - font-style:normal; -} - -/* ********* The ActionBarWidget ********* */ - -.point-of-sale .pos-actionbar{ - position:absolute; - left: 0; - bottom: 0px; - height: 105px; - width: 100%; - margin: 0; - background: #f5f5f5; /*#ebebeb;*/ - border-top: solid 1px #afafb6; - z-index:900; -} - -.point-of-sale .pos-actionbar ul{ - list-style: none; -} - -.point-of-sale .pos-actionbar-left-pane{ - height: 100%; - width: 434px; - margin: 0px; - padding-left:3px; - padding-right:3px; - border-right: solid 1px #dfdfdf; - float: left; -} - -.point-of-sale .pos-actionbar-button-list{ - height: 100%; - margin: 0px; - padding-left:3px; - padding-right:3px; - overflow:hidden; -} - -.point-of-sale .pos-actionbar .button{ - width: 90px; - height: 90px; - text-align:center; - margin:3px; - margin-top:6px; - float:left; - - font-size: 14px; - font-weight: bold; - - cursor: pointer; - - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.1); -} -.point-of-sale .pos-actionbar .button .label{ - margin-top: 37px; -} -.point-of-sale .pos-actionbar .button .icon{ - margin-top: 10px; -} - -.point-of-sale .pos-actionbar .button.disabled *{ - opacity: 0.5; -} - -.point-of-sale .pos-actionbar .button.rightalign{ - float:right; -} - -/* ********* The PopupWidgets ********* */ - -.point-of-sale .modal-dialog{ - position: absolute; - left: 0; - top: 0; - width: 100%; - height:100%; - background-color: rgba(0,0,0,0.5); - z-index:1000; -} -.point-of-sale .modal-dialog .popup{ - position: absolute; - left:50%; - top:50%; - width:500px; - height:400px; - margin-left: -250px; - margin-top: -200px; - padding:10px; - padding-top:20px; - text-align:center; - font-size:20px; - font-weight:bold; - background-color: #F0EEEE; - border: 1px solid #E0DDDD; - -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3); - z-index:1200; -} -.point-of-sale .popup .footer{ - position:absolute; - bottom:0; - left:0; - width:100%; - height:60px; - border-top: 1px solid #E0DDDD; -} -.point-of-sale .popup .button{ - float:right; - width: 110px; - height: 40px; - line-height:40px; - text-align:center; - margin:3px; - margin-top:10px; - margin-right:10px; - - font-size: 14px; - font-weight: bold; - - cursor: pointer; - - border: 1px solid #cacaca; - border-radius: 4px; - - background: #e2e2e2; - background: -webkit-linear-gradient(#f0f0f0, #e2e2e2); - background: -moz-linear-gradient(#f0f0f0, #e2e2e2); - background: -ms-linear-gradient(#f0f0f0, #e2e2e2); - background: linear-gradient(#f0f0f0, #e2e2e2); - -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); - box-shadow: 0px 2px 2px rgba(0,0,0, 0.3); -} - -.point-of-sale .popup .button.big-left{ - position:absolute; - top: 120px; - left:40px; - width: 180px; - height: 180px; - line-height:180px; -} - -.point-of-sale .popup .button.big-right{ - position:absolute; - top: 120px; - right:40px; - width: 180px; - height: 180px; - line-height:180px; -} - -/* ********* The ScrollBarWidget ********* */ - -.point-of-sale .scrollbar{ - position:absolute; - top:7px; - right:7px; - width:48px; - bottom:7px; - background: rgba(0,0,0,0.1); - -} -.point-of-sale .scrollbar .button{ - width:100%; - height: 48px; - line-height: 38px; - text-align: center; - font-size:48px; - border-radius: 4px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .button{ - color:white; - background: rgba(0,0,0,0.6); - -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - box-shadow: 0px 1px 4px rgba(0,0,0,0.01); - text-shadow: rgba(255,255,255,0.5) 0px 0px 10px; - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .button.disabled{ - background: rgba(0,0,0,0.3); - color:rgba(255,255,255,0.5); - -webkit-transition: all 250ms ease-in-out; - -moz-transition: all 250ms ease-in-out; - transition: all 250ms ease-in-out; -} -.point-of-sale .scrollbar .down-button{ - position:absolute; - bottom:0px; -} -.point-of-sale .scrollbar .up-button{ - position:absolute; - top:0px; -} -.point-of-sale .scrollbar .scroller{ - position:absolute; - top:33%; - bottom:50%; - width:100%; - background: rgba(0,0,0,0.1); - border-radius: 4px; -} - diff --git a/addons/point_of_sale/static/src/js/TODO.txt b/addons/point_of_sale/static/src/js/TODO.txt deleted file mode 100644 index 1696f0b117d..00000000000 --- a/addons/point_of_sale/static/src/js/TODO.txt +++ /dev/null @@ -1,63 +0,0 @@ -v Affichage Catégories doivent respecter le poids -v Affichage Catégories par poids doivent avoir une hiérarchie plate -v Le Onscreen keyboard ne fonctionne plus -v Scrolling dans la sélection produit et dans - la liste de courses -v Réductions -v Redesign liste de courses -v bugs scans produits par prix par poids -v si pas photo photo par defaut -v WebSQL -v Redesign header, bouton retour aux backend en mode caissière -v bouton exit en mode caissière doit retourner au backend. -v posting orders -v bug ajout ligne payment à zero + curseur dedans + suppression + design plus grand -v si case print via proxy cochée, alors on skip l'ecran receipt -v bouton exit self checkout -v demarrage en mode self-checkout si self-checkout -v produits poid code barre non reconnus -v activer popup client aux écrans self-checkout -v discount pas plus grand que 100 -v numpad dans l'écran payment pour pouvoir entrer le montant -v différence de total. Methode d'arrondis - -- login alternatif -- user preferences : désactiver la balance etc. -- numpad state parfois sans state -- numpad state en mode pesée ?? le cacher ? -- Redesign du receipt -- retirer le receipt screen -- le numpad en mode paymenet marche pas -- l'écran de payement est hyper laid -- l'écran de pesée est hyper laid -x générer les données de printing -x popups d'erreur certainement buggés - -TODO AUG 20 ------------ - -* GREEN STATUS - - make it green ! - -* TRUNK - - Connection status tooltip - v Remove receipt screen - - Finish the receipt JSON generation - - Modifie le widget de liste course - v supprimer l'écran - v bloquer sur l'impression - v L'impression est foireuse - - description_sales - -* CLIENT - v create a new branch - - Self-checkout welcome screen - ~ removal of products for the root category - - Terminal de payement - - Code à barres - - Vidanges - - Design - - Backport - - CSS pour écran résistif - - Test sur matos - diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 68acd4b874e..bd32e040304 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -43,7 +43,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal 'orders': new module.OrderCollection(), //this is the product list as seen by the product list widgets, it will change based on the category filters - 'products': new module.ProductCollection(), 'cashRegisters': null, 'bank_statements': null, @@ -444,11 +443,11 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } if(parsed_code.type === 'price'){ - selectedOrder.addProduct(new module.Product(product), {price:parsed_code.value}); + selectedOrder.addProduct(product, {price:parsed_code.value}); }else if(parsed_code.type === 'weight'){ - selectedOrder.addProduct(new module.Product(product), {quantity:parsed_code.value, merge:false}); + selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false}); }else{ - selectedOrder.addProduct(new module.Product(product)); + selectedOrder.addProduct(product); } return true; }, @@ -461,16 +460,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal model: module.CashRegister, }); - module.Product = Backbone.Model.extend({ - get_image_url: function(){ - return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image_medium', id: this.get('id')}); - }, - }); - - module.ProductCollection = Backbone.Collection.extend({ - model: module.Product, - }); - // An orderline represent one element of the content of a client's shopping cart. // An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. @@ -479,7 +468,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.pos = options.pos; this.order = options.order; this.product = options.product; - this.price = options.product.get('price'); + this.price = options.product.price; this.quantity = 1; this.quantityStr = '1'; this.discount = 0; @@ -492,7 +481,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); this.discount = disc; this.discountStr = '' + disc; - this.trigger('change'); + this.trigger('change',this); }, // returns the discount [0,100]% get_discount: function(){ @@ -522,7 +511,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.quantityStr = '' + this.quantity; } } - this.trigger('change'); + this.trigger('change',this); }, // return the quantity of product get_quantity: function(){ @@ -541,7 +530,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, // return the unit of measure of the product get_unit: function(){ - var unit_id = (this.product.get('uos_id') || this.product.get('uom_id')); + var unit_id = (this.product.uos_id || this.product.uom_id); if(!unit_id){ return undefined; } @@ -558,7 +547,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // selects or deselects this orderline set_selected: function(selected){ this.selected = selected; - this.trigger('change'); + this.trigger('change',this); }, // returns true if this orderline is selected is_selected: function(){ @@ -567,7 +556,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // when we add an new orderline we want to merge it with the last line to see reduce the number of items // in the orderline. This returns true if it makes sense to merge the two can_be_merged_with: function(orderline){ - if( this.get_product().get('id') !== orderline.get_product().get('id')){ //only orderline of the same product can be merged + if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged return false; }else if(this.get_product_type() !== orderline.get_product_type()){ return false; @@ -587,7 +576,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal qty: this.get_quantity(), price_unit: this.get_unit_price(), discount: this.get_discount(), - product_id: this.get_product().get('id'), + product_id: this.get_product().id, }; }, //used to create a json of the ticket, to be sent to the printer @@ -597,19 +586,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal unit_name: this.get_unit().name, price: this.get_unit_price(), discount: this.get_discount(), - product_name: this.get_product().get('name'), + product_name: this.get_product().name, price_display : this.get_display_price(), price_with_tax : this.get_price_with_tax(), price_without_tax: this.get_price_without_tax(), tax: this.get_tax(), - product_description: this.get_product().get('description'), - product_description_sale: this.get_product().get('description_sale'), + product_description: this.get_product().description, + product_description_sale: this.get_product().description_sale, }; }, // changes the base price of the product for this orderline set_unit_price: function(price){ this.price = round_di(parseFloat(price) || 0, 2); - this.trigger('change'); + this.trigger('change',this); }, get_unit_price: function(){ var rounding = this.pos.get('currency').rounding; @@ -637,8 +626,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var product_list = this.pos.get('product_list'); var product = this.get_product(); - var taxes_ids = product.get('taxes_id');; - var taxes = self.pos.get('taxes'); + var taxes_ids = product.taxes_id; + var taxes = self.pos.taxes; var taxtotal = 0; _.each(taxes_ids, function(el) { var tax = _.detect(taxes, function(t) {return t.id === el;}); @@ -749,7 +738,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, addProduct: function(product, options){ options = options || {}; - var attr = product.toJSON(); + var attr = JSON.parse(JSON.stringify(product)); attr.pos = this.pos; attr.order = this; var line = new module.Orderline({}, {pos: this.pos, order: this, product: product}); @@ -951,6 +940,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.selected_orderline = undefined; } }, + deselectLine: function(){ + if(this.selected_orderline){ + this.selected_orderline.set_selected(false); + this.selected_orderline = undefined; + } + }, }); module.OrderCollection = Backbone.Collection.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 88a83fc52e1..965451ee0eb 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -634,7 +634,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; - currentOrder.addPaymentLine(cashregister); + currentOrder.add_payment_line(cashregister); self.pos.push_order(currentOrder) currentOrder.destroy(); self.pos.proxy.transaction_end(); @@ -745,19 +745,23 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why var self = this; - this.product_categories_widget = new module.ProductCategoriesWidget(this,{}); - this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget')); this.product_list_widget = new module.ProductListWidget(this,{ click_product_action: function(product){ - if(product.get('to_weight') && self.pos.iface_electronic_scale){ + if(product.to_weight && self.pos.iface_electronic_scale){ self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product}); }else{ self.pos.get('selectedOrder').addProduct(product); } }, + product_list: this.pos.db.get_product_by_category(0) }); this.product_list_widget.replace($('.placeholder-ProductListWidget')); + + this.product_categories_widget = new module.ProductCategoriesWidget(this,{ + product_list_widget: this.product_list_widget, + }); + this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget')); }, show: function(){ @@ -766,7 +770,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.product_categories_widget.reset_category(); - this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state); + this.pos_widget.order_widget.set_editable(true); if(this.pos_widget.screen_selector.current_mode === 'client'){ this.add_action_button({ @@ -781,13 +785,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa close: function(){ this._super(); - this.pos_widget.order_widget.set_numpad_state(null); - this.pos_widget.payment_screen.set_numpad_state(null); + + this.pos_widget.order_widget.set_editable(false); + if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ this.pos_widget.onscreen_keyboard.hide(); } }, - }); module.ReceiptScreenWidget = module.ScreenWidget.extend({ @@ -812,8 +816,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(); var self = this; - this.pos_widget.set_numpad_visible(false); - var print_button = this.add_action_button({ label: _t('Print'), icon: '/point_of_sale/static/src/img/icons/png48/printer.png', @@ -876,7 +878,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, close: function(){ this._super(); - this.pos_widget.set_numpad_visible(true); } }); @@ -888,7 +889,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(parent,options); this.model = options.model; this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.bindPaymentLineEvents(); + this.bind_payment_line_events(); this.bind_orderline_events(); this.paymentlinewidgets = []; this.focusedLine = null; @@ -899,7 +900,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.hotkey_handler = function(event){ if(event.which === 13){ - self.validateCurrentOrder(); + self.validate(); }else if(event.which === 27){ self.back(); } @@ -912,8 +913,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos.proxy.open_cashbox(); } - this.set_numpad_state(this.pos_widget.numpad.state); - + this.enable_numpad(); + this.add_action_button({ label: _t('Back'), icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', @@ -927,7 +928,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa name: 'validation', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ - self.validateCurrentOrder(); + self.validate(); }, }); @@ -937,7 +938,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa name: 'invoice', icon: '/point_of_sale/static/src/img/icons/png48/invoice.png', click: function(){ - self.validateCurrentOrder({invoice: true}); + self.validate({invoice: true}); }, }); } @@ -953,13 +954,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); } - this.updatePaymentSummary(); + this.update_payment_summary(); this.line_refocus(); }, close: function(){ this._super(); - this.pos_widget.order_widget.set_numpad_state(null); - this.pos_widget.payment_screen.set_numpad_state(null); + this.disable_numpad(); $('body').off('keyup',this.hotkey_handler); }, back: function() { @@ -970,13 +970,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); this.pos_widget.screen_selector.set_current_screen(this.back_screen); }, - validateCurrentOrder: function(options) { + validate: function(options) { var self = this; options = options || {}; var currentOrder = this.pos.get('selectedOrder'); - if(!this.isPaymentPaid()){ + if(!this.is_paid()){ return; } @@ -1018,19 +1018,19 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa $("input").blur(); },250); }, - bindPaymentLineEvents: function() { + bind_payment_line_events: function() { this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); - this.currentPaymentLines.bind('add', this.addPaymentLine, this); + this.currentPaymentLines.bind('add', this.add_payment_line, this); this.currentPaymentLines.bind('remove', this.renderElement, this); - this.currentPaymentLines.bind('all', this.updatePaymentSummary, this); + this.currentPaymentLines.bind('all', this.update_payment_summary, this); }, bind_orderline_events: function() { this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('all', this.updatePaymentSummary, this); + this.currentOrderLines.bind('all', this.update_payment_summary, this); }, change_selected_order: function() { this.currentPaymentLines.unbind(); - this.bindPaymentLineEvents(); + this.bind_payment_line_events(); this.currentOrderLines.unbind(); this.bind_orderline_events(); this.renderElement(); @@ -1045,7 +1045,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.focusedLine.focus(); } }, - addPaymentLine: function(newPaymentLine) { + add_payment_line: function(newPaymentLine) { var self = this; var l = new module.PaymentlineWidget(this, { payment_line: newPaymentLine, @@ -1069,21 +1069,21 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.paymentlinewidgets = []; this.currentPaymentLines.each(_.bind( function(paymentLine) { - this.addPaymentLine(paymentLine); + this.add_payment_line(paymentLine); }, this)); - this.updatePaymentSummary(); + this.update_payment_summary(); }, deleteLine: function(lineWidget) { this.currentPaymentLines.remove([lineWidget.payment_line]); lineWidget.destroy(); }, - isPaymentPaid: function(){ + is_paid: function(){ var currentOrder = this.pos.get('selectedOrder'); return (currentOrder.getTotalTaxIncluded() >= 0.000001 && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); }, - updatePaymentSummary: function() { + update_payment_summary: function() { var currentOrder = this.pos.get('selectedOrder'); var paidTotal = currentOrder.getPaidTotal(); var dueTotal = currentOrder.getTotalTaxIncluded(); @@ -1099,25 +1099,29 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } if(this.pos_widget.action_bar){ - this.pos_widget.action_bar.set_button_disabled('validation', !this.isPaymentPaid()); - this.pos_widget.action_bar.set_button_disabled('invoice', !this.isPaymentPaid()); + this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid()); + this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid()); } }, - set_numpad_state: function(numpadState) { - if (this.numpadState) { - this.numpadState.unbind('set_value', this.set_value); - this.numpadState.unbind('change:mode', this.setNumpadMode); - } - this.numpadState = numpadState; - if (this.numpadState) { - this.numpadState.bind('set_value', this.set_value, this); - this.numpadState.bind('change:mode', this.setNumpadMode, this); - this.numpadState.reset(); - this.setNumpadMode(); - } + enable_numpad: function(){ + this.disable_numpad(); //ensure we don't register the callbacks twice + this.numpad_state = this.pos_widget.numpad.state; + if(this.numpad_state){ + this.numpad_state.reset(); + this.numpad_state.changeMode('payment'); + this.numpad_state.bind('set_value', this.set_value, this); + this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this); + } + }, - setNumpadMode: function() { - this.numpadState.set({mode: 'payment'}); + disable_numpad: function(){ + if(this.numpad_state){ + this.numpad_state.unbind('set_value', this.set_value); + this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment); + } + }, + set_mode_back_to_payment: function() { + this.numpad_state.set({mode: 'payment'}); }, set_value: function(val) { this.currentPaymentLines.last().set_amount(val); diff --git a/addons/point_of_sale/static/src/js/widget_scrollbar.js b/addons/point_of_sale/static/src/js/widget_scrollbar.js deleted file mode 100644 index 001e96d0bb6..00000000000 --- a/addons/point_of_sale/static/src/js/widget_scrollbar.js +++ /dev/null @@ -1,265 +0,0 @@ -/* - * This Widget provides a javascript scrollbar that is suitable to use with resistive - * tactile screens. - * - * Options: - * target_widget : the widget that will be scrolled. - * target_selector : if you don't want to scroll the root element of the the widget, you can provide a - * jquery selector string that will match on the widget's dom element. If there is no widget provided, - * it will match on the document - * step: on each click, the target will be scrolled by it's deplayed size multiplied by this value. - * duration: this is the duration of the scrolling animation - * wheel_step: the target will be scrolled by wheel_step pixels on each mouse scroll. - * track_bottom: the target will be kept on bottom when it's on the bottom and the size has changed - * on_show: this function will be called with the scrollbar as sole argument when the scrollbar is shown - * on_hide: this function will be called with the scrollbar as sole argument when the scrollbar is hidden - */ -function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_sale - - module.ScrollbarWidget = instance.web.Widget.extend({ - template:'ScrollbarWidget', - - init: function(parent,options){ - var self = this; - options = options || {}; - this._super(parent,options); - this.target_widget = options.target_widget; - this.target_selector = options.target_selector; - this.scroll_target = this.target().scrollTop(); - this.scroll_step = options.step || 0.8; - this.scroll_duration = options.duration || 250; - this.wheel_step = options.wheel_step || 80; - this.name = options.name || 'unnamed'; - this.bottom = false; // true if the scroller cannot be scrolled further - this.track_bottom = options.track_bottom || false; - this.on_show = options.on_show || function(){}; - this.on_hide = options.on_hide || function(){}; - - // these handlers are declared once for the object's lifetime so that we can bind and unbind them. - this.resize_handler = function(){ - setTimeout(function(){ - if(self.bottom && self.track_bottom){ - self.set_position(Number.MAX_VALUE); - } - self.update_scroller_dimensions(); - self.update_button_status(); - self.auto_hide(false); - },0); - }; - this.target_mousewheel_handler = function(event,delta){ - self.scroll(delta*self.wheel_step); - } - }, - - renderElement: function(){ - this._super(); - var self = this; - this.$('.up-button').off('click').click(function(){ - self.page_up(); - }); - this.$('.down-button').off('click').click(function(){ - self.page_down(); - }); - this.update_scroller_dimensions(false); - this.update_button_status(); - this.auto_hide(false); - this.$el.bind('mousewheel',function(event,delta){ - self.scroll(delta*self.wheel_step); - return false; - }); - this.$el.bind('click',function(event){ - var vpos = event.pageY - self.$el.offset().top; - var spos = self.scroller_dimensions(); - if(vpos > spos.bar_pos && vpos < spos.pos){ - self.page_up(); - }else if( (vpos < spos.bar_pos + spos.bar_height) && - (vpos > spos.pos + spos.height) ){ - self.page_down(); - } - }); - // FIXME: use the event bus to handle window resize events - $(window).unbind('resize',this.resize_handler); - $(window).bind('resize',this.resize_handler); - - this.target().unbind('mousewheel',this.target_mousewheel_handler); - this.target().bind('mousewheel',this.target_mousewheel_handler); - - // because the rendering is asynchronous we must wait for the next javascript update - // for good dimensions values - setTimeout(function(){ - self.update_scroller_dimensions(false); - self.update_button_status(); - self.auto_hide(false); - },0); - }, - - destroy: function(){ - $(window).unbind('resize',this.resize_handler); - this.target().unbind('mousewheel',this.target_mousewheel_handler); - this._super(); - }, - - // shows the scrollbar. if animated is true, it will do it in an animated fashion - show: function(animated){ //FIXME: animated show and hide don't work ... ? - if(animated){ - this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing'); - }else{ - this.$el.removeClass('oe_hidden').css('width','48px'); - } - this.on_show(this); - }, - - // hides the scrollbar. if animated is true, it will do it in a animated fashion - hide: function(animated){ - var self = this; - if(animated){ - this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');}); - }else{ - this.$el.addClass('oe_hidden').css('width','0px'); - } - this.on_hide(this); - }, - - // returns the scroller position and other information as a dictionnary with the following fields: - // pos: the position in pixels of the top of the scroller starting from the top of the scrollbar - // height: the height of the scroller in pixels - // bar_pos: the position of the top of the scrollbar's inner region, starting from the top - // bar_height: the height of the scrollbar's inner region - scroller_dimensions: function(){ - var target = this.target()[0]; - var scroller_height = target.clientHeight / target.scrollHeight || 0; - var scroller_pos = this.scroll_target / target.scrollHeight || 0; - var button_up_height = this.$('.up-button')[0].offsetHeight || 48; - var button_down_height = this.$('.down-button')[0].offsetHeight || 48; - - var bar_height = this.$el[0].offsetHeight || 96; - var scrollbar_height = bar_height - button_up_height - button_down_height; - - scroller_pos = scroller_pos * scrollbar_height + button_up_height; - scroller_height = scroller_height * scrollbar_height; - - return { pos: Math.round(scroller_pos), - height: Math.round(scroller_height), - bar_pos: button_up_height, - bar_height: scrollbar_height }; - }, - - - //checks if it should show or hide the scrollbar based on the target content and then show or hide it - // if animated is true, then the scrollbar will be shown or hidden with an animation - auto_hide: function(animated){ - var target = this.target()[0]; - if(target.clientHeight && (target.clientHeight === target.scrollHeight)){ - this.hide(animated); - }else{ - this.show(animated); - } - }, - - //returns the pageup/down scrolling distance in pixels - get_scroll_step: function(){ - var target = this.target()[0]; - var step = target.clientHeight * this.scroll_step; - var c = target.scrollHeight / step; - var c = Math.max(1,Math.ceil(c)); - return target.scrollHeight / c; - }, - - //sets the scroller to the correct size and position based on the target scrolling status - //if animated is true, the scroller will move smoothly to its destination - update_scroller_dimensions: function(animated){ - var dim = this.scroller_dimensions(); - var target = this.target()[0]; - if(animated){ - this.$('.scroller').animate({'top':dim.pos+'px', 'height': dim.height+'px'},this.scroll_duration); - }else{ - this.$('.scroller').css({'top':dim.pos+'px', 'height': dim.height+'px'}); - } - if(this.scroll_target + target.clientHeight >= target.scrollHeight){ - this.bottom = true; - }else{ - this.bottom = false; - } - }, - - //disable or enable the up/down buttons according to the scrolled position - update_button_status: function(){ - var target = this.target()[0]; - this.$('.up-button').removeClass('disabled'); - this.$('.down-button').removeClass('disabled'); - if(this.scroll_target === 0){ - this.$('.up-button').addClass('disabled'); - } - if(this.scroll_target + target.clientHeight >= target.scrollHeight){ - this.$('.down-button').addClass('disabled'); - } - }, - - //returns the jquery object of the scrolling target - target: function(){ - if(this.target_widget){ - if(this.target_selector){ - return this.target_widget.$(this.target_selector); - }else{ - return this.target_widget.$el; - } - }else if(this.target_selector){ - return $(this.target_selector); - }else{ - return undefined; - } - }, - - //scroll one page up - page_up: function(){ - return this.set_position(this.scroll_target - this.get_scroll_step(), true); - }, - - //scroll one page down - page_down: function(){ - return this.set_position(this.scroll_target + this.get_scroll_step(), true); - }, - - //scrolls up or down by pixels - scroll: function(pixels){ - return this.set_position(this.scroll_target - pixels, false); - }, - - //scroll to a specific position (in pixels). - //if animated is true, it will do this in an animated fashion with a duration equal to scroll_duration - set_position: function(position,animated){ - var self = this; - var target = this.target()[0]; - var bottom = target.scrollHeight-target.clientHeight; - this.scroll_target = Math.max(0,Math.min(bottom,position)); - if(this.scroll_target === 0){ - this.position = 'top'; - }else if(this.scroll_target === 'bottom'){ - this.position = 'bottom'; - }else{ - this.position = 'center'; - } - if(animated){ - this.target().animate({'scrollTop':this.scroll_target},this.scroll_duration); - this.update_button_status(); - this.update_scroller_dimensions(true); - }else{ - this.target().scrollTop(this.scroll_target); - this.update_scroller_dimensions(false); - this.update_button_status(); - } - return this.scroll_target; - }, - - //returns the current position of the scrollbar - get_position: function(){ - return this.scroll_target; - }, - - //returns true if it cannot be scrolled further down - is_at_bottom: function(){ - return this.bottom; - } - - }); -} diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index de571ff51f5..438dbc4bc00 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -58,6 +58,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa init: function(parent, options) { this._super(parent); this.state = new module.NumpadState(); + window.numpadstate = this.state; + var self = this; + this.state.bind('change:buffer',function(){ + console.log('BUFFER:',self.state.get('buffer')); + }) }, start: function() { this.state.bind('change:mode', this.changedMode, this); @@ -129,61 +134,50 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - module.OrderlineWidget = module.PosBaseWidget.extend({ - template: 'OrderlineWidget', - init: function(parent, options) { - this._super(parent,options); - - this.model = options.model; - this.order = options.order; - - this.model.bind('change', this.refresh, this); - }, - renderElement: function() { - var self = this; - this._super(); - this.$el.click(function(){ - self.order.selectLine(self.model); - self.trigger('order_line_selected'); - }); - if(this.model.is_selected()){ - this.$el.addClass('selected'); - } - }, - refresh: function(){ - this.renderElement(); - this.trigger('order_line_refreshed'); - }, - destroy: function(){ - this.model.unbind('change',this.refresh,this); - this._super(); - }, - }); - module.OrderWidget = module.PosBaseWidget.extend({ template:'OrderWidget', init: function(parent, options) { + var self = this; this._super(parent,options); - this.display_mode = options.display_mode || 'numpad'; // 'maximized' | 'actionbar' | 'numpad' - this.set_numpad_state(options.numpadState); + this.editable = false; this.pos.bind('change:selectedOrder', this.change_selected_order, this); this.bind_orderline_events(); - this.orderlinewidgets = []; + this.line_click_handler = function(event){ + if(!self.editable){ + return; + } + self.pos.get('selectedOrder').selectLine(this.orderline); + self.pos_widget.numpad.state.reset(); + }; }, - set_numpad_state: function(numpadState) { - if (this.numpadState) { - this.numpadState.unbind('set_value', this.set_value); - } - this.numpadState = numpadState; - if (this.numpadState) { - this.numpadState.bind('set_value', this.set_value, this); - this.numpadState.reset(); - } + enable_numpad: function(){ + this.disable_numpad(); //ensure we don't register the callbacks twice + this.numpad_state = this.pos_widget.numpad.state; + if(this.numpad_state){ + this.numpad_state.reset(); + this.numpad_state.bind('set_value', this.set_value, this); + } + + }, + disable_numpad: function(){ + if(this.numpad_state){ + this.numpad_state.unbind('set_value', this.set_value); + this.numpad_state.reset(); + } + }, + set_editable: function(editable){ + this.editable = editable; + if(editable){ + this.enable_numpad(); + }else{ + this.disable_numpad(); + this.pos.get('selectedOrder').deselectLine(); + } }, set_value: function(val) { var order = this.pos.get('selectedOrder'); - if (order.get('orderLines').length !== 0) { - var mode = this.numpadState.get('mode'); + if (this.editable && order.getSelectedLine()) { + var mode = this.numpad_state.get('mode'); if( mode === 'quantity'){ order.getSelectedLine().set_quantity(val); }else if( mode === 'discount'){ @@ -194,81 +188,96 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } }, change_selected_order: function() { - this.currentOrderLines.unbind(); this.bind_orderline_events(); this.renderElement(); }, bind_orderline_events: function() { - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('add', function(){ this.renderElement(true);}, this); - this.currentOrderLines.bind('remove', this.renderElement, this); + var lines = this.pos.get('selectedOrder').get('orderLines'); + lines.unbind(); + lines.bind('add', function(){ + this.numpad_state.reset(); + this.renderElement(true); + },this); + lines.bind('remove', function(line){ + this.remove_orderline(line); + this.numpad_state.reset(); + this.update_summary(); + },this); + lines.bind('change', function(line){ + this.rerender_orderline(line); + this.update_summary(); + },this); }, update_numpad: function() { this.selected_line = this.pos.get('selectedOrder').getSelectedLine(); if (this.numpadState) this.numpadState.reset(); }, - renderElement: function(goto_bottom) { - var self = this; - var scroller = this.$('.order-scroller')[0]; - var scrollbottom = true; - var scrollTop = 0; - if(scroller){ - var overflow_bottom = scroller.scrollHeight - scroller.clientHeight; - scrollTop = scroller.scrollTop; - if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){ - scrollbottom = false; - } - } - this._super(); + render_orderline: function(orderline){ + var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline}); + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + el_node.orderline = orderline; + el_node.addEventListener('click',this.line_click_handler); - // freeing subwidgets - - for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){ - this.orderlinewidgets[i].destroy(); + orderline.node = el_node; + return el_node; + }, + remove_orderline: function(order_line){ + if(this.pos.get('selectedOrder').get('orderLines').length === 0){ + this.renderElement(); + }else{ + order_line.node.parentNode.removeChild(order_line.node); } - this.orderlinewidgets = []; + }, + rerender_orderline: function(order_line){ + var node = order_line.node; + var replacement_line = this.render_orderline(order_line); + node.parentNode.replaceChild(replacement_line,node); + }, + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + renderElement: function(scrollbottom){ + this.pos_widget.numpad.state.reset(); - var $content = this.$('.orderlines'); - this.currentOrderLines.each(_.bind( function(orderLine) { - var line = new module.OrderlineWidget(this, { - model: orderLine, - order: this.pos.get('selectedOrder'), - }); - line.on('order_line_selected', self, self.update_numpad); - line.on('order_line_refreshed', self, self.update_summary); - line.appendTo($content); - self.orderlinewidgets.push(line); - }, this)); - this.update_numpad(); + var order = this.pos.get('selectedOrder'); + var orderlines = order.get('orderLines').models; + + var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + + var list_container = el_node.querySelector('.orderlines'); + for(var i = 0, len = orderlines.length; i < len; i++){ + var orderline = this.render_orderline(orderlines[i]); + list_container.appendChild(orderline); + } + + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + this.el = el_node; this.update_summary(); - scroller = this.$('.order-scroller')[0]; - - if(scroller){ - if(scrollbottom){ - scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight; - }else{ - scroller.scrollTop = scrollTop; - } + if(scrollbottom){ + this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length; } }, update_summary: function(){ var order = this.pos.get('selectedOrder'); var total = order ? order.getTotalTaxIncluded() : 0; var taxes = order ? total - order.getTotalTaxExcluded() : 0; - this.$('.summary .total > .value').html(this.format_currency(total)); - this.$('.summary .total .subentry .value').html(this.format_currency(taxes)); - }, - set_display_mode: function(mode){ - if(this.display_mode !== mode){ - this.display_mode = mode; - this.renderElement(); - } + + this.el.querySelector('.summary .total > .value').innerText = this.format_currency(total); + this.el.querySelector('.summary .total .subentry .value').innerText = this.format_currency(taxes); }, }); - module.PaymentlineWidget = module.PosBaseWidget.extend({ template: 'PaymentlineWidget', init: function(parent, options) { @@ -439,8 +448,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - module.CategoryButton = module.PosBaseWidget.extend({ - }); module.ProductCategoriesWidget = module.PosBaseWidget.extend({ template: 'ProductCategoriesWidget', init: function(parent, options){ @@ -451,7 +458,13 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.category = this.pos.root_category; this.breadcrumb = []; this.subcategories = []; + this.product_list_widget = options.product_list_widget || null; + this.category_cache = {}; this.set_category(); + this.switch_category_handler = function(event){ + self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId']))); + self.renderElement(); + }; }, // changes the category. if undefined, sets to root category @@ -474,46 +487,77 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, get_image_url: function(category){ - return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id}); + return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id; + }, + + render_category: function( category, with_image ){ + if(!this.category_cache[category.id]){ + + if(with_image){ + var image_url = this.get_image_url(category); + var category_html = QWeb.render('CategoryButton',{ + widget: this, + category: category, + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + var img = category_node.querySelector('img'); + img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img); + }else{ + var category_html = QWeb.render('CategorySimpleButton',{ + widget: this, + category: category, + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + } + this.category_cache[category.id] = category_node; + } + return this.category_cache[category.id]; + }, + + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); }, renderElement: function(){ var self = this; - this._super(); + + var el_str = openerp.qweb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; + + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + + this.el = el_node; var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons - _.each(this.subcategories, function(category){ - if(category.image){ + for(var i = 0; i < this.subcategories.length; i++){ + if(this.subcategories[i].image){ hasimages = true; + break; } - }); + } - _.each(this.subcategories, function(category){ - if(hasimages){ - var button = QWeb.render('CategoryButton',{category:category}); - var button = _.str.trim(button); - var button = $(button); - button.find('img').replaceWith(self.pos_widget.image_cache.get_image(self.get_image_url(category))); - }else{ - var button = QWeb.render('CategorySimpleButton',{category:category}); - button = _.str.trim(button); // we remove whitespace between buttons to fix spacing - var button = $(button); - } + var list_container = el_node.querySelector('.category-list'); + for(var i = 0, len = this.subcategories.length; i < len; i++){ + list_container.appendChild(this.render_category(this.subcategories[i],hasimages)); + }; + + var buttons = el_node.querySelectorAll('.js-category-switch'); + for(var i = 0; i < buttons.length; i++){ + buttons[i].addEventListener('click',this.switch_category_handler); + } - button.appendTo(this.$('.category-list')).click(function(event){ - var id = category.id; - var cat = self.pos.db.get_category_by_id(id); - self.set_category(cat); - self.renderElement(); - }); - }); - // breadcrumb click actions - this.$(".oe-pos-categories-list a").click(function(event){ - var id = $(event.target).data("category-id"); - var category = self.pos.db.get_category_by_id(id); - self.set_category(category); - self.renderElement(); - }); this.search_and_categories(); @@ -522,11 +566,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa } }, - set_product_type: function(type){ // 'all' | 'weightable' - this.product_type = type; - this.reset_category(); - }, - // resets the current category to the root category reset_category: function(){ this.set_category(); @@ -536,7 +575,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // empties the content of the search box clear_search: function(){ var products = this.pos.db.get_product_by_category(this.category.id); - this.pos.get('products').reset(products); + this.product_list_widget.set_product_list(products); this.$('.searchbox input').val('').focus(); this.$('.search-clear').fadeOut(); }, @@ -547,9 +586,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // find all products belonging to the current category var products = this.pos.db.get_product_by_category(this.category.id); - self.pos.get('products').reset(products); + this.product_list_widget.set_product_list(products); + /* var searchtimeout = null; // filter the products according to the search string this.$('.searchbox input').keyup(function(event){ @@ -562,17 +602,17 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(query){ if(event.which === 13){ if( self.pos.get('products').size() === 1 ){ - self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0)); + self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0)); // FIXME self.clear_search(); } }else{ var products = self.pos.db.search_product_in_category(self.category.id, query); - self.pos.get('products').reset(products); + this.product_list_widget.set_product_list(products); self.$('.search-clear').fadeIn(); } }else{ var products = self.pos.db.get_product_by_category(self.category.id); - self.pos.get('products').reset(products); + this.product_list_widget.set_product_list(products); self.$('.search-clear').fadeOut(); } },200); @@ -581,11 +621,11 @@ 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.clear_search(); - }); + });*/ }, }); - module.ProductListWidget = module.ScreenWidget.extend({ + module.ProductListWidget = module.PosBaseWidget.extend({ template:'ProductListWidget', init: function(parent, options) { var self = this; @@ -595,38 +635,65 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.weight = options.weight || 0; this.show_scale = options.show_scale || false; this.next_screen = options.next_screen || false; - this.click_product_action = options.click_product_action; - this.pos.get('products').bind('reset', function(){ - self.renderElement(); - }); + this.click_product_handler = function(event){ + var product = self.pos.db.get_product_by_id(this.dataset['productId']); + options.click_product_action(product); + }; + + this.product_list = options.product_list || []; + this.product_cache = {}; }, + set_product_list: function(product_list){ + this.product_list = product_list; + this.renderElement(); + }, + get_product_image_url: function(product){ + return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; + }, + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + + render_product: function(product){ + if(!this.product_cache[product.id]){ + var image_url = this.get_product_image_url(product); + var product_html = QWeb.render('Product',{ + widget: this, + product: product, + }); + var product_node = document.createElement('div'); + product_node.innerHTML = product_html; + product_node = product_node.childNodes[1]; + var img = product_node.querySelector('img'); + img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img); + this.product_cache[product.id] = product_node; + } + return this.product_cache[product.id]; + }, + renderElement: function() { var self = this; - this._super(); - var products = this.pos.get('products').models || []; + // this._super() + var el_str = openerp.qweb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; - _.each(products,function(product,i){ - var $product = $(QWeb.render('Product',{ widget:self, product: products[i] })); - $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url())); - $product.appendTo(self.$('.product-list')); - }); - - this.$el.delegate('.product','touchstart',function TOUCHSTART(){ - console.log('Touchstart',(new Date()).getTime()); - }); - - this.$el.delegate('.product','touchend',function TOUCHEND(){ - console.log('Touchend',(new Date()).getTime()); - }); - - this.$el.delegate('.product','click',function TOUCHCLICK(){ - console.log('ClickStart',(new Date()).getTime()); - self.click_product_action(new module.Product(self.pos.db.get_product_by_id(+$(this).data('product-id')))); - console.log('ClickEnd',(new Date()).getTime()); - }); + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + this.el = el_node; + var list_container = el_node.querySelector('.product-list'); + for(var i = 0, len = this.product_list.length; i < len; i++){ + var product_node = this.render_product(this.product_list[i]); + product_node.addEventListener('click',this.click_product_handler); + list_container.appendChild(product_node); + }; }, }); @@ -1046,13 +1113,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.set_left_action_bar_visible(false); this.numpad.show(); this.paypad.show(); - this.order_widget.set_display_mode('numpad'); }else{ this.numpad.hide(); this.paypad.hide(); - if(this.order_widget.display_mode === 'numpad'){ - this.order_widget.set_display_mode('maximized'); - } } } }, @@ -1062,20 +1125,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(visible){ this.set_numpad_visible(false); this.left_action_bar.show(); - this.order_widget.set_display_mode('actionbar'); }else{ this.left_action_bar.hide(); - if(this.order_widget.display_mode === 'actionbar'){ - this.order_widget.set_display_mode('maximized'); - } } } }, - set_fullscreen: function(){ - if(this.el.webkitRequestFullscreen){ - this.el.webkitRequestFullscreen(); - } - }, //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.) set_leftpane_visible: function(visible){ if(visible !== this.leftpane_visible){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index d8ce116d749..941fdc8c80b 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -137,7 +137,7 @@ -
      4. +
      5. @@ -148,23 +148,24 @@ -
      6. +
      7. +
        + @@ -425,52 +427,36 @@
      8. - + - + - + - +
        - +
      9. - -
        -
        - &atilde; -
        - -
        -
        - -
        - &auml; -
        -
        -
        -
          - +
        • Your shopping cart is empty
        • -
        -
        +
        Total: 0.00 €
        Taxes: 0.00€
        @@ -480,8 +466,6 @@
        -
        -
        @@ -538,32 +522,32 @@
        - -
      10. + +
      11. - + - +
          - +
        • - + - + at - + / - +
        • - +
        • With a - % + % discount
        • @@ -621,7 +605,7 @@ - +
          With a % discount From d4e7b6eb962d38349ac62676c79c0467e9a80d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 4 Dec 2013 18:21:22 +0100 Subject: [PATCH 23/64] [WIP] point_of_sale: more perf improvement, this time on the receipt and payment screen + lots of usability improvements and cleanups bzr revid: fva@openerp.com-20131204172122-mzl4wg4uw51uzdfv --- addons/point_of_sale/__openerp__.py | 2 - .../lib/mousewheel/jquery.mousewheel-3.0.6.js | 84 ---- addons/point_of_sale/static/src/css/pos.css | 102 ++--- addons/point_of_sale/static/src/js/main.js | 2 - addons/point_of_sale/static/src/js/models.js | 49 ++- addons/point_of_sale/static/src/js/screens.js | 358 ++++++++++++------ addons/point_of_sale/static/src/js/widgets.js | 69 +--- addons/point_of_sale/static/src/xml/pos.xml | 91 ++--- 8 files changed, 378 insertions(+), 379 deletions(-) delete mode 100644 addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index d963144ceca..44ceb83b02d 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -89,13 +89,11 @@ Main Features 'installable': True, 'application': True, 'js': [ - 'static/lib/mousewheel/jquery.mousewheel-3.0.6.js', 'static/lib/fastclick.js', 'static/src/js/db.js', 'static/src/js/models.js', 'static/src/js/widget_base.js', 'static/src/js/widget_keyboard.js', - 'static/src/js/widget_scrollbar.js', 'static/src/js/widgets.js', 'static/src/js/devices.js', 'static/src/js/screens.js', diff --git a/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js b/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js deleted file mode 100644 index 38b60951b20..00000000000 --- a/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js +++ /dev/null @@ -1,84 +0,0 @@ -/*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) - * Licensed under the MIT License (LICENSE.txt). - * - * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. - * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. - * Thanks to: Seamus Leahy for adding deltaX and deltaY - * - * Version: 3.0.6 - * - * Requires: 1.2.2+ - */ - -(function($) { - -var types = ['DOMMouseScroll', 'mousewheel']; - -if ($.event.fixHooks) { - for ( var i=types.length; i; ) { - $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; - } -} - -$.event.special.mousewheel = { - setup: function() { - if ( this.addEventListener ) { - for ( var i=types.length; i; ) { - this.addEventListener( types[--i], handler, false ); - } - } else { - this.onmousewheel = handler; - } - }, - - teardown: function() { - if ( this.removeEventListener ) { - for ( var i=types.length; i; ) { - this.removeEventListener( types[--i], handler, false ); - } - } else { - this.onmousewheel = null; - } - } -}; - -$.fn.extend({ - mousewheel: function(fn) { - return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); - }, - - unmousewheel: function(fn) { - return this.unbind("mousewheel", fn); - } -}); - - -function handler(event) { - var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; - event = $.event.fix(orgEvent); - event.type = "mousewheel"; - - // Old school scrollwheel delta - if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } - if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } - - // New school multidimensional scroll (touchpads) deltas - deltaY = delta; - - // Gecko - if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { - deltaY = 0; - deltaX = -1*delta; - } - - // Webkit - if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } - if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } - - // Add event and delta to the front of the arguments - args.unshift(event, delta, deltaX, deltaY); - - return ($.event.dispatch || $.event.handle).apply(this, args); -} - -})(jQuery); diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 9d728f8c7da..18673ec1cc2 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -705,37 +705,60 @@ /* b) The payment screen */ -.pos .pos-step-container { - display: inline-block; - font-size: 1.5em; -} -.pos .greyed-out{ - color: #AAA; -} -.pos .pos-step-container input{ - font-size: 1em; - outline: none; - border: none; - padding: 0px 8px; - padding-top: 8px; - margin-left: 16px; - background: white; - color: #4c4c4c; - box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset; - -webkit-animation: all 250ms linear; -} - -.pos .pos-step-container input:focus{ - color: rgb(130, 124, 255); - box-shadow: 0px 2px rgba(130, 124, 255, 0.3) inset; - -webkit-animation: all 250ms linear; -} .pos .pos-payment-container { + display: inline-block; + font-size: 16px; text-align: left; - min-width: 320px; - margin-top: 40px; + width: 360px; } +.pos .payment-due-total { + text-align: center; + font-weight: bold; + font-size: 48px; + margin: 27px; + text-shadow: 0px 2px rgb(202, 202, 202); +} +.pos .paymentline{ + position: relative; + padding: 8px; + border-box: 3px; + box-sizing: border-box; + border-radius: 3px; +} +.pos .paymentline-input{ + font-size: 20px; + font-family: Lato; + display: block; + width: 100%; + box-sizing: border-box; + outline: none; + border: none; + padding: 6px 8px; + background: white; + color: #484848; + text-align: right; + box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset; +} + +.pos .paymentline-input:focus{ + color: rgb(130, 124, 255); + box-shadow: 0px 2px rgba(219, 219, 219, 0.3) inset; + -webkit-animation: all 250ms linear; +} + +.paymentline-delete { + width: 32px; + height: 32px; + padding: 5px; + text-align: center; + box-sizing: border-box; + position: absolute; + bottom: 10px; + left: 8px; + cursor: pointer; +} + .pos .pos-payment-container .left-block{ display: inline-block; width:49%; @@ -743,14 +766,15 @@ padding:0; text-align:left; } -.pos .pos-payment-container .header{ - margin-top: 0px; - margin-bottom:20px; - font-weight: bold; -} .pos .pos-payment-container .infoline{ margin-top:5px; margin-bottom:5px; + padding: 0px 8px; + opacity: 0.5; +} +.pos .pos-payment-container .infoline.bigger{ + opacity: 1; + font-size: 20px; } .pos .pos-payment-container .right-block{ display: inline-block; @@ -759,17 +783,8 @@ padding:0; text-align:right; } -.pos .pos-payment-container table { - width: 100%; - margin-bottom: 20px; -} -.pos .pos-payment-container td { - vertical-align: middle; -} -.pos .pos-payment-container .paymentline-type { - font-size: 1em; - font-weight: bold; - margin-right:10px; +.pos .paymentline.selected{ + background: rgb(220,220,220); } /* c) The receipt screen */ @@ -784,6 +799,7 @@ background-color: white; margin: 20px; padding: 15px; + font-size: 14px; padding-bottom:30px; display: inline-block; font-family: "Inconsolata"; diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 55f0bbd1f61..90fa3567cda 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -13,8 +13,6 @@ openerp.point_of_sale = function(instance) { openerp_pos_keyboard(instance,module); // import pos_keyboard_widget.js - openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js - openerp_pos_screens(instance,module); // import pos_screens.js openerp_pos_devices(instance,module); // import pos_devices.js diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index bd32e040304..20fcc5e553f 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -669,25 +669,30 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal model: module.Orderline, }); - // Every PaymentLine contains a cashregister and an amount of money. + // Every Paymentline contains a cashregister and an amount of money. module.Paymentline = Backbone.Model.extend({ initialize: function(attributes, options) { this.amount = 0; this.cashregister = options.cashRegister; + this.name = this.cashregister.get('journal_id')[1]; + this.selected = false; }, //sets the amount of money on this payment line set_amount: function(value){ - this.amount = parseFloat(value) || 0; - this.trigger('change'); + this.amount = round_di(parseFloat(value) || 0, 2); + this.trigger('change:amount',this); }, // returns the amount of money on this paymentline get_amount: function(){ return this.amount; }, - // returns the associated cashRegister - get_cashregister: function(){ - return this.cashregister; + set_selected: function(selected){ + if(this.selected !== selected){ + this.selected = selected; + this.trigger('change:selected',this); + } }, + // returns the associated cashRegister //exports as JSON for server communication export_as_JSON: function(){ return { @@ -713,7 +718,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // An order more or less represents the content of a client's shopping cart (the OrderLines) - // plus the associated payment information (the PaymentLines) + // plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. module.Order = Backbone.Model.extend({ @@ -727,8 +732,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal name: "Order " + this.uid, client: null, }); - this.pos = attributes.pos; - this.selected_orderline = undefined; + this.pos = attributes.pos; + this.selected_orderline = undefined; + this.selected_paymentline = undefined; this.screen_data = {}; // see ScreenSelector this.receipt_type = 'receipt'; // 'receipt' || 'invoice' return this; @@ -765,13 +771,21 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal getLastOrderline: function(){ return this.get('orderLines').at(this.get('orderLines').length -1); }, - addPaymentLine: function(cashRegister) { + addPaymentline: function(cashRegister) { var paymentLines = this.get('paymentLines'); var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister}); if(cashRegister.get('journal').type !== 'cash'){ - newPaymentline.set_amount( this.getDueLeft() ); + newPaymentline.set_amount( Math.max(this.getDueLeft(),0) ); } paymentLines.add(newPaymentline); + this.selectPaymentline(newPaymentline); + + }, + removePaymentline: function(line){ + if(this.selected_paymentline === line){ + this.selectPaymentline(undefined); + } + this.get('paymentLines').remove(line); }, getName: function() { return this.get('name'); @@ -946,6 +960,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.selected_orderline = undefined; } }, + selectPaymentline: function(line){ + console.log("SELECT_PAYMENTLINE",line); + if(line !== this.selected_paymentline){ + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(false); + } + this.selected_paymentline = line; + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(true); + } + this.trigger('change:selected_paymentline',this.selected_paymentline); + } + }, }); module.OrderCollection = Backbone.Collection.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 965451ee0eb..c182d9d06b2 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -157,6 +157,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned barcode_product_error_popup: 'error-product', //if defined, this popup will be loaded when there's an error in the popup + hotkeys_handlers: {}, + // what happens when a product is scanned : // 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. @@ -634,7 +636,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; - currentOrder.add_payment_line(cashregister); + currentOrder.addPaymentline(cashregister); self.pos.push_order(currentOrder) currentOrder.destroy(); self.pos.proxy.transaction_end(); @@ -802,15 +804,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa init: function(parent, options) { this._super(parent,options); - this.model = options.model; this.user = this.pos.get('user'); this.company = this.pos.get('company'); this.shop_obj = this.pos.get('shop'); }, renderElement: function() { this._super(); - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.change_selected_order(); }, show: function(){ this._super(); @@ -828,6 +827,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa click: function() { self.finishOrder(); }, }); + this.refresh(); this.print(); // THIS IS THE HACK OF THE CENTURY @@ -859,22 +859,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa finishOrder: function() { this.pos.get('selectedOrder').destroy(); }, - change_selected_order: function() { - if (this.currentOrderLines) - this.currentOrderLines.unbind(); - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('add', this.refresh, this); - this.currentOrderLines.bind('change', this.refresh, this); - this.currentOrderLines.bind('remove', this.refresh, this); - if (this.currentPaymentLines) - this.currentPaymentLines.unbind(); - this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); - this.currentPaymentLines.bind('all', this.refresh, this); - this.refresh(); - }, refresh: function() { - this.currentOrder = this.pos.get('selectedOrder'); - $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this})); + var order = this.pos.get('selectedOrder'); + $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{ + widget:this, + order: order, + orderlines: order.get('orderLines').models, + paymentlines: order.get('paymentLines').models, + })); }, close: function(){ this._super(); @@ -886,17 +878,68 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa back_screen: 'products', next_screen: 'receipt', init: function(parent, options) { + var self = this; this._super(parent,options); - this.model = options.model; - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.bind_payment_line_events(); - this.bind_orderline_events(); - this.paymentlinewidgets = []; - this.focusedLine = null; + + this.pos.bind('change:selectedOrder',function(){ + this.bind_events(); + this.renderElement(); + },this); + + this.bind_events(); + + this.line_delete_handler = function(event){ + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + self.pos.get('selectedOrder').removePaymentline(node.line) + } + event.stopPropagation(); + }; + + this.line_change_handler = function(event){ + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + node.line.set_amount(this.value); + } + + }; + + this.line_click_handler = function(event){ + console.log('click'); + var node = this; + while(node && !node.classList.contains('paymentline')){ + node = node.parentNode; + } + if(node){ + self.pos.get('selectedOrder').selectPaymentline(node.line); + } + }; + }, show: function(){ this._super(); var self = this; + + this.enable_numpad(); + this.update_payment_summary(); + this.focus_selected_line(); + window.removeall = function(){ + self.remove_empty_lines(); + }; + window.listlines = function(){ + var lines = self.pos.get('selectedOrder').get('paymentLines').models; + for(var i = 0; i < lines.length; i++){ + console.log('LINE:',lines[i]); + } + }; + + /* this.hotkey_handler = function(event){ if(event.which === 13){ @@ -907,13 +950,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }; $('body').on('keyup',this.hotkey_handler); + + */ - - if( this.pos.iface_cashdrawer && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ return pl.cashregister.get('journal').type === 'cash'; })){ + if( this.pos.iface_cashdrawer + && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ + return pl.cashregister.get('journal').type === 'cash'; + })){ this.pos.proxy.open_cashbox(); } - this.enable_numpad(); this.add_action_button({ label: _t('Back'), @@ -928,17 +974,17 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa name: 'validation', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ - self.validate(); + self.validate_order(); }, }); - if(this.pos.iface_invoicing){ + if( this.pos.iface_invoicing ){ this.add_action_button({ label: 'Invoice', name: 'invoice', icon: '/point_of_sale/static/src/img/icons/png48/invoice.png', click: function(){ - self.validate({invoice: true}); + self.validate_order({invoice: true}); }, }); } @@ -954,23 +1000,163 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); } - this.update_payment_summary(); - this.line_refocus(); }, close: function(){ this._super(); this.disable_numpad(); - $('body').off('keyup',this.hotkey_handler); + //$('body').off('keyup',this.hotkey_handler); + }, + remove_empty_lines: function(){ + var order = this.pos.get('selectedOrder'); + var lines = order.get('paymentLines').models.slice(0); + for(var i = 0; i < lines.length; i++){ + var line = lines[i]; + if(line.get_amount() === 0){ + order.removePaymentline(line); + } + } }, back: function() { - _.each(this.paymentlinewidgets, function(widget){ - if( widget.payment_line.get_amount() === 0 ){ - widget.payment_line.destroy(); - } - }); + this.remove_empty_lines(); this.pos_widget.screen_selector.set_current_screen(this.back_screen); }, - validate: function(options) { + bind_events: function() { + if(this.old_order){ + this.old_order.unbind(null,null,this); + } + var order = this.pos.get('selectedOrder'); + order.bind('change:selected_paymentline',this.focus_selected_line,this); + + this.old_order = order; + + if(this.old_paymentlines){ + this.old_paymentlines.unbind(null,null,this); + } + var paymentlines = order.get('paymentLines'); + paymentlines.bind('add', this.add_paymentline, this); + paymentlines.bind('change:selected', this.rerender_paymentline, this); + paymentlines.bind('change:amount', function(line){ + if(!line.selected && line.node){ + line.node.value = line.amount.toFixed(2); + } + this.update_payment_summary(); + },this); + paymentlines.bind('remove', this.remove_paymentline, this); + paymentlines.bind('all', this.update_payment_summary, this); + + this.old_paymentlines = paymentlines; + + if(this.old_orderlines){ + this.old_orderlines.unbind(null,null,this); + } + var orderlines = order.get('orderLines'); + orderlines.bind('all', this.update_payment_summary, this); + + this.old_orderlines = orderlines; + }, + focus_selected_line: function(){ + var line = this.pos.get('selectedOrder').selected_paymentline; + if(line){ + debugger; + if(!line.node){ + debugger; + } + var input = line.node.querySelector('input'); + if(!input){ + return; + } + var value = input.value; + input.focus(); + + if(this.numpad_state){ + this.numpad_state.reset(); + } + + if(Number(value) === 0){ + input.value = ''; + }else{ + input.value = value; + input.select(); + } + } + }, + add_paymentline: function(line) { + var list_container = this.el.querySelector('.payment-lines'); + list_container.appendChild(this.render_paymentline(line)); + + if(this.numpad_state){ + this.numpad_state.reset(); + } + }, + render_paymentline: function(line){ + var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line}); + el_html = _.str.trim(el_html); + + var el_node = document.createElement('tbody'); + el_node.innerHTML = el_html; + el_node = el_node.childNodes[0]; + el_node.line = line; + el_node.querySelector('.paymentline-delete') + .addEventListener('click', this.line_delete_handler); + el_node.addEventListener('click', this.line_click_handler); + el_node.querySelector('input') + .addEventListener('keyup', this.line_change_handler); + + line.node = el_node; + + return el_node; + }, + rerender_paymentline: function(line){ + var old_node = line.node; + var new_node = this.render_paymentline(line); + + old_node.parentNode.replaceChild(new_node,old_node); + console.log('RERENDER',line); + }, + remove_paymentline: function(line){ + console.log('Removing line from DOM:',line); + line.node.parentNode.removeChild(line.node); + line.node = undefined; + }, + renderElement: function(){ + this._super(); + + var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models; + var list_container = this.el.querySelector('.payment-lines'); + + for(var i = 0; i < paymentlines.length; i++){ + list_container.appendChild(this.render_paymentline(paymentlines[i])); + } + + //this.update_payment_summary(); + }, + update_payment_summary: function() { + var currentOrder = this.pos.get('selectedOrder'); + var paidTotal = currentOrder.getPaidTotal(); + var dueTotal = currentOrder.getTotalTaxIncluded(); + var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; + var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; + + this.$('.payment-due-total').html(this.format_currency(dueTotal)); + this.$('.payment-paid-total').html(this.format_currency(paidTotal)); + this.$('.payment-remaining').html(this.format_currency(remaining)); + this.$('.payment-change').html(this.format_currency(change)); + if(currentOrder.selected_orderline === undefined){ + remaining = 1; // What is this ? + } + + if(this.pos_widget.action_bar){ + this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid()); + this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid()); + } + }, + is_paid: function(){ + var currentOrder = this.pos.get('selectedOrder'); + return (currentOrder.getTotalTaxIncluded() >= 0.000001 + && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); + + }, + validate_order: function(options) { var self = this; options = options || {}; @@ -1012,97 +1198,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.screen_selector.set_current_screen(this.next_screen); } } + // hide onscreen (iOS) keyboard setTimeout(function(){ document.activeElement.blur(); $("input").blur(); },250); }, - bind_payment_line_events: function() { - this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines'); - this.currentPaymentLines.bind('add', this.add_payment_line, this); - this.currentPaymentLines.bind('remove', this.renderElement, this); - this.currentPaymentLines.bind('all', this.update_payment_summary, this); - }, - bind_orderline_events: function() { - this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines'); - this.currentOrderLines.bind('all', this.update_payment_summary, this); - }, - change_selected_order: function() { - this.currentPaymentLines.unbind(); - this.bind_payment_line_events(); - this.currentOrderLines.unbind(); - this.bind_orderline_events(); - this.renderElement(); - }, - line_refocus: function(lineWidget){ - if(lineWidget){ - if(this.focusedLine !== lineWidget){ - this.focusedLine = lineWidget; - } - } - if(this.focusedLine){ - this.focusedLine.focus(); - } - }, - add_payment_line: function(newPaymentLine) { - var self = this; - var l = new module.PaymentlineWidget(this, { - payment_line: newPaymentLine, - }); - l.on('delete_payment_line', self, function(r) { - self.deleteLine(r); - }); - l.appendTo(this.$('.paymentlines')); - this.paymentlinewidgets.push(l); - if(this.numpadState){ - this.numpadState.resetValue(); - } - this.line_refocus(l); - }, - renderElement: function() { - this._super(); - this.$('.paymentlines').empty(); - for(var i = 0, len = this.paymentlinewidgets.length; i < len; i++){ - this.paymentlinewidgets[i].destroy(); - } - this.paymentlinewidgets = []; - - this.currentPaymentLines.each(_.bind( function(paymentLine) { - this.add_payment_line(paymentLine); - }, this)); - this.update_payment_summary(); - }, - deleteLine: function(lineWidget) { - this.currentPaymentLines.remove([lineWidget.payment_line]); - lineWidget.destroy(); - }, - is_paid: function(){ - var currentOrder = this.pos.get('selectedOrder'); - return (currentOrder.getTotalTaxIncluded() >= 0.000001 - && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); - - }, - update_payment_summary: function() { - var currentOrder = this.pos.get('selectedOrder'); - var paidTotal = currentOrder.getPaidTotal(); - var dueTotal = currentOrder.getTotalTaxIncluded(); - var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; - var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; - - this.$('.payment-due-total').html(this.format_currency(dueTotal)); - this.$('.payment-paid-total').html(this.format_currency(paidTotal)); - this.$('.payment-remaining').html(this.format_currency(remaining)); - this.$('.payment-change').html(this.format_currency(change)); - if(currentOrder.selected_orderline === undefined){ - remaining = 1; // What is this ? - } - - if(this.pos_widget.action_bar){ - this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid()); - this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid()); - } - }, enable_numpad: function(){ this.disable_numpad(); //ensure we don't register the callbacks twice this.numpad_state = this.pos_widget.numpad.state; @@ -1124,7 +1226,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.numpad_state.set({mode: 'payment'}); }, set_value: function(val) { - this.currentPaymentLines.last().set_amount(val); + var selected_line =this.pos.get('selectedOrder').selected_paymentline; + if(selected_line){ + selected_line.set_amount(val); + selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2); + } }, }); } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 438dbc4bc00..90787f9f610 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -60,9 +60,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.state = new module.NumpadState(); window.numpadstate = this.state; var self = this; - this.state.bind('change:buffer',function(){ - console.log('BUFFER:',self.state.get('buffer')); - }) }, start: function() { this.state.bind('change:mode', this.changedMode, this); @@ -128,7 +125,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa console.warn('TODO should not get there...?'); return; } - self.pos.get('selectedOrder').addPaymentLine(self.cashRegister); + self.pos.get('selectedOrder').addPaymentline(self.cashRegister); self.pos_widget.screen_selector.set_current_screen('payment'); }); }, @@ -208,11 +205,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.update_summary(); },this); }, - update_numpad: function() { - this.selected_line = this.pos.get('selectedOrder').getSelectedLine(); - if (this.numpadState) - this.numpadState.reset(); - }, render_orderline: function(orderline){ var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline}); var el_node = document.createElement('div'); @@ -236,6 +228,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa var replacement_line = this.render_orderline(order_line); node.parentNode.replaceChild(replacement_line,node); }, + // overriding the openerp framework replace method for performance reasons replace: function($target){ this.renderElement(); var target = $target[0]; @@ -278,58 +271,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - module.PaymentlineWidget = module.PosBaseWidget.extend({ - template: 'PaymentlineWidget', - init: function(parent, options) { - this._super(parent,options); - this.payment_line = options.payment_line; - this.payment_line.bind('change', this.changedAmount, this); - }, - changeAmount: function(event) { - var newAmount = event.currentTarget.value; - var amount = parseFloat(newAmount); - if(!isNaN(amount)){ - this.amount = amount; - this.payment_line.set_amount(amount); - } - }, - checkAmount: function(e){ - if (e.which !== 0 && e.charCode !== 0) { - if(isNaN(String.fromCharCode(e.charCode))){ - return (String.fromCharCode(e.charCode) === "." && e.currentTarget.value.toString().split(".").length < 2)?true:false; - } - } - return true - }, - changedAmount: function() { - if (this.amount !== this.payment_line.get_amount()){ - this.renderElement(); - } - }, - renderElement: function() { - var self = this; - this.name = this.payment_line.get_cashregister().get('journal_id')[1]; - this._super(); - this.$('input').keypress(_.bind(this.checkAmount, this)) - .keyup(function(event){ - self.changeAmount(event); - }); - this.$('.delete-payment-line').click(function() { - self.trigger('delete_payment_line', self); - }); - }, - focus: function(){ - var val = this.$('input')[0].value; - this.$('input')[0].focus(); - if(Number(val) === 0){ - this.$('input')[0].value = ''; - }else{ - this.$('input')[0].value = val; - this.$('input')[0].select(); - } - }, - }); - module.OrderButtonWidget = module.PosBaseWidget.extend({ template:'OrderButtonWidget', init: function(parent, options) { @@ -918,12 +859,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.pos.delete_current_order(); }); - $('body').on('keyup',function(event){ - if(event.which === 13){ - self.set_fullscreen(); - } - }); - //when a new order is created, add an order button widget self.pos.get('orders').bind('add', function(new_order){ var new_order_button = new module.OrderButtonWidget(null, { diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 941fdc8c80b..5591acf0ac7 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -246,41 +246,32 @@

          Payment

          -
          -
          -
          -
          - - Total: +
          +
          +
          +
          +
          + + Paid: - +
          - -
          -
          - +
          @@ -556,14 +547,26 @@ - - + +
          +
          + +
          + + + + +
          +
          + + + - + - - + + @@ -590,7 +593,7 @@
          + Date.CultureInfo.formatPatterns.longTime)"/>


          Phone:
          @@ -603,7 +606,7 @@ - + @@ -623,33 +626,33 @@
          Subtotal: - +
          Tax: - +
          Discount: - +
          Total: - +

          - +
          - + - +

          Change: - +
          From 019938f2c803257409b4477b218b1cf6661bff71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 5 Dec 2013 18:53:25 +0100 Subject: [PATCH 24/64] [WIP] point_of_sale: finished css optimisations bzr revid: fva@openerp.com-20131205175325-8v81lfj69020x0qe --- addons/point_of_sale/controllers/main.py | 7 +- addons/point_of_sale/static/src/css/pos.css | 174 +++++++++++------- .../static/src/img/bc-arrow-big.png | Bin 0 -> 315 bytes addons/point_of_sale/static/src/js/models.js | 1 - addons/point_of_sale/static/src/js/screens.js | 47 ++--- addons/point_of_sale/static/src/js/widgets.js | 102 +++++----- addons/point_of_sale/static/src/xml/pos.xml | 75 ++++---- 7 files changed, 216 insertions(+), 190 deletions(-) create mode 100644 addons/point_of_sale/static/src/img/bc-arrow-big.png diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 705fd853ec1..4871997f5de 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -32,8 +32,7 @@ html_template = """ - - %(css)s + %(js)s ' % i for i in js_list) - css = "\n".join('' % i for i in css_list) + #css = "\n".join('' % i for i in css_list) r = html_template % { 'js': js, - 'css': css, + # 'css': css, 'modules': simplejson.dumps(module_boot(request.db)), 'init': """ console.log('LOADED'); diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 18673ec1cc2..52d2ec291e1 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1,34 +1,35 @@ +/* --- Fonts --- */ + @font-face{ font-family: 'Inconsolata'; src: url(../fonts/Inconsolata.otf); } +/* --- Generic Restyling and Resets --- */ + +html { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +table { + border-spacing: 0px; + border-collapse: collapse; +} +td { + padding: 0px; +} + .oe_hidden{ display: none !important; } - -.pos { - padding: 0; - margin: 0; - background-color: #f0eeee; - font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial; - color: #555555; - font-size: 12px; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - text-shadow: none; - overflow: hidden; +.clearfix:after { + content:" "; + display: block; + visibility: hidden; + line-height: 0; + height: 0; + clear: both; } -.pos * { - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} .pos input::-webkit-outer-spin-button, .pos input::-webkit-inner-spin-button { @@ -55,6 +56,25 @@ list-style-type: none; } +.pos { + padding: 0; + margin: 0; + background-color: #f0eeee; + font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial; + color: #555555; + font-size: 12px; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + text-shadow: none; + overflow: hidden; +} + /* ********* The black loading screen ********* */ .pos .loader{ @@ -66,6 +86,7 @@ height:100%; z-index: 999; text-align: center; + font-family: Lato; } /* ********* Generic Layout Constructs ********* */ @@ -143,6 +164,7 @@ font-size: 11px; border: 1px solid #cacaca; background: #e2e2e2; + border-radius: 3px; } .pos ul, .pos ol { padding: 0; @@ -157,10 +179,6 @@ .pos .pos-center-align { text-align: center; } -.pos .pos-right-align input { - text-align: right; - border: 1px solid #cecbcb; -} .pos .pos-disc-font { font-size: 12px; font-style:italic; @@ -198,7 +216,7 @@ line-height:100%; vertical-align: middle; } -.pos .pos-branding img { +.pos .pos-logo { height: 32px; width: 116px; margin-left:5px; @@ -223,7 +241,7 @@ height:100%; } -.pos .pos-rightheader button { +.pos .order-button{ color: #273072; height:27px; margin:3px; @@ -231,28 +249,27 @@ border: 1px solid #353A7E; background: #7f82ac; text-shadow: none; + border-radius: 3px; +} +.pos .order-button.selected{ + font-weight: 900; + background: #666997; + color: rgb(236, 237, 255); } -.pos .pos-rightheader button.square{ +.pos .order-button.square{ width: 32px; margin-left:4px; } .pos .order-selector { - display: inline; + display: inline-block; } .pos .orders { - display: inline; + display: inline-block; + vertical-align: top; margin-left: 8px; } -.pos .order-selector-button { - display: inline; -} -.pos .selected-order button { - font-weight: 900; - background: #7174A8 !important; - color: rgb(236, 237, 255) !important; -} /* c) The session buttons */ @@ -261,8 +278,8 @@ height:32px; padding-left:10px; padding-right:10px; - border-right: 1px solid #3a3a3a; - border-left: 1px solid #3a3a3a; + border-right: 1px solid #292929; + border-left: 1px solid #292929; color:#DDD; line-height:32px; text-align:center; @@ -392,7 +409,7 @@ /* ********* The right pane contains the screens and headers ********* */ -.pos .pos-rightpane { +.pos .rightpane { position: absolute; top: 0; /*bottom: 105px;*/ @@ -400,19 +417,22 @@ left: 440px; right: 0; vertical-align: top; + -webkit-transform: translate3d(0,0,0); } -.pos .pos-rightpane header { +.pos .rightpane-header { padding: 0; - height: 32px; + height: 48px; border-bottom: 1px solid #c7c7c7; background: #d3d3d3; + text-align: center; } /* ********* The product list ********* */ .pos .product-list { - padding:10px !important; + padding: 10px; + text-align: left; -webkit-transform: translate3d(0,0,0); } @@ -439,29 +459,36 @@ /* a) the product list navigation bar */ -.pos .breadcrumb li { - float: left; - line-height: 32px; - height: 32px; +.pos .breadcrumbs{ + display: inline-block; + text-align: left; + float:left; } -.pos .breadcrumb li:last-child { +.pos .breadcrumb{ + float: left; + display: inline-block; + line-height: 48px; + height: 48px; + min-width: 48px; +} +.pos .breadcrumb:last-child { padding-right: 3px; border-right: 1px solid #c5c5c5; } -.pos .breadcrumb a { +.pos .breadcrumb-button { display: inline-block; padding: 0 9px; vertical-align: top; - color: #555555; - font-weight: bold; + color: #808080; + font-size: 14px; + cursor: pointer; } -.pos .bc-arrow { - height: 33px; +.pos .breadcrumb-arrow{ + width: 28px; } -.pos .homeimg { - width: 19px; - height: 19px; - margin: 6px 0; +.pos .breadcrumb-homeimg { + width: 27px; + margin: 12px 6px; } /* b) the search box */ @@ -471,19 +498,28 @@ right: 2px; } .pos .searchbox input { - width: 130px; + width: 100px; border: 1px solid #cecbcb; - padding: 3px 19px; + padding: 10px 20px; + padding-left: 38px; margin: 6px; - background: url("../img/search.png") no-repeat 5px; + background: url("../img/search.png") no-repeat 15px; background-color: white; + border-radius: 20px; + font-family: Lato; +} +.pos .searchbox input:focus { + outline: none; + box-shadow: 0px 0px 0px 2px rgb(153, 153, 255) inset; + color: rgb(153, 153, 255); } .pos .search-clear { position: absolute; + width: 30px; + height: 30px; top: 11px; - right: 11px; + left: 11px; cursor: pointer; - display: none; } /* c) the categories list */ @@ -498,7 +534,8 @@ } .pos .category-list{ - padding:10px !important; + text-align: left; + padding: 10px; } /* d) the category button */ @@ -726,6 +763,9 @@ box-sizing: border-box; border-radius: 3px; } +.pos .paymentline-name{ + margin-bottom: 8px; +} .pos .paymentline-input{ font-size: 20px; font-family: Lato; @@ -738,6 +778,7 @@ background: white; color: #484848; text-align: right; + border-radius: 3px; box-shadow: 0px 2px rgba(143, 143, 143, 0.3) inset; } @@ -803,6 +844,8 @@ padding-bottom:30px; display: inline-block; font-family: "Inconsolata"; + border: solid 1px rgb(220,220,220); + border-radius: 3px; } .pos .pos-sale-ticket .emph{ font-size: 20px; @@ -829,7 +872,7 @@ .pos .pos-content { top: 0px !important; } - .pos .pos-rightpane { + .pos .rightpane { left: 0px !important; background-color: white; } @@ -1133,6 +1176,7 @@ cursor: pointer; border: 1px solid #cacaca; + border-radius: 3px; background: #e2e2e2; } diff --git a/addons/point_of_sale/static/src/img/bc-arrow-big.png b/addons/point_of_sale/static/src/img/bc-arrow-big.png new file mode 100644 index 0000000000000000000000000000000000000000..f845fe6837d05cd3549d3873024e4074fbd39c26 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^GC*v=!3HFmlBI3}DYhhUcNd1sAh?`)+ee@XXMsm# zF#`j)FbFd;%$g$s6l5>)^mS#w%freeZ&4%h9b~`>PZ!6Kh{JEE@8vyYAi~z)p(&xv zn$&d0@h}U|0j3-WK`$#VwH<+%mL#)fIJLJ3%~>w+Cn~z`m)~>yir`mKwE~e@Yp-4O zmEP*L^Vz4vKiD!FUEOALWxCzo-8JuX`2kIN4$X=G
          - +
          - - -
            + + +
            @@ -45,7 +45,7 @@
            -
            +
            @@ -137,49 +137,49 @@ -
          1. +
            -
          2. +
            -
          3. + -
          4. +
            -
            - +
            -
              -
            +
            +
            @@ -188,8 +188,8 @@
            -
              -
            +
            +
            @@ -219,7 +219,6 @@
            -

            Product Weighting

            @@ -245,7 +244,6 @@

            -

            Payment

            @@ -275,7 +273,6 @@
            -

            Receipt

            @@ -285,7 +282,7 @@
            -

            Welcome

            +

            Welcome

            Please scan an item or your member card

            @@ -299,14 +296,14 @@
            -

            Please scan an item

            +

            Please scan an item

            -

            Payment

            +

            Payment

            Please insert your card in the reader and follow the instructions to complete @@ -317,7 +314,7 @@

            -

            Please put your product on the scale

            +

            Please put your product on the scale

            @@ -415,7 +412,7 @@ -
          5. +
            @@ -432,7 +429,7 @@
            -
          6. +
            @@ -446,7 +443,7 @@
          -
          +
          Total: 0.00 € @@ -578,9 +575,7 @@ -
        • - -
        • +
          From 173ffb995f511e8a42b56c9e75d9dc25b1fa4fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Sat, 7 Dec 2013 18:00:14 +0100 Subject: [PATCH 25/64] [WIP] point_of_sale: making the standalone pos launchable from the pos backend bzr revid: fva@openerp.com-20131207170014-9pctopzgx5ahtb21 --- addons/point_of_sale/__openerp__.py | 5 ----- addons/point_of_sale/controllers/main.py | 6 ++---- addons/point_of_sale/point_of_sale.py | 14 ++++++-------- addons/point_of_sale/point_of_sale_view.xml | 5 +++-- addons/point_of_sale/wizard/pos_session_opening.py | 7 +++---- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 44ceb83b02d..fa42a19e209 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -100,11 +100,6 @@ Main Features 'static/src/js/tests.js', 'static/src/js/main.js', ], - 'css': [ - 'static/src/css/pos.css', # this is the default css with hover effects - #'static/src/css/pos_nohover.css', # this css has no hover effects (for resistive touchscreens) - 'static/src/css/keyboard.css' - ], 'qweb': ['static/src/xml/pos.xml'], 'auto_install': False, } diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 4871997f5de..24cd29f6a69 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -32,7 +32,9 @@ html_template = """ + + %(js)s