diff options
| author | Frédéric Van der Essen <fva@openerp.com> | 2014-08-25 17:36:12 +0200 |
|---|---|---|
| committer | Frédéric Van der Essen <fva@openerp.com> | 2014-08-25 17:36:12 +0200 |
| commit | e96bb90d21b72d18e61438ddf5ea2c28cd9673fb (patch) | |
| tree | 2d7e1313f52573a05ae6a8aefe25b6a43bc6e0e3 | |
| parent | 48e0afcf95dd3263bd226066b4f1fc4aa450ccd5 (diff) | |
| parent | d7f93defb899b2b48a14d679e5dffffdb4a00eff (diff) | |
Merge pull request #1983 from odoo-dev/master-pos-new-payment-screen-fva
Point of Sale : New Payment Screen & Receipt Screens
35 files changed, 2488 insertions, 1014 deletions
diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 03e56800e5a..69fe4bc9ea5 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -72,6 +72,7 @@ class pos_config(osv.osv): 'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"), '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'), + 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), @@ -394,7 +395,7 @@ class pos_session(osv.osv): for obj in self.browse(cr, uid, ids, context=context): for statement in obj.statement_ids: statement.unlink(context=context) - return True + return super(pos_session, self).unlink(cr, uid, ids, context=context) def open_cb(self, cr, uid, ids, context=None): @@ -553,6 +554,7 @@ class pos_order(osv.osv): orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] order_ids = [] + for tmp_order in orders_to_save: to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 2202627ae3e..d9d777d5ea3 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -628,57 +628,80 @@ </div> </header> - <label for="name" class="oe_edit_only"/> - <h1> - <field name="name"/> - </h1> - <group col="4"> - <field name="picking_type_id" widget="selection" groups="stock.group_locations" - on_change="onchange_picking_type_id(picking_type_id)"/> - <field name="stock_location_id" groups="stock.group_locations"/> - <field name="pricelist_id" groups="product.group_sale_pricelist"/> - <field name="currency_id" invisible="1"/> - <field name="journal_id" widget="selection"/> - <field name="group_by" groups="account.group_account_user"/> - <field name="sequence_id" readonly="1" groups="base.group_no_one"/> - </group> - <separator string="Available Payment Methods" colspan="4"/> - <field name="journal_ids" colspan="4" nolabel="1"> - <tree string="Journals"> - <field name="code" /> - <field name="name" /> - <field name="type" /> - <field name="cash_control" /> - </tree> - </field> - <group string="Features" > - <group> - <field name="iface_vkeyboard" /> - <field name="iface_invoicing" /> + <sheet> + <label for="name" class="oe_edit_only"/> + <h1> + <field name="name"/> + </h1> + <group col="4"> + <field name="picking_type_id" widget="selection" groups="stock.group_locations" + on_change="onchange_picking_type_id(picking_type_id)"/> + <field name="stock_location_id" groups="stock.group_locations"/> + <field name="pricelist_id" groups="product.group_sale_pricelist"/> + <field name="currency_id" invisible="1"/> + <field name="journal_id" widget="selection"/> + <field name="group_by" groups="account.group_account_user"/> + <field name="sequence_id" readonly="1" groups="base.group_no_one"/> </group> - <group> - <field name="iface_big_scrollbars" /> + <separator string="Available Payment Methods" colspan="4"/> + <field name="journal_ids" colspan="4" nolabel="1"> + <tree string="Journals"> + <field name="code" /> + <field name="name" /> + <field name="type" /> + <field name="cash_control" /> + </tree> + </field> + <group string="Features" > + <group> + <field name="iface_vkeyboard" /> + <field name="iface_invoicing" /> + </group> + <group> + <field name="iface_fullscreen" /> + <field name="iface_big_scrollbars" /> + </group> </group> - </group> - <group string="Hardware Proxy" > - <field name="proxy_ip" /> - <field name="iface_print_via_proxy" /> - <field name="iface_scan_via_proxy" /> - <field name="iface_electronic_scale" /> - <field name="iface_cashdrawer" /> - </group> - <group string="Receipt" > - <field name="receipt_header" placeholder="A custom receipt header message"/> - <field name="receipt_footer" placeholder="A custom receipt header footage"/> - </group> - <group string="Barcode Types" col="4"> - <field name="barcode_product" /> - <field name="barcode_cashier" /> - <field name="barcode_customer" /> - <field name="barcode_weight" /> - <field name="barcode_discount" /> - <field name="barcode_price" /> - </group> + <group string="Hardware Proxy" > + <field name="proxy_ip" /> + <field name="iface_print_via_proxy" /> + <field name="iface_scan_via_proxy" /> + <field name="iface_electronic_scale" /> + <field name="iface_cashdrawer" /> + </group> + <group string="Receipt" > + <field name="receipt_header" placeholder="A custom receipt header message"/> + <field name="receipt_footer" placeholder="A custom receipt header footage"/> + </group> + <group string="Barcode Types" col="1"> + <p> + Barcode Patterns allow to match barcodes to actions or to embed information such as price and quantity in the barcode. + Barcode Patterns only work with EAN13 barcodes. + </p> + <p> + Each type of barcode accepts a list of patterns seprated by commas. A scanned + barcode will be attributed to a type if it matches one of its patterns. + The patterns take the form of EAN13 barcodes. Numbers in the pattern must match + the number in the scanned barcode. A 'x' or a '*' in a pattern will match + any one number. If the patterns are shorter than EAN13 barcodes, they are assumed + to be prefixes and match at the beginning. Weight, Price and Discount patterns also + tell how the weight, price or discount is encoded in the barcode. 'N' indicate the + positions where the integer part is en encoded, and 'D' where the decimals are encoded. + If multiple pattern match one barcode, the longest pattern with the less 'x' or '*' is + considered the matching one. If a barcode matches no pattern it will not be found in + the POS. + </p> + <group col="4"> + + <field name="barcode_product" /> + <field name="barcode_cashier" /> + <field name="barcode_customer" /> + <field name="barcode_weight" /> + <field name="barcode_discount" /> + <field name="barcode_price" /> + </group> + </group> + </sheet> </form> </field> diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 30939636b23..1609e1b2633 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -20,6 +20,19 @@ .ui-dialog .ui-icon-closethick{ float: right; } +div.modal.in { + position: absolute; + background: white; + padding: 20px; + box-shadow: 0px 10px 20px black; + border-radius: 3px; + max-width: 600px; + max-height: 400px; + margin-top: -200px; + margin-left: -300px; + top: 50%; + left: 50%; +} /* --- Generic Restyling and Resets --- */ html { @@ -483,10 +496,11 @@ td { margin-right: 4px; } -.pos .control-button.highlight{ - background: #6EC89B; - border: solid 1px #6EC89B; - color: white; +.pos .control-button.highlight, +.pos .button.highlight { + background: #6EC89B !important; + border: solid 1px #6EC89B !important; + color: white !important; } .pos .control-button:active { background: #7F82AC; @@ -502,20 +516,20 @@ td { color: inherit; } -/* ********* The paypad contains the payment buttons ********* */ +/* ********* The actionpad (payment, set customer) ********* */ -.pos .paypad { - padding: 8px 4px 8px 8px; +.pos .actionpad{ + padding: 8px 3px 8px 19px; display: inline-block; text-align: center; vertical-align: top; - width: 205px; + width: 183px; max-height: 350px; overflow-y: auto; overflow-x: hidden; } -.pos .paypad button { - height: 39px; +.pos .actionpad .button { + height: 50px; display: block; width: 100%; margin: 0px -6px 4px -2px; @@ -524,13 +538,20 @@ td { color: #555555; font-size: 14px; } -.pos .paypad button:active, -.pos .numpad button:active, -.pos .numpad .selected-mode, -.pos .popup button:active{ - border: none; +.pos .actionpad .button.pay { + height: 158px; +} +.pos .actionpad .button.pay .fa { + display: block; + font-size: 32px; + line-height: 54px; + padding-top: 6px; + background: rgb(86, 86, 86); color: white; - background: #7f82ac; + width: 60px; + margin: auto; + border-radius: 30px; + margin-bottom: 10px; } /* ********* The Numpad ********* */ @@ -543,7 +564,7 @@ td { .pos .numpad button { height: 50px; width: 50px; - margin: 0px 0px 4px 0px; + margin: 0px 3px 4px 0px; font-weight: bold; vertical-align: middle; color: #555555; @@ -913,6 +934,9 @@ td { overflow-y: auto; border-right: dashed 1px rgb(215,215,215); } +.screen .left-content.pc40{ + right: 66%; +} .screen .right-content{ position: absolute; right:0px; top: 64px; bottom: 0px; @@ -920,6 +944,9 @@ td { overflow-x: hidden; overflow-y: auto; } +.screen .right-content.pc60{ + left:34% +} .screen .centered-content{ position: absolute; right:25%; top: 64px; bottom: 0px; @@ -966,101 +993,125 @@ td { position:relative; } -/* b) The payment screen */ +/* b) The payment screen */ -.pos .pos-payment-container { +.pos .payment-numpad { display: inline-block; - font-size: 16px; - text-align: left; - width: 360px; -} -.pos .payment-due-total { + width: 50%; + box-sizing: border-box; + padding: 8px; text-align: center; - font-weight: bold; - font-size: 48px; - margin: 27px; - text-shadow: 0px 2px rgb(202, 202, 202); + float: left; } -.pos .paymentline{ - position: relative; +.pos .payment-numpad .numpad button { + width: 66px; + height: 66px; +} + +.pos .paymentlines-container { + padding: 16px; + padding-top: 0; + border-bottom: dashed 1px gainsboro; + min-height: 154px; +} + +.pos .paymentlines { + width: 100%; +} +.pos .paymentlines .controls { + width: 40px; +} +.pos .paymentlines .label > * { + font-size: 16px; padding: 8px; - border-box: 3px; - box-sizing: border-box; - -moz-box-sizing: border-box; +} +.pos .paymentlines tbody{ + background: white; border-radius: 3px; } -.pos .paymentline-name{ - margin-bottom: 8px; +.pos .paymentline{ + font-size: 22px; } -.pos .paymentline-input{ - font-size: 20px; - font-family: Lato; - display: block; - width: 100%; - box-sizing: border-box; - -moz-box-sizing: border-box; - outline: none; - border: none; - padding: 6px 8px; +.pos .paymentline.selected { + font-size: 22px; + background: #6EC89B; + color: white; +} +.pos .paymentline.selected .edit { background: white; - color: #484848; - text-align: right; + color: #6EC89B; + outline: 3px blue; + box-shadow: 0px 0px 0px 3px #6EC89B; + position: relative; border-radius: 3px; - 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; +.pos .paymentline > *{ + padding: 8px 12px; } - -.paymentline-delete { - width: 32px; - height: 32px; - padding: 5px; - text-align: center; - box-sizing: border-box; - -moz-box-sizing: border-box; - position: absolute; - bottom: 10px; - left: 8px; +.pos .paymentline .col-due, +.pos .paymentline .col-tendered, +.pos .paymentline .col-change { + min-width: 90px; +} +.pos .paymentline .col-change.highlight { + background: rgb(184, 152, 204); +} +.pos .paymentline .col-name { + font-size: 16px; +} +.pos .paymentline .delete-button{ cursor: pointer; + text-align: center; } - -.pos .pos-payment-container .left-block{ +.pos .payment-buttons { display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:left; + width: 50%; + box-sizing: border-box; + padding: 16px; + float: right; } -.pos .pos-payment-container .infoline{ - margin-top:5px; - margin-bottom:5px; + +.payment-screen .payment-buttons .button { + background: rgb(221, 221, 221); + line-height: 48px; + margin-bottom: 4px; + border-radius: 3px; + font-size: 16px; padding: 0px 8px; - opacity: 0.5; -} -.pos .pos-payment-container .infoline.bigger{ - opacity: 1; - font-size: 20px; + border: solid 1px rgb(202, 202, 202); + cursor: pointer; + text-align: center; } -.pos .pos-payment-container .right-block{ - display: inline-block; - width:49%; - margin:0; - padding:0; - text-align:right; +.payment-screen .paymentlines-empty .total { + text-align: center; + padding: 24px 0px 18px; + font-size: 64px; + color: #43996E; + text-shadow: 0px 2px white, 0px 2px 2px rgba(0, 0, 0, 0.27); } -.pos .paymentline.selected{ - background: rgb(220,220,220); +.payment-screen .paymentlines-empty .message { + text-align: center; } /* c) The receipt screen */ +.pos .receipt-screen .centered-content .button { + line-height: 40px; + padding: 3px 13px; + font-size: 20px; + text-align: center; + background: rgb(230, 230, 230); + margin: 16px; + margin-bottom: 0px; + border-radius: 3px; + border: solid 1px rgb(209, 209, 209); + cursor: pointer; +} .pos .pos-receipt-container { font-size: 0.75em; + text-align: center; } .pos .pos-sale-ticket { @@ -1095,10 +1146,16 @@ td { } @media print { + body { + margin: 0; + } .oe_leftbar, .pos .pos-topheader, .pos .pos-leftpane, - .pos .keyboard_frame { + .pos .keyboard_frame, + .pos .receipt-screen header, + .pos .receipt-screen .top-content, + .pos .receipt-screen .centered-content .button { display: none !important; } .pos, @@ -1119,12 +1176,17 @@ td { left: 0px !important; background-color: white; } - .pos .receipt-screen header { - display: none !important; - } .pos .receipt-screen { text-align: left; } + .pos .receipt-screen .centered-content{ + position: static; + border: none; + margin: none; + } + .pos .pos-receipt-container { + text-align: left; + } .pos-actionbar { display: none !important; } @@ -1469,10 +1531,12 @@ td { font-size: 24px; vertical-align: top; } -.splitbill-screen .paymentmethods { +.splitbill-screen .paymentmethods, +.payment-screen .paymentmethods { margin: 16px; } -.splitbill-screen .paymentmethod { +.splitbill-screen .paymentmethod, +.payment-screen .paymentmethod { background: rgb(221, 221, 221); line-height: 40px; margin-bottom: 4px; @@ -1482,6 +1546,14 @@ td { cursor: pointer; text-align: center; } +.splitbill-screen .paymentmethod.active, +.payment-screen .paymentmethod.active { + background: #6EC89B; + color: white; + border-color: #6EC89B; +} + + /* ********* The ActionBarWidget ********* */ @@ -1663,7 +1735,17 @@ td { .pos .popup .comment{ font-weight: normal; font-size: 18px; - margin: 0px 16px; + margin: 0px 16px 16px 16px; +} +.pos .popup .comment.traceback { + height: 264px; + overflow: auto; + font-size: 14px; + text-align: left; + font-family: 'Inconsolata'; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; } .pos .popup .footer{ position:absolute; diff --git a/addons/point_of_sale/static/src/img/ios-share-icon.png b/addons/point_of_sale/static/src/img/ios-share-icon.png Binary files differnew file mode 100644 index 00000000000..8588657e123 --- /dev/null +++ b/addons/point_of_sale/static/src/img/ios-share-icon.png diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 38230fb0ca2..08d4f879968 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -345,6 +345,9 @@ function openerp_pos_db(instance, module){ }); this.save('orders',orders); }, + remove_all_orders: function(){ + this.save('orders',[]); + }, get_orders: function(){ return this.load('orders',[]); }, diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 90fa3567cda..373a5f66761 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -19,8 +19,6 @@ openerp.point_of_sale = function(instance) { openerp_pos_widgets(instance,module); // import pos_widgets.js - openerp_pos_tests(instance,module); // import pos_tests.js - instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget'); }; diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 92276db60de..e57353fd231 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -114,180 +114,250 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return done; }, - // helper function to load data from the server + // helper function to load data from the server. Obsolete use the models loader below. fetch: function(model, fields, domain, ctx){ this._load_progress = (this._load_progress || 0) + 0.05; this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress); return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() }, - // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. - load_server_data: function(){ - var self = this; - - var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) - .then(function(users){ - self.user = users[0]; - - return self.fetch('res.company', - [ - 'currency_id', - 'email', - 'website', - 'company_registry', - 'vat', - 'name', - 'phone', - 'partner_id', - ], - [['id','=',users[0].company_id[0]]], - {show_address_only: true}); - }).then(function(companies){ - self.company = companies[0]; - - return self.fetch('product.uom', null, null); - }).then(function(units){ - self.units = units; - var units_by_id = {}; - for(var i = 0, len = units.length; i < len; i++){ - units_by_id[units[i].id] = units[i]; - units[i].groupable = ( units[i].category_id[0] === 1 ); - units[i].is_unit = ( units[i].id === 1 ); + // Server side model loaders. This is the list of the models that need to be loaded from + // the server. The models are loaded one by one by this list's order. The 'loaded' callback + // is used to store the data in the appropriate place once it has been loaded. This callback + // can return a deferred that will pause the loading of the next module. + // a shared temporary dictionary is available for loaders to communicate private variables + // used during loading such as object ids, etc. + models: [ + { + model: 'res.users', + fields: ['name','company_id'], + domain: function(self){ return [['id','=',self.session.uid]]; }, + loaded: function(self,users){ self.user = users[0]; }, + },{ + model: 'res.company', + fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' ], + domain: function(self){ return [['id','=',self.user.company_id[0]]]; }, + loaded: function(self,companies){ self.company = companies[0]; }, + },{ + model: 'product.uom', + fields: [], + domain: null, + loaded: function(self,units){ + self.units = units; + var units_by_id = {}; + for(var i = 0, len = units.length; i < len; i++){ + units_by_id[units[i].id] = units[i]; + units[i].groupable = ( units[i].category_id[0] === 1 ); + units[i].is_unit = ( units[i].id === 1 ); + } + self.units_by_id = units_by_id; + } + },{ + model: 'res.users', + fields: ['name','ean13'], + domain: null, + loaded: function(self,users){ self.users = users; }, + },{ + model: 'res.partner', + fields: ['name','street','city','country_id','phone','zip','mobile','email','ean13'], + domain: null, + loaded: function(self,partners){ + self.partners = partners; + self.db.add_partners(partners); + }, + },{ + model: 'account.tax', + fields: ['name','amount', 'price_include', 'type'], + domain: null, + loaded: function(self,taxes){ self.taxes = taxes; }, + },{ + model: 'pos.session', + fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'], + domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; }, + loaded: function(self,pos_sessions){ self.pos_session = pos_sessions[0]; }, + },{ + model: 'pos.config', + fields: [], + domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; }, + loaded: function(self,configs){ + self.config = configs[0]; + self.config.use_proxy = self.config.iface_payment_terminal || + self.config.iface_electronic_scale || + self.config.iface_print_via_proxy || + self.config.iface_scan_via_proxy || + self.config.iface_cashdrawer; + + self.barcode_reader.add_barcode_patterns({ + 'product': self.config.barcode_product, + 'cashier': self.config.barcode_cashier, + 'client': self.config.barcode_customer, + 'weight': self.config.barcode_weight, + 'discount': self.config.barcode_discount, + 'price': self.config.barcode_price, + }); + }, + },{ + model: 'stock.location', + fields: [], + domain: function(self){ return [['id','=', self.config.stock_location_id[0]]]; }, + loaded: function(self, locations){ self.shop = locations[0]; }, + },{ + model: 'product.pricelist', + fields: ['currency_id'], + domain: function(self){ return [['id','=',self.config.pricelist_id[0]]]; }, + loaded: function(self, pricelists){ self.pricelist = pricelists[0]; }, + },{ + model: 'res.currency', + fields: ['symbol','position','rounding','accuracy'], + domain: function(self){ return [['id','=',self.pricelist.currency_id[0]]]; }, + loaded: function(self, currencies){ + self.currency = currencies[0]; + }, + },{ + model: 'product.packaging', + fields: ['ean','product_tmpl_id'], + domain: null, + loaded: function(self, packagings){ + self.db.add_packagings(packagings); + }, + },{ + model: 'pos.category', + fields: ['id','name','parent_id','child_id','image'], + domain: null, + loaded: function(self, categories){ + self.db.add_categories(categories); + }, + },{ + model: 'product.product', + fields: ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants', + 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', + 'product_tmpl_id'], + domain: function(self){ return [['sale_ok','=',true],['available_in_pos','=',true]]; }, + context: function(self){ return { pricelist: self.pricelist.id }; }, + loaded: function(self, products){ + self.db.add_products(products); + }, + },{ + model: 'account.bank.statement', + fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], + domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; }, + loaded: function(self, bankstatements, tmp){ + self.bankstatements = bankstatements; + + tmp.journals = []; + _.each(bankstatements,function(statement){ + tmp.journals.push(statement.journal_id[0]); + }); + }, + },{ + model: 'account.journal', + fields: [], + domain: function(self,tmp){ return [['id','in',tmp.journals]]; }, + loaded: function(self, journals){ + self.journals = journals; + + // associate the bank statements with their journals. + var bankstatements = self.bankstatements; + for(var i = 0, ilen = bankstatements.length; i < ilen; i++){ + for(var j = 0, jlen = journals.length; j < jlen; j++){ + if(bankstatements[i].journal_id[0] === journals[j].id){ + bankstatements[i].journal = journals[j]; + bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; + } + } + } + self.cashregisters = bankstatements; + }, + },{ + loaded: function(self){ + self.company_logo = new Image(); + self.company_logo.crossOrigin = 'anonymous'; + var logo_loaded = new $.Deferred(); + self.company_logo.onload = function(){ + var img = self.company_logo; + var ratio = 1; + var targetwidth = 300; + var maxheight = 150; + if( img.width !== targetwidth ){ + ratio = targetwidth / img.width; + } + if( img.height * ratio > maxheight ){ + ratio = maxheight / img.height; } - self.units_by_id = units_by_id; + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + var c = document.createElement('canvas'); + c.width = width; + c.height = height + var ctx = c.getContext('2d'); + ctx.drawImage(self.company_logo,0,0, width, height); - return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); - }).then(function(users){ - self.users = users; + self.company_logo_base64 = c.toDataURL(); + window.logo64 = self.company_logo_base64; + logo_loaded.resolve(); + }; + self.company_logo.onerror = function(){ + logo_loaded.reject(); + }; + self.company_logo.src = window.location.origin + '/web/binary/company_logo'; - return self.fetch('res.partner', ['name','street','city','country_id','phone','zip','mobile','email','ean13']); - }).then(function(partners){ - self.partners = partners; - self.db.add_partners(partners); - - return self.fetch('account.tax', ['name','amount', 'price_include', 'type']); - }).then(function(taxes){ - self.taxes = taxes; - - return self.fetch( - 'pos.session', - ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number'], - [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] - ); - }).then(function(pos_sessions){ - self.pos_session = pos_sessions[0]; + return logo_loaded; + }, + }, + ], - return self.fetch('pos.config',[],[['id','=', self.pos_session.config_id[0]]]); - }).then(function(configs){ - self.config = configs[0]; - self.config.use_proxy = self.config.iface_payment_terminal || - self.config.iface_electronic_scale || - self.config.iface_print_via_proxy || - self.config.iface_scan_via_proxy || - self.config.iface_cashdrawer; + // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. + load_server_data: function(){ + var self = this; + var loaded = new $.Deferred(); + var progress = 0; + var progress_step = 1.0 / self.models.length; + var tmp = {}; // this is used to share a temporary state between models loaders + + function load_model(index){ + if(index >= self.models.length){ + loaded.resolve(); + }else{ + var model = self.models[index]; + self.pos_widget.loading_message(_t('Loading')+' '+(model.model || ''), progress); + var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; + var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; + var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context; + progress += progress_step; - self.barcode_reader.add_barcode_patterns({ - 'product': self.config.barcode_product, - 'cashier': self.config.barcode_cashier, - 'client': self.config.barcode_customer, - 'weight': self.config.barcode_weight, - 'discount': self.config.barcode_discount, - 'price': self.config.barcode_price, - }); - return self.fetch('stock.location',[],[['id','=', self.config.stock_location_id[0]]]); - }).then(function(shops){ - self.shop = shops[0]; - - return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]); - }).then(function(pricelists){ - self.pricelist = pricelists[0]; - - return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]); - }).then(function(currencies){ - self.currency = currencies[0]; - - return self.fetch('product.packaging',['ean','product_tmpl_id']); - }).then(function(packagings){ - self.db.add_packagings(packagings); - - return self.fetch('pos.category', ['id','name','parent_id','child_id','image']); - }).then(function(categories){ - self.db.add_categories(categories); - - return self.fetch( - 'product.product', - ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants', - 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', - 'product_tmpl_id'], - [['sale_ok','=',true],['available_in_pos','=',true]], - {pricelist: self.pricelist.id} // context for price - ); - }).then(function(products){ - self.db.add_products(products); - - return self.fetch( - 'account.bank.statement', - ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], - [['state','=','open'],['pos_session_id', '=', self.pos_session.id]] - ); - }).then(function(bankstatements){ - var journals = []; - _.each(bankstatements,function(statement) { - journals.push(statement.journal_id[0]); - }); - self.bankstatements = bankstatements; - return self.fetch('account.journal', undefined, [['id','in', journals]]); - }).then(function(journals){ - self.journals = journals; - - // associate the bank statements with their journals. - var bankstatements = self.bankstatements; - for(var i = 0, ilen = bankstatements.length; i < ilen; i++){ - for(var j = 0, jlen = journals.length; j < jlen; j++){ - if(bankstatements[i].journal_id[0] === journals[j].id){ - bankstatements[i].journal = journals[j]; - bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; - } + if( model.model ){ + new instance.web.Model(model.model).query(fields).filter(domain).context(context).all() + .then(function(result){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,result,tmp)) + .then(function(){ load_model(index + 1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + loaded.reject(err); + } + },function(err){ + loaded.reject(err); + }); + }else if( model.loaded ){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,tmp)) + .then( function(){ load_model(index +1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + loaded.reject(err); } + }else{ + load_model(index + 1); } - self.cashregisters = bankstatements; - - // Load the company Logo + } + } - self.company_logo = new Image(); - self.company_logo.crossOrigin = 'anonymous'; - var logo_loaded = new $.Deferred(); - self.company_logo.onload = function(){ - var img = self.company_logo; - var ratio = 1; - var targetwidth = 300; - var maxheight = 150; - if( img.width !== targetwidth ){ - ratio = targetwidth / img.width; - } - if( img.height * ratio > maxheight ){ - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - var c = document.createElement('canvas'); - c.width = width; - c.height = height - var ctx = c.getContext('2d'); - ctx.drawImage(self.company_logo,0,0, width, height); - - self.company_logo_base64 = c.toDataURL(); - window.logo64 = self.company_logo_base64; - logo_loaded.resolve(); - }; - self.company_logo.onerror = function(){ - logo_loaded.reject(); - }; - self.company_logo.src = window.location.origin + '/web/binary/company_logo'; + try{ + load_model(0); + }catch(err){ + loaded.reject(err); + } - return logo_loaded; - }); - return loaded; }, @@ -324,20 +394,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. push_order: function(order) { var self = this; - this.proxy.log('push_order',order.export_as_JSON()); - var order_id = this.db.add_order(order.export_as_JSON()); - var pushed = new $.Deferred(); - this.set('synch',{state:'connecting', pending:self.db.get_orders().length}); + if(order){ + this.proxy.log('push_order',order.export_as_JSON()); + this.db.add_order(order.export_as_JSON()); + } + + var pushed = new $.Deferred(); this.flush_mutex.exec(function(){ - var flushed = self._flush_all_orders(); + var flushed = self._flush_orders(self.db.get_orders()); - flushed.always(function(){ + flushed.always(function(ids){ pushed.resolve(); }); - - return flushed; }); return pushed; }, @@ -361,8 +431,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var order_id = this.db.add_order(order.export_as_JSON()); - this.set('synch',{state:'connecting', pending:self.db.get_orders().length}); - this.flush_mutex.exec(function(){ var done = new $.Deferred(); // holds the mutex @@ -373,7 +441,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // things will happen as a duplicate will be sent next time // so we must make sure the server detects and ignores duplicated orders - var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true}); + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); transfer.fail(function(){ invoiced.reject('error-transfer'); @@ -382,10 +450,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // on success, get the order id generated by the server transfer.pipe(function(order_server_id){ + // generate the pdf and download it self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{ active_ids:order_server_id, }}); + invoiced.resolve(); done.resolve(); }); @@ -397,62 +467,33 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return invoiced; }, - // attemps to send all pending orders ( stored in the pos_db ) to the server, - // and remove the successfully sent ones from the db once - // it has been confirmed that they have been sent correctly. - flush: function() { + // wrapper around the _save_to_server that updates the synch status widget + _flush_orders: function(orders, options) { var self = this; - var flushed = new $.Deferred(); - - this.flush_mutex.exec(function(){ - var done = new $.Deferred(); - - self._flush_all_orders() - .done( function(){ flushed.resolve();}) - .fail( function(){ flushed.reject(); }) - .always(function(){ done.resolve(); }); - return done; - }); + this.set('synch',{ state: 'connecting', pending: orders.length}); - return flushed; - }, - - // attempts to send the locally stored order of id 'order_id' - // the sending is asynchronous and can take some time to decide if it is successful or not - // it is therefore important to only call this method from inside a mutex - // this method returns a deferred indicating wether the sending was successful or not - // there is a timeout parameter which is set to 2 seconds by default. - _flush_order: function( order_id, options) { - return this._flush_all_orders([this.db.get_order(order_id)], options); - }, - - // attempts to send all the locally stored orders. As with _flush_order, it should only be - // called from within a mutex. - // this method returns a deferred that always succeeds when all orders have been tried to be sent, - // even if none of them could actually be sent. - _flush_all_orders: function () { - var self = this; - self.set('synch', { - state: 'connecting', - pending: self.get('synch').pending - }); - return self._save_to_server(self.db.get_orders()).done(function () { + return self._save_to_server(orders, options).done(function (server_ids) { var pending = self.db.get_orders().length; + self.set('synch', { state: pending ? 'connecting' : 'connected', pending: pending }); + + return server_ids; }); }, // send an array of orders to the server // available options: // - timeout: timeout for the rpc call in ms + // returns a deferred that resolves with the list of + // server generated ids for the sent orders _save_to_server: function (orders, options) { if (!orders || !orders.length) { var result = $.Deferred(); - result.resolve(); + result.resolve([]); return result; } @@ -474,11 +515,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal shadow: !options.to_invoice, timeout: timeout } - ).then(function () { + ).then(function (server_ids) { _.each(orders, function (order) { self.db.remove_order(order.id); }); - }).fail(function (unused, event){ + return server_ids; + }).fail(function (error, event){ + if(error.code === 200 ){ // Business Logic Error, not a connection problem + self.pos_widget.screen_selector.show_popup('error-traceback',{ + message: error.data.message, + comment: error.data.debug + }); + } // prevent an error popup creation by the rpc failure // we want the failure to be silent as we send the orders in the background event.preventDefault(); @@ -816,6 +864,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.receipt_type = 'receipt'; // 'receipt' || 'invoice' this.temporary = attributes.temporary || false; this.sequence_number = this.pos.pos_session.sequence_number++; + this.to_invoice = false; return this; }, is_empty: function(){ @@ -948,11 +997,47 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return sum + paymentLine.get_amount(); }), 0); }, - getChange: function() { - return this.getPaidTotal() - this.getTotalTaxIncluded(); + getChange: function(paymentline) { + if (!paymentline) { + var change = this.getPaidTotal() - this.getTotalTaxIncluded(); + } else { + var change = -this.getTotalTaxIncluded(); + var lines = this.get('paymentLines').models; + for (var i = 0; i < lines.length; i++) { + change += lines[i].get_amount(); + if (lines[i] === paymentline) { + break; + } + } + } + return round_pr(Math.max(0,change), this.pos.currency.rounding); + }, + getDueLeft: function(paymentline) { + if (!paymentline) { + var due = this.getTotalTaxIncluded() - this.getPaidTotal(); + } else { + var due = this.getTotalTaxIncluded(); + var lines = this.get('paymentLines').models; + for (var i = 0; i < lines.length; i++) { + if (lines[i] === paymentline) { + break; + } else { + due -= lines[i].get_amount(); + } + } + } + return round_pr(Math.max(0,due), this.pos.currency.rounding); }, - getDueLeft: function() { - return this.getTotalTaxIncluded() - this.getPaidTotal(); + isPaid: function(){ + return this.getDueLeft() === 0; + }, + isPaidWithCash: function(){ + return !!this.get('paymentLines').find( function(pl){ + return pl.cashregister.journal.type === 'cash'; + }); + }, + finalize: function(){ + this.destroy(); }, // sets the type of receipt 'receipt'(default) or 'invoice' set_receipt_type: function(type){ @@ -984,6 +1069,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } } }, + set_to_invoice: function(to_invoice) { + this.to_invoice = to_invoice; + }, + is_to_invoice: function(){ + return this.to_invoice; + }, + // remove all the paymentlines with zero money in it + clean_empty_paymentlines: function() { + var lines = this.get('paymentLines').models; + var empty = []; + for ( var i = 0; i < lines.length; i++) { + if (!lines[i].get_amount()) { + empty.push(lines[i]); + } + } + for ( var i = 0; i < empty.length; i++) { + this.removePaymentline(empty[i]); + } + }, //see set_screen_data get_screen_data: function(key){ return this.screen_data[key]; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2f2f5e57312..fd1d1f69584 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -73,8 +73,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa load_saved_screen: function(){ this.close_popup(); var selectedOrder = this.pos.get('selectedOrder'); - // this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh'); - this.set_current_screen(this.default_screen,null,'refresh'); + // FIXME : this changing screen behaviour is sometimes confusing ... + this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh'); + //this.set_current_screen(this.default_screen,null,'refresh'); }, set_user_mode: function(user_mode){ @@ -210,26 +211,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa barcode_error_action: function(code){ this.pos_widget.screen_selector.show_popup('error-barcode',code.code); }, - // shows an action bar on the screen. The actionbar is automatically shown when you add a button - // with add_action_button() - show_action_bar: function(){ - this.pos_widget.action_bar.show(); - }, - - // hides the action bar. The actionbar is automatically hidden when it is empty - hide_action_bar: function(){ - this.pos_widget.action_bar.hide(); - }, - - // adds a new button to the action bar. The button definition takes three parameters, all optional : - // - label: the text below the button - // - icon: a small icon that will be shown - // - click: a callback that will be executed when the button is clicked. - // the method returns a reference to the button widget, and automatically show the actionbar. - add_action_button: function(button_def){ - this.show_action_bar(); - return this.pos_widget.action_bar.add_new_button(button_def); - }, // this method shows the screen and sets up all the widget related to this screen. Extend this method // if you want to alter the behavior of the screen. @@ -241,12 +222,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$el.removeClass('oe_hidden'); } - if(this.pos_widget.action_bar.get_button_count() > 0){ - this.show_action_bar(); - }else{ - this.hide_action_bar(); - } - var self = this; this.pos_widget.set_numpad_visible(this.show_numpad); @@ -269,7 +244,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa if(this.pos.barcode_reader){ this.pos.barcode_reader.reset_action_callbacks(); } - this.pos_widget.action_bar.destroy_buttons(); }, // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the @@ -314,6 +288,26 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); + module.FullscreenPopup = module.PopUpWidget.extend({ + template:'FullscreenPopupWidget', + show: function(){ + var self = this; + this._super(); + this.renderElement(); + this.$('.button.fullscreen').off('click').click(function(){ + window.document.body.webkitRequestFullscreen(); + self.pos_widget.screen_selector.close_popup(); + }); + this.$('.button.cancel').off('click').click(function(){ + self.pos_widget.screen_selector.close_popup(); + }); + }, + ismobile: function(){ + return typeof window.orientation !== 'undefined'; + } + }); + + module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({ template:'ChooseReceiptPopupWidget', show: function(){ @@ -348,6 +342,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var self = this; this._super(); + $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>'); + if( text && (text.message || text.comment) ){ this.$('.message').text(text.message); this.$('.comment').text(text.comment); @@ -365,6 +361,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, }); + module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({ + template:'ErrorTracebackPopupWidget', + }); module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({ template:'ErrorSessionPopupWidget', @@ -375,6 +374,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show: function(barcode){ this._super(); this.$('.barcode').text(barcode); + }, }); @@ -404,14 +404,22 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, }); - module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({ - template: 'ErrorNoClientPopupWidget', - }); - module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({ template: 'ErrorInvoiceTransferPopupWidget', }); + module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({ + template: 'UnsentOrdersPopupWidget', + show: function(options){ + var self = this; + this._super(options); + this.renderElement(); + this.$('.button.confirm').click(function(){ + self.pos_widget.screen_selector.close_popup(); + }); + }, + }); + module.ScaleScreenWidget = module.ScreenWidget.extend({ template:'ScaleScreenWidget', @@ -557,6 +565,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ClientListScreenWidget = module.ScreenWidget.extend({ template: 'ClientListScreenWidget', + init: function(parent, options){ + this._super(parent, options); + this.partner_cache = new module.DomCache(); + }, + show_leftpane: false, auto_back: true, @@ -587,7 +600,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa } this.$('.client-list-contents').delegate('.client-line','click',function(event){ - console.log('uh'); self.line_select(event,$(this),parseInt($(this).data('id'))); }); @@ -628,14 +640,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$('.searchbox input').focus(); }, render_list: function(partners){ - var contents = this.$('.client-list-contents'); - contents.empty(); + var contents = this.$el[0].querySelector('.client-list-contents'); + contents.innerHtml = ""; for(var i = 0, len = partners.length; i < len; i++){ - var clientline = $(QWeb.render('ClientLine',{partner:partners[i]})); - if( partners[i] === this.new_client ){ - clientline.addClass('highlight'); + var partner = partners[i]; + var clientline = this.partner_cache.get_node(partner.id); + if(!clientline){ + var clientline_html = QWeb.render('ClientLine',{partner:partners[i]}); + var clientline = document.createElement('tbody'); + clientline.innerHTML = clientline_html; + clientline = clientline.childNodes[1]; + this.partner_cache.cache_node(partner.id,clientline); + } + if( partners === this.new_client ){ + clientline.classList.add('highlight'); + }else{ + clientline.classList.remove('highlight'); } - contents.append(clientline); + contents.appendChild(clientline); } }, save_changes: function(){ @@ -730,30 +752,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ReceiptScreenWidget = module.ScreenWidget.extend({ template: 'ReceiptScreenWidget', - - show_numpad: true, - show_leftpane: true, + show_numpad: false, + show_leftpane: false, show: function(){ this._super(); var self = this; - var print_button = this.add_action_button({ - label: _t('Print'), - icon: '/point_of_sale/static/src/img/icons/png48/printer.png', - click: function(){ self.print(); }, - }); - - var finish_button = this.add_action_button({ - label: _t('Next Order'), - icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', - click: function() { self.finishOrder(); }, - }); - this.refresh(); this.print(); - // // The problem is that in chrome the print() is asynchronous and doesn't // execute until all rpc are finished. So it conflicts with the rpc used // to send the orders to the backend, and the user is able to go to the next @@ -770,372 +778,345 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // 2 seconds is the same as the default timeout for sending orders and so the dialog // should have appeared before the timeout... so yeah that's not ultra reliable. - finish_button.set_disabled(true); + this.lock_screen(true); setTimeout(function(){ - finish_button.set_disabled(false); + self.lock_screen(false); }, 2000); }, + lock_screen: function(locked) { + this._locked = locked; + if (locked) { + this.$('.next').removeClass('highlight'); + } else { + this.$('.next').addClass('highlight'); + } + }, print: function() { window.print(); }, - finishOrder: function() { - this.pos.get('selectedOrder').destroy(); + finish_order: function() { + if (!this._locked) { + this.pos.get_order().finalize(); + } + }, + renderElement: function() { + var self = this; + this._super(); + this.$('.next').click(function(){ + self.finish_order(); + }); + this.$('.button.print').click(function(){ + self.print(); + }); }, refresh: function() { - var order = this.pos.get('selectedOrder'); - $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{ + var order = this.pos.get_order(); + this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{ widget:this, order: order, orderlines: order.get('orderLines').models, paymentlines: order.get('paymentLines').models, })); }, - close: function(){ - this._super(); - } }); - module.PaymentScreenWidget = module.ScreenWidget.extend({ - template: 'PaymentScreenWidget', - back_screen: 'products', - next_screen: 'receipt', + template: 'PaymentScreenWidget', + back_screen: 'product', + next_screen: 'receipt', + show_leftpane: false, + show_numpad: false, init: function(parent, options) { var self = this; - this._super(parent,options); + this._super(parent, options); this.pos.bind('change:selectedOrder',function(){ - this.bind_events(); this.renderElement(); + this.watch_order_changes(); },this); + this.watch_order_changes(); - 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) + this.inputbuffer = ""; + this.firstinput = true; + this.keyboard_handler = function(event){ + var key = ''; + if ( event.keyCode === 13 ) { // Enter + self.validate_order(); + } else if ( event.keyCode === 190 ) { // Dot + key = '.'; + } else if ( event.keyCode === 46 ) { // Delete + key = 'CLEAR'; + } else if ( event.keyCode === 8 ) { // Backspace + key = 'BACKSPACE'; + event.preventDefault(); // Prevents history back nav + } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers + key = '' + (event.keyCode - 48); + } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers + key = '' + (event.keyCode - 96); + } else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus + key = '-'; + } else if ( event.keyCode === 107 ) { // Plus + key = '+'; } - 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); - } - }; + self.payment_input(key); - this.line_click_handler = function(event){ - var node = this; - while(node && !node.classList.contains('paymentline')){ - node = node.parentNode; - } - if(node){ - self.pos.get('selectedOrder').selectPaymentline(node.line); - } }; - - this.hotkey_handler = function(event){ - if(event.which === 13){ - self.validate_order(); - }else if(event.which === 27){ - self.back(); - } - }; - }, - show: function(){ - this._super(); - var self = this; - - this.enable_numpad(); - this.focus_selected_line(); - - document.body.addEventListener('keyup', this.hotkey_handler); - - this.add_action_button({ - label: _t('Back'), - icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', - click: function(){ - self.back(); - }, - }); - - this.add_action_button({ - label: _t('Validate'), - name: 'validation', - icon: '/point_of_sale/static/src/img/icons/png48/validate.png', - click: function(){ - self.validate_order(); - }, - }); - - if( this.pos.config.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_order({invoice: true}); - }, - }); + // resets the current input buffer + reset_input: function(){ + var line = this.pos.get_order().selected_paymentline; + this.firstinput = true; + if (line) { + this.inputbuffer = this.format_currency_no_symbol(line.get_amount()); + } else { + this.inputbuffer = ""; } - - if( this.pos.config.iface_cashdrawer ){ - this.add_action_button({ - label: _t('Cash'), - name: 'cashbox', - icon: '/point_of_sale/static/src/img/open-cashbox.png', - click: function(){ - self.pos.proxy.open_cashbox(); - }, - }); + }, + // handle both keyboard and numpad input. Accepts + // a string that represents the key pressed. + payment_input: function(input) { + var oldbuf = this.inputbuffer.slice(0); + + if (input === '.') { + if (this.firstinput) { + this.inputbuffer = "0."; + }else if (!this.inputbuffer.length || this.inputbuffer === '-') { + this.inputbuffer += "0."; + } else if (this.inputbuffer.indexOf('.') < 0){ + this.inputbuffer = this.inputbuffer + '.'; + } + } else if (input === 'CLEAR') { + this.inputbuffer = ""; + } else if (input === 'BACKSPACE') { + this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1); + } else if (input === '+') { + if ( this.inputbuffer[0] === '-' ) { + this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length); + } + } else if (input === '-') { + if ( this.inputbuffer[0] === '-' ) { + this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length); + } else { + this.inputbuffer = '-' + this.inputbuffer; + } + } else if (input[0] === '+' && !isNaN(parseFloat(input))) { + this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input)); + } else if (!isNaN(parseInt(input))) { + if (this.firstinput) { + this.inputbuffer = '' + input; + } else { + this.inputbuffer += input; + } } - this.update_payment_summary(); + this.firstinput = false; - }, - close: function(){ - this._super(); - this.disable_numpad(); - document.body.removeEventListener('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); + if (this.inputbuffer !== oldbuf) { + var order = this.pos.get_order(); + if (order.selected_paymentline) { + order.selected_paymentline.set_amount(parseFloat(this.inputbuffer)); + this.order_changes(); + this.render_paymentlines(); + this.$('.paymentline.selected .edit').text(this.inputbuffer); } } }, - back: function() { - this.remove_empty_lines(); - this.pos_widget.screen_selector.set_current_screen(this.back_screen); + click_numpad: function(button) { + this.payment_input(button.data('action')); }, - 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); + render_numpad: function() { + var self = this; + var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this })); + numpad.on('click','button',function(){ + self.click_numpad($(this)); + }); + return numpad; + }, + click_delete_paymentline: function(cid){ + var lines = this.pos.get_order().get('paymentLines').models; + for ( var i = 0; i < lines.length; i++ ) { + if (lines[i].cid === cid) { + this.pos.get_order().removePaymentline(lines[i]); + this.reset_input(); + this.render_paymentlines(); + return; + } } - 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){ - var input = line.node.querySelector('input'); - if(!input){ + click_paymentline: function(cid){ + var lines = this.pos.get_order().get('paymentLines').models; + for ( var i = 0; i < lines.length; i++ ) { + if (lines[i].cid === cid) { + this.pos.get_order().selectPaymentline(lines[i]); + this.reset_input(); + this.render_paymentlines(); return; } - var value = input.value; - input.focus(); + } + }, + render_paymentlines: function() { + var self = this; + var order = this.pos.get_order(); + var lines = order.get('paymentLines').models; - if(this.numpad_state){ - this.numpad_state.reset(); - } + this.$('.paymentlines-container').empty(); + var lines = $(QWeb.render('PaymentScreen-Paymentlines', { + widget: this, + order: order, + paymentlines: lines, + })); - if(Number(value) === 0){ - input.value = ''; - }else{ - input.value = value; - input.select(); + lines.on('click','.delete-button',function(){ + self.click_delete_paymentline($(this).data('cid')); + }); + + lines.on('click','.paymentline',function(){ + self.click_paymentline($(this).data('cid')); + }); + + lines.appendTo(this.$('.paymentlines-container')); + }, + click_paymentmethods: function(id) { + var cashregister = null; + for ( var i = 0; i < this.pos.cashregisters.length; i++ ) { + if ( this.pos.cashregisters[i].journal_id[0] === id ){ + cashregister = this.pos.cashregisters[i]; + break; } } + this.pos.get_order().addPaymentline( cashregister ); + this.reset_input(); + this.render_paymentlines(); }, - 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_paymentmethods: function() { + var self = this; + var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this })); + methods.on('click','.paymentmethod',function(){ + self.click_paymentmethods($(this).data('id')); + }); + return methods; + }, + click_invoice: function(){ + var order = this.pos.get_order(); + order.set_to_invoice(!order.is_to_invoice()); + if (order.is_to_invoice()) { + this.$('.js_invoice').addClass('highlight'); + } else { + this.$('.js_invoice').removeClass('highlight'); } }, - render_paymentline: function(line){ - var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line}); - el_html = _.str.trim(el_html); + renderElement: function() { + var self = this; + this._super(); - 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); + var numpad = this.render_numpad(); + numpad.appendTo(this.$('.payment-numpad')); - line.node = el_node; + var methods = this.render_paymentmethods(); + methods.appendTo(this.$('.paymentmethods-container')); + + this.render_paymentlines(); + + this.$('.back').click(function(){ + self.pos_widget.screen_selector.back(); + }); + + this.$('.next').click(function(){ + self.validate_order(); + }); + + this.$('.js_invoice').click(function(){ + self.click_invoice(); + }); - 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); }, - remove_paymentline: function(line){ - line.node.parentNode.removeChild(line.node); - line.node = undefined; + show: function(){ + this.pos.get_order().clean_empty_paymentlines(); + this.reset_input(); + this.render_paymentlines(); + this.order_changes(); + window.document.body.addEventListener('keydown',this.keyboard_handler); + this._super(); }, - renderElement: function(){ + hide: function(){ + window.document.body.removeEventListener('keydown',this.keyboard_handler); 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()); + }, + // sets up listeners to watch for order changes + watch_order_changes: function() { + var self = this; + var order = this.pos.get_order(); + if(this.old_order){ + this.old_order.unbind(null,null,this); } + order.bind('all',function(){ + self.order_changes(); + }); + this.old_order = order; }, - is_paid: function(){ - var currentOrder = this.pos.get('selectedOrder'); - return (currentOrder.getTotalTaxIncluded() < 0.000001 - || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); - + // called when the order is changed, used to show if + // the order is paid or not + order_changes: function(){ + var self = this; + var order = this.pos.get_order(); + if (order.isPaid()) { + self.$('.next').addClass('highlight'); + }else{ + self.$('.next').removeClass('highlight'); + } }, - validate_order: function(options) { + // Check if the order is paid, then sends it to the backend, + // and complete the sale process + validate_order: function() { var self = this; - options = options || {}; - var currentOrder = this.pos.get('selectedOrder'); + var order = this.pos.get_order(); - if(!this.is_paid()){ + if (!order.isPaid() || this.invoicing) { return; } - if( this.pos.config.iface_cashdrawer - && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ - return pl.cashregister.journal.type === 'cash'; - })){ + if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) { this.pos.proxy.open_cashbox(); } - if(options.invoice){ - // deactivate the validation button while we try to send the order - this.pos_widget.action_bar.set_button_disabled('validation',true); - this.pos_widget.action_bar.set_button_disabled('invoice',true); - - var invoiced = this.pos.push_and_invoice_order(currentOrder); + if (order.is_to_invoice()) { + var invoiced = this.pos.push_and_invoice_order(order); + this.invoicing = true; invoiced.fail(function(error){ - if(error === 'error-no-client'){ - self.pos_widget.screen_selector.show_popup('error-no-client'); - }else{ + self.invoicing = false; + if (error === 'error-no-client') { + self.pos_widget.screen_selector.show_popup('confirm',{ + message: _t('Please select the Customer'), + comment: _t('You need to select the customer before you can invoice an order.'), + confirm: function(){ + self.pos_widget.screen_selector.set_current_screen('clientlist'); + }, + }); + } else { self.pos_widget.screen_selector.show_popup('error-invoice-transfer'); } - self.pos_widget.action_bar.set_button_disabled('validation',false); - self.pos_widget.action_bar.set_button_disabled('invoice',false); }); invoiced.done(function(){ - self.pos_widget.action_bar.set_button_disabled('validation',false); - self.pos_widget.action_bar.set_button_disabled('invoice',false); - self.pos.get('selectedOrder').destroy(); + self.invoicing = false; + order.finalize(); }); - - }else{ - this.pos.push_order(currentOrder) - if(this.pos.config.iface_print_via_proxy){ + } else { + this.pos.push_order(order) + if (this.pos.config.iface_print_via_proxy) { var receipt = currentOrder.export_for_printing(); this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{ receipt: receipt, widget: self, })); - this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen - }else{ + order.finalize(); //finish order and go back to scan screen + } else { this.pos_widget.screen_selector.set_current_screen(this.next_screen); } } - - // hide onscreen (iOS) keyboard - setTimeout(function(){ - document.activeElement.blur(); - $("input").blur(); - },250); - }, - 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); - } - - }, - 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) { - 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/tests.js b/addons/point_of_sale/static/src/js/tests.js index 018b343de58..08aa6e87213 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -1,93 +1,89 @@ -function openerp_pos_tests(instance, module){ //module is instance.point_of_sale +(function() { + 'use strict'; - // Various UI Tests to measure performance and memory leaks. - module.UiTester = function(){ - var running = false; - var queue = new module.JobQueue(); + openerp.Tour.register({ + id: 'pos_basic_order', + name: 'Complete a basic order trough the Front-End', + path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening', + mode: 'test', + steps: [ + { + title: 'Wait fot the bloody screen to be ready', + wait: 200, + }, + { + title: 'Load the Session', + waitNot: '.oe_loading:visible', + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + { + title: 'Loading the Loading Screen', + waitFor: '.loader' + }, + { + title: 'Waiting for the end of loading...', + waitFor: '.loader:hidden', + }, + { + title: 'Loading The Point of Sale', + waitFor: '.pos', + }, + { + title: 'On va manger des CHIPS!', + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The chips have been added to the Order', + waitFor: '.order .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The order total has been updated to the correct value', + wait: 2000, + waitFor: '.order .total .value:contains("1.48 €")', + }, + { + title: "Let's buy more chips", + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: "Let's veryify we pay the correct price for two bags of chips", + waitFor: '.order .total .value:contains("2.96 €")', + }, + { + title: "Let's pay with a debit card", + element: ".paypad-button:contains('Bank')", + }, + { + title: "Let's accept the payment", + onload: function(){ + // The test cannot validate or cancel the print() ... so we replace it by a noop !. + window._print = window.print; + window.print = function(){ console.log('Print!') }; + }, + element: ".button .iconlabel:contains('Validate'):visible", + }, + { + title: "Let's finish the order", + element: ".button:not(.disabled) .iconlabel:contains('Next'):visible", + }, + { + onload: function(){ + window.print = window._print; + window._print = undefined; + }, + title: "Let's wait for the order posting", + waitFor: ".oe_status.js_synch .js_connected:visible", + }, + { + title: "Let's close the Point of Sale", + element: ".header-button:contains('Close')", + }, + { + title: "Wait for the backend to ready itself", + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + ], + }); - // stop the currently running test - this.stop = function(){ - queue.clear(); - }; +})(); - // randomly switch product categories - this.category_switch = function(interval){ - queue.schedule(function(){ - var breadcrumbs = $('.breadcrumb-button'); - var categories = $('.category-button'); - if(categories.length > 0){ - var rnd = Math.floor(Math.random()*categories.length); - categories.eq(rnd).click(); - }else{ - var rnd = Math.floor(Math.random()*breadcrumbs.length); - breadcrumbs.eq(rnd).click(); - } - },{repeat:true, duration:interval}); - }; - - // randomly order products then resets the order - this.order_products = function(interval){ - - queue.schedule(function(){ - var def = new $.Deferred(); - var order_queue = new module.JobQueue(); - var order_size = 1 + Math.floor(Math.random()*10); - - while(order_size--){ - order_queue.schedule(function(){ - var products = $('.product'); - if(products.length > 0){ - var rnd = Math.floor(Math.random()*products.length); - products.eq(rnd).click(); - } - },{duration:20}); - } - order_queue.finished().then(function(){ - $('.deleteorder-button').click(); - def.resolve(); - }); - return def; - },{repeat:true, duration: interval}); - - }; - - // makes a complete product order cycle ( print via proxy must be activated, and scale deactivated ) - this.full_order_cycle = function(interval){ - queue.schedule(function(){ - var def = new $.Deferred(); - var order_queue = new module.JobQueue(); - var order_size = 1 + Math.floor(Math.random()*50); - - while(order_size--){ - order_queue.schedule(function(){ - var products = $('.product'); - if(products.length > 0){ - var rnd = Math.floor(Math.random()*products.length); - products.eq(rnd).click(); - } - },{duration:50}); - } - order_queue.schedule(function(){ - $('.paypad-button:first').click(); - },{duration:250}); - order_queue.schedule(function(){ - $('.paymentline-input:first').val(10000); - $('.paymentline-input:first').keydown(); - $('.paymentline-input:first').keyup(); - },{duration:250}); - order_queue.schedule(function(){ - $('.pos-actionbar-button-list .button:eq(2)').click(); - },{duration:250}); - order_queue.schedule(function(){ - def.resolve(); - }); - return def; - },{repeat: true, duration: interval}); - }; - }; - - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ - window.pos_test_ui = new module.UiTester(); - } - -} diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index 648ab725f9e..c3f7d2baf5f 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -1,5 +1,7 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of_sale + var round_pr = instance.web.round_precision + // This is a base class for all Widgets in the POS. It exposes relevant data to the // templates : // - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after } @@ -27,6 +29,12 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10))); + this.format_currency_no_symbol = function(amount){ + amount = round_pr(amount,this.currency.rounding); + amount = amount.toFixed(decimals); + return amount; + }; + this.format_currency = function(amount){ if(typeof amount === 'number'){ amount = Math.round(amount*100)/100; @@ -37,7 +45,7 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of }else{ return this.currency.symbol + ' ' + amount; } - } + }; }, show: function(){ diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 5db12165737..b5db7ee47e1 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -83,44 +83,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - // The paypad allows to select the payment method (cashregisters) - // used to pay the order. - module.PaypadWidget = module.PosBaseWidget.extend({ - template: 'PaypadWidget', + // The action pads contains the payment button and the customer selection button. + module.ActionpadWidget = module.PosBaseWidget.extend({ + template: 'ActionpadWidget', renderElement: function() { var self = this; this._super(); - - _.each(this.pos.cashregisters,function(cashregister) { - var button = new module.PaypadButtonWidget(self,{ - pos: self.pos, - pos_widget : self.pos_widget, - cashregister: cashregister, - }); - button.appendTo(self.$el); + this.$('.pay').click(function(){ + self.pos.pos_widget.screen_selector.set_current_screen('payment'); }); - } - }); - - module.PaypadButtonWidget = module.PosBaseWidget.extend({ - template: 'PaypadButtonWidget', - init: function(parent, options){ - this._super(parent, options); - this.cashregister = options.cashregister; - }, - renderElement: function() { - var self = this; - this._super(); - - this.$el.click(function(){ - if (self.pos.get('selectedOrder').get('screen') === 'receipt'){ //TODO Why ? - console.warn('TODO should not get there...?'); - return; - } - self.pos.get('selectedOrder').addPaymentline(self.cashregister); - self.pos_widget.screen_selector.set_current_screen('payment'); + this.$('.set-customer').click(function(){ + self.pos.pos_widget.screen_selector.set_current_screen('clientlist'); }); - }, + } }); module.OrderWidget = module.PosBaseWidget.extend({ @@ -318,90 +293,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); - module.ActionButtonWidget = instance.web.Widget.extend({ - template:'ActionButtonWidget', - icon_template:'ActionButtonWidgetWithIcon', - init: function(parent, options){ - this._super(parent, options); - this.label = options.label || 'button'; - this.rightalign = options.rightalign || false; - this.click_action = options.click; - this.disabled = options.disabled || false; - if(options.icon){ - this.icon = options.icon; - this.template = this.icon_template; - } - }, - set_disabled: function(disabled){ - if(this.disabled != disabled){ - this.disabled = !!disabled; - this.renderElement(); - } - }, - renderElement: function(){ - this._super(); - if(this.click_action && !this.disabled){ - this.$el.click(_.bind(this.click_action, this)); - } - }, - }); - - module.ActionBarWidget = instance.web.Widget.extend({ - template:'ActionBarWidget', - init: function(parent, options){ - this._super(parent,options); - this.button_list = []; - this.buttons = {}; - this.visibility = {}; - }, - set_element_visible: function(element, visible, action){ - if(visible != this.visibility[element]){ - this.visibility[element] = !!visible; - if(visible){ - this.$('.'+element).removeClass('oe_hidden'); - }else{ - this.$('.'+element).addClass('oe_hidden'); - } - } - if(visible && action){ - this.action[element] = action; - this.$('.'+element).off('click').click(action); - } - }, - set_button_disabled: function(name, disabled){ - var b = this.buttons[name]; - if(b){ - b.set_disabled(disabled); - } - }, - destroy_buttons:function(){ - for(var i = 0; i < this.button_list.length; i++){ - this.button_list[i].destroy(); - } - this.button_list = []; - this.buttons = {}; - return this; - }, - get_button_count: function(){ - return this.button_list.length; - }, - add_new_button: function(button_options){ - var button = new module.ActionButtonWidget(this,button_options); - this.button_list.push(button); - if(button_options.name){ - this.buttons[button_options.name] = button; - } - button.appendTo(this.$('.pos-actionbar-button-list')); - return button; - }, - show:function(){ - this.$el.removeClass('oe_hidden'); - }, - hide:function(){ - this.$el.addClass('oe_hidden'); - }, - }); - module.ProductCategoriesWidget = module.PosBaseWidget.extend({ template: 'ProductCategoriesWidget', init: function(parent, options){ @@ -797,6 +688,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.$('.button.reference').click(function(){ self.pos.barcode_reader.scan(self.$('input.ean').val()); }); + this.$('.button.show_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('unsent-orders'); + }); + this.$('.button.delete_orders').click(function(){ + self.pos.pos_widget.screen_selector.show_popup('confirm',{ + message: _t('Delete Unsent Orders ?'), + comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'), + confirm: function(){ + self.pos.db.remove_all_orders(); + self.pos.set({synch: { state:'connected', pending: 0 }}); + }, + }); + }); _.each(this.eans, function(ean, name){ self.$('.button.'+name).click(function(){ self.$('input.ean').val(ean); @@ -840,7 +744,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.set_status(synch.state, synch.pending); }); this.$el.click(function(){ - self.pos.flush(); + self.pos.push_order(); }); }, }); @@ -903,7 +807,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa // - a header, containing the list of orders // - a leftpane, containing the list of bought products (orderlines) // - a rightpane, containing the screens (see pos_screens.js) - // - an actionbar on the bottom, containing various action buttons // - popups // - an onscreen keyboard // a screen_selector which controls the switching between screens and the showing/closing of popups @@ -1001,23 +904,50 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.screen_selector.show_popup('error', 'Sorry, we could not create a user session'); }else if(!self.pos.config){ self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session'); + }else if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && ( + window.screen.availWidth > window.innerWidth || + window.screen.availHeight > window.innerHeight )){ + self.screen_selector.show_popup('fullscreen'); } self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');}); - self.pos.flush(); - - }).fail(function(){ // error when loading models data from the backend - return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id']) - .pipe( _.bind(function(res){ - return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']}) - .pipe(_.bind(function(result){ - var action = result.result; - this.do_action(action); - }, this)); - }, self)); + self.pos.push_order(); + + }).fail(function(err){ // error when loading models data from the backend + self.loading_error(err); }); }, + loading_error: function(err){ + var self = this; + + var message = err.message; + var comment = err.stack; + + if(err.message === 'XmlHttpRequestError '){ + message = 'Network Failure (XmlHttpRequestError)'; + comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.'; + }else if(err.message === 'OpenERP Server Error'){ + message = err.data.message; + comment = err.data.debug; + } + + if( typeof comment !== 'string' ){ + comment = 'Traceback not available.'; + } + + var popup = $(QWeb.render('ErrorTracebackPopupWidget',{ + widget: { message: message, comment: comment }, + })); + + popup.find('.button').click(function(){ + self.close(); + }); + + popup.css({ zindex: 9001 }); + + popup.appendTo(this.$el); + }, loading_progress: function(fac){ this.$('.loader .loader-feedback').removeClass('oe_hidden'); this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'}); @@ -1076,15 +1006,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {}); this.choose_receipt_popup.appendTo(this.$el); - this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {}); - this.error_no_client_popup.appendTo(this.$el); - this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {}); this.error_invoice_transfer_popup.appendTo(this.$el); + this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{}); + this.error_traceback_popup.appendTo(this.$el); + this.confirm_popup = new module.ConfirmPopupWidget(this,{}); this.confirm_popup.appendTo(this.$el); + this.fullscreen_popup = new module.FullscreenPopup(this,{}); + this.fullscreen_popup.appendTo(this.$el); + + this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{}); + this.unsent_orders_popup.appendTo(this.$el); + // -------- Misc --------- this.close_button = new module.HeaderButtonWidget(this,{ @@ -1104,11 +1040,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.username = new module.UsernameWidget(this,{}); this.username.replace(this.$('.placeholder-UsernameWidget')); - this.action_bar = new module.ActionBarWidget(this); - this.action_bar.replace(this.$(".placeholder-RightActionBar")); - - this.paypad = new module.PaypadWidget(this, {}); - this.paypad.replace(this.$('.placeholder-PaypadWidget')); + this.actionpad = new module.ActionpadWidget(this, {}); + this.actionpad.replace(this.$('.placeholder-ActionpadWidget')); this.numpad = new module.NumpadWidget(this); this.numpad.replace(this.$('.placeholder-NumpadWidget')); @@ -1137,9 +1070,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'error-barcode': this.error_barcode_popup, 'error-session': this.error_session_popup, 'choose-receipt': this.choose_receipt_popup, - 'error-no-client': this.error_no_client_popup, 'error-invoice-transfer': this.error_invoice_transfer_popup, + 'error-traceback': this.error_traceback_popup, 'confirm': this.confirm_popup, + 'fullscreen': this.fullscreen_popup, + 'unsent-orders': this.unsent_orders_popup, }, default_screen: 'products', default_mode: 'cashier', @@ -1164,10 +1099,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.numpad_visible = visible; if(visible){ this.numpad.show(); - this.paypad.show(); + this.actionpad.show(); }else{ this.numpad.hide(); - this.paypad.hide(); + this.actionpad.hide(); } } }, diff --git a/addons/point_of_sale/static/src/sounds/error.wav b/addons/point_of_sale/static/src/sounds/error.wav Binary files differnew file mode 100644 index 00000000000..472f3910719 --- /dev/null +++ b/addons/point_of_sale/static/src/sounds/error.wav diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 929212cce8d..77dea42c5db 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -36,19 +36,12 @@ <div class='subwindow-container'> <div class='subwindow-container-fix pads'> <div class="control-buttons oe_hidden"></div> - <div class="placeholder-PaypadWidget"></div> + <div class="placeholder-ActionpadWidget"></div> <div class="placeholder-NumpadWidget"></div> </div> </div> </div> - <div class='subwindow collapsed'> - <div class='subwindow-container'> - <div class='subwindow-container-fix'> - <div class='placeholder-LeftActionBar'></div> - </div> - </div> - </div> </div> </div> @@ -60,14 +53,6 @@ </div> </div> </div> - - <div class='subwindow collapsed'> - <div class='subwindow-container'> - <div class='subwindow-container-fix'> - <div class='placeholder-RightActionBar'></div> - </div> - </div> - </div> </div> </div> @@ -144,8 +129,14 @@ <div>There are pending operations that could not be saved into the database, are you sure you want to exit?</div> </t> - <t t-name="PaypadWidget"> - <div class="paypad touch-scrollable"> + <t t-name="ActionpadWidget"> + <div class='actionpad'> + <button class='button set-customer'> + <i class='fa fa-user' /> Set Customer + </button> + <button class="button pay"> + <i class='fa fa-chevron-right' /> Payment + </button> </div> </t> @@ -383,41 +374,179 @@ </div> </t> - + <t t-name="PaymentScreen-Paymentlines"> + <t t-if="!paymentlines.length"> + <div class='paymentlines-empty'> + <div class='total'> + <t t-esc="widget.format_currency(order.getTotalTaxIncluded())"/> + </div> + <div class='message'> + Please select a payment method. + </div> + </div> + </t> + + <t t-if="paymentlines.length"> + <table class='paymentlines'> + <colgroup> + <col class='due' /> + <col class='tendered' /> + <col class='change' /> + <col class='method' /> + <col class='controls' /> + </colgroup> + <thead> + <tr class='label'> + <th>Due</th> + <th>Tendered</th> + <th>Change</th> + <th>Method</th> + <th></th> + </tr> + </thead> + <tbody> + <t t-foreach='paymentlines' t-as='line'> + <t t-if='line.selected'> + <tr class='paymentline selected'> + <td class='col-due'> <t t-esc='widget.format_currency_no_symbol(order.getDueLeft(line))' /> </td> + <td class='col-tendered edit'> + <t t-esc='widget.inputbuffer' /> + <!-- <t t-esc='line.get_amount()' /> --> + </td> + <t t-if='order.getChange(line)'> + <td class='col-change highlight' > + <t t-esc='widget.format_currency_no_symbol(order.getChange(line))' /> + </td> + </t> + <t t-if='!order.getChange(line)'> + <td class='col-change' ></td> + </t> + + <td class='col-name' > <t t-esc='line.name' /> </td> + <td class='delete-button' t-att-data-cid='line.cid'> <i class='fa fa-times-circle' /> </td> + </tr> + </t> + <t t-if='!line.selected'> + <tr class='paymentline' t-att-data-cid='line.cid'> + <td class='col-due'> <t t-esc='widget.format_currency_no_symbol(order.getDueLeft(line))' /> </td> + <td class='col-tendered'> <t t-esc='widget.format_currency_no_symbol(line.get_amount())' /> </td> + <td class='col-change'> + <t t-if='order.getChange(line)'> + <t t-esc='widget.format_currency_no_symbol(order.getChange(line))' /> + </t> + </td> + <td class='col-name'> <t t-esc='line.name' /> </td> + <td class='delete-button' t-att-data-cid='line.cid'> <i class='fa fa-times-circle' /> </td> + </tr> + </t> + </t> + </tbody> + </table> + </t> + + </t> + + <t t-name="PaymentScreen-Numpad"> + <div class="numpad"> + <button class="input-button number-char" data-action='1'>1</button> + <button class="input-button number-char" data-action='2'>2</button> + <button class="input-button number-char" data-action='3'>3</button> + <button class="mode-button" data-action='+10'>+10</button> + <br /> + <button class="input-button number-char" data-action='4'>4</button> + <button class="input-button number-char" data-action='5'>5</button> + <button class="input-button number-char" data-action='6'>6</button> + <button class="mode-button" data-action='+20'>+20</button> + <br /> + <button class="input-button number-char" data-action='7'>7</button> + <button class="input-button number-char" data-action='8'>8</button> + <button class="input-button number-char" data-action='9'>9</button> + <button class="mode-button" data-action='+50'>+50</button> + <br /> + <button class="input-button numpad-char" data-action='CLEAR' >C</button> + <button class="input-button number-char" data-action='0'>0</button> + <button class="input-button number-char" data-action='.'>.</button> + <button class="input-button numpad-backspace" data-action='BACKSPACE' > + <img src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" /> + </button> + <br /> + </div> + </t> + <t t-name="PaymentScreen-Paymentmethods"> + <div class='paymentmethods'> + <t t-foreach="widget.pos.cashregisters" t-as="cashregister"> + <div class="paymentmethod" t-att-data-id="cashregister.journal_id[0]"> + <t t-esc="cashregister.journal_id[1]" /> + </div> + </t> + </div> + </t> + <t t-name="PaymentScreenWidget"> - <div class="payment-screen screen touch-scrollable"> - <div class="pos-payment-container"> - <div class='payment-due-total'></div> - <div class='payment-lines'></div> - <div class='payment-info'> - <div class="infoline"> - <span class='left-block'> - Paid: - </span> - <span class="right-block payment-paid-total"></span> - </div> - <div class="infoline"> - <span class='left-block'> - Remaining: - </span> - <span class="right-block payment-remaining"></span> + <div class='payment-screen screen'> + <div class='screen-content'> + <div class='top-content'> + <span class='button back'> + <i class='fa fa-angle-double-left'></i> + Back + </span> + <h1>Payment</h1> + <span class='button next'> + Validate + <i class='fa fa-angle-double-right'></i> + </span> + </div> + <div class='left-content pc40 touch-scrollable scrollable-y'> + + <div class='paymentmethods-container'> </div> - <div class="infoline bigger" > - <span class='left-block'> - Change: - </span> - <span class="right-block payment-change"></span> + + </div> + <div class='right-content pc60 touch-scrollable scrollable-y'> + + <section class='paymentlines-container'> + </section> + + <section class='payment-numpad'> + </section> + + <div class='payment-buttons'> + <t t-if='widget.pos.config.iface_invoicing'> + <div t-attf-class='button js_invoice #{ widget.pos.get_order().is_to_invoice() ? "highlight" : ""} '> + <i class='fa fa-file-text-o' /> Invoice + </div> + </t> + <t t-if='widget.pos.config.iface_cashdrawer'> + <div class='button js_invoice'> + <i class='fa fa-archive' /> Open Cashbox + </div> + </t> </div> + + </div> </div> </div> + </t> <t t-name="ReceiptScreenWidget"> - <div class="receipt-screen screen touch-scrollable" > - <div class="pos-step-container"> - <div class="pos-receipt-container"> + <div class='receipt-screen screen'> + <div class='screen-content'> + <div class='top-content'> + <h1>Receipt</h1> + <span class='button next'> + Next Order + <i class='fa fa-angle-double-right'></i> + </span> + </div> + <div class="centered-content"> + <div class="button print"> + <i class='fa fa-print'></i> Print + </div> + <div class="pos-receipt-container"> + </div> </div> </div> </div> @@ -586,6 +715,51 @@ </div> </t> + <t t-name="FullscreenPopupWidget"> + <div class="modal-dialog"> + <div class="popup popup-fullscreen"> + <p class="message">Fullscreen Setup</p> + + <t t-if='widget.ismobile()'> + <p class="comment"> + The best way to make the point of sale fullscreen on mobile + devices is to add the point of sale to your home screen. On + iPhone and iPad this is done by tapping <img src='/point_of_sale/static/src/img/ios-share-icon.png' /> + and then <i>Add to Homescreen</i> + </p> + <p class='comment'> + This also works on Android with the Chrome Beta Browser, using the <i>Add to Homescreen</i> option + in the browser's menu. + </p> + <p class='comment'> + If you want to work in fullscreen just this time tap the <i>Go Fullscreen</i> button. + </p> + </t> + + <t t-if='!widget.ismobile()'> + <p class="comment"> + The best way to make the point of sale fullscreen on desktop + and laptops is to launch your browser in kiosk mode. Please + refer to your browser's documentation for the specific + instructions. + </p> + <p class="comment"> + If you want to work in fullscreen just this time, click the <i> Go Fullscreen</i> button. + </p> + </t> + + <div class="footer"> + <div class="button fullscreen"> + Go Fullscreen + </div> + <div class="button cancel"> + Cancel + </div> + </div> + </div> + </div> + </t> + <t t-name="ErrorSessionPopupWidget"> <div class="modal-dialog"> <div class="popup popup-nosession"> @@ -594,10 +768,11 @@ </div> </t> - <t t-name="ErrorNoClientPopupWidget"> + <t t-name="ErrorInvoiceTransferPopupWidget"> <div class="modal-dialog"> - <div class="popup popup-noclient"> - <p class="message">An anonymous order cannot be invoiced</p> + <div class="popup popup-invoice"> + <p class="message">The Order could not be sent to the server for invoicing. Invoices cannot be generated + in offline mode. Please check your internet connection and try again.</p> <div class="footer"> <div class="button"> Ok @@ -607,11 +782,11 @@ </div> </t> - <t t-name="ErrorInvoiceTransferPopupWidget"> + <t t-name="ErrorPopupWidget"> <div class="modal-dialog"> - <div class="popup popup-invoice"> - <p class="message">The Order could not be sent to the server for invoicing. Invoices cannot be generated - in offline mode. Please check your internet connection and try again.</p> + <div class="popup popup-error"> + <p class="message"><t t-esc=" widget.message || 'Error' " /></p> + <p class="comment"><t t-esc=" widget.comment || '' "/></p> <div class="footer"> <div class="button"> Ok @@ -621,11 +796,11 @@ </div> </t> - <t t-name="ErrorPopupWidget"> + <t t-name="ErrorTracebackPopupWidget"> <div class="modal-dialog"> <div class="popup popup-error"> <p class="message"><t t-esc=" widget.message || 'Error' " /></p> - <p class="comment"><t t-esc=" widget.comment || '' "/></p> + <p class="comment traceback"><t t-esc=" widget.comment || '' "/></p> <div class="footer"> <div class="button"> Ok @@ -672,6 +847,29 @@ </div> </t> + <t t-name="UnsentOrdersPopupWidget"> + <div class="modal-dialog"> + <div class="popup popup-unsent-orders"> + <p class="message">Unsent Orders</p> + <t t-if='widget.pos.db.get_orders().length === 0'> + <p class='comment'> + There are no unsent orders + </p> + </t> + <t t-if='widget.pos.db.get_orders().length > 0'> + <p class='comment traceback'> + <t t-esc='JSON.stringify(widget.pos.db.get_orders(),null,2)' /> + </p> + </t> + <div class="footer"> + <div class="button confirm"> + Ok + </div> + </div> + </div> + </div> + </t> + <t t-name="Product"> <span class='product' t-att-data-product-id="product.id"> <div class="product-img"> @@ -779,6 +977,12 @@ <li class="button reference">Reference</li> </ul> + <p class="category">Unsent Orders</p> + <ul> + <li class="button show_orders">Show All Unsent Orders</li> + <li class="button delete_orders">Delete All Unsent Orders</li> + </ul> + <p class="category">Hardware Status</p> <ul> <li class="status weighting">Weighting</li> @@ -818,12 +1022,6 @@ </tr> </t> - <t t-name="PaypadButtonWidget"> - <button class="paypad-button" t-att-cash-register-id="widget.cashregister.id"> - <t t-esc="widget.cashregister.journal.name"/> - </button> - </t> - <t t-name="OrderButtonWidget"> <span class="order-button select-order"> <t t-if='widget.selected'> @@ -944,30 +1142,6 @@ </div> </t> - <t t-name="ActionBarWidget"> - <div class="pos-actionbar"> - <ul class="pos-actionbar-button-list"> - </ul> - </div> - </t> - - <t t-name="ActionButtonWidget"> - <li t-att-class=" 'button '+ (widget.rightalign ? 'rightalign ' : '') + (widget.disabled ? 'disabled ' : '')"> - <div class='label'> - <t t-esc="widget.label" /> - </div> - </li> - </t> - - <t t-name="ActionButtonWidgetWithIcon"> - <li t-att-class=" 'button '+ (widget.rightalign ? 'rightalign ' : '') + (widget.disabled ? 'disabled ' : '')"> - <div class='icon'> - <img t-att-src="widget.icon" /> - <div class='iconlabel'><t t-esc="widget.label" /></div> - </div> - </li> - </t> - <!-- Onscreen Keyboard : http://net.tutsplus.com/tutorials/javascript-ajax/creating-a-keyboard-with-css-and-jquery/ --> <t t-name="OnscreenKeyboardFull"> diff --git a/addons/point_of_sale/test/test_frontend.py b/addons/point_of_sale/test/test_frontend.py new file mode 100644 index 00000000000..7e51c5ae643 --- /dev/null +++ b/addons/point_of_sale/test/test_frontend.py @@ -0,0 +1,9 @@ + +import openerp.tests + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) +class TestUi(openerp.tests.HttpCase): + def test_01_pos_basic_order(self): + self.phantom_js("/", "openerp.Tour.run('pos_basic_order', 'test')", "openerp.Tour.tours.pos_basic_order", login="admin") + diff --git a/addons/pos_discount/__init__.py b/addons/pos_discount/__init__.py new file mode 100644 index 00000000000..5487d145a9b --- /dev/null +++ b/addons/pos_discount/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + +import discount + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/pos_discount/__openerp__.py b/addons/pos_discount/__openerp__.py new file mode 100644 index 00000000000..d367db148cd --- /dev/null +++ b/addons/pos_discount/__openerp__.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + + +{ + 'name': 'Point of Sale Discounts', + 'version': '1.0', + 'category': 'Point of Sale', + 'sequence': 6, + 'summary': 'Simple Discounts in the Point of Sale ', + 'description': """ + +======================= + +This module allows the cashier to quickly give a percentage +sale discount to a customer. + +""", + 'author': 'OpenERP SA', + 'depends': ['point_of_sale'], + 'data': [ + 'views/views.xml', + 'views/templates.xml' + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/pos_discount/discount.py b/addons/pos_discount/discount.py new file mode 100644 index 00000000000..f8d24eb638b --- /dev/null +++ b/addons/pos_discount/discount.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + +import logging + +import openerp + +from openerp import tools +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +class pos_config(osv.osv): + _inherit = 'pos.config' + _columns = { + 'discount_pc': fields.float('Discount Percentage', help='The discount percentage'), + 'discount_product_id': fields.many2one('product.product','Discount Product', help='The product used to model the discount'), + } + _defaults = { + 'discount_pc': 10, + } + diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js new file mode 100644 index 00000000000..5eaab362d20 --- /dev/null +++ b/addons/pos_discount/static/src/js/discount.js @@ -0,0 +1,34 @@ +openerp.pos_discount = function(instance){ + var module = instance.point_of_sale; + var round_pr = instance.web.round_precision + var QWeb = instance.web.qweb; + + QWeb.add_template('/pos_discount/static/src/xml/discount.xml'); + + module.PosWidget.include({ + build_widgets: function(){ + var self = this; + this._super(); + + if(!this.pos.config.discount_product_id){ + return; + } + + var discount = $(QWeb.render('DiscountButton')); + + discount.click(function(){ + var order = self.pos.get('selectedOrder'); + var product = self.pos.db.get_product_by_id(self.pos.config.discount_product_id[0]); + var discount = - self.pos.config.discount_pc/ 100.0 * order.getTotalTaxIncluded(); + if( discount < 0 ){ + order.addProduct(product, { price: discount }); + } + }); + + discount.appendTo(this.$('.control-buttons')); + this.$('.control-buttons').removeClass('oe_hidden'); + }, + }); + +}; + diff --git a/addons/pos_discount/static/src/xml/discount.xml b/addons/pos_discount/static/src/xml/discount.xml new file mode 100644 index 00000000000..65a38afb97d --- /dev/null +++ b/addons/pos_discount/static/src/xml/discount.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="DiscountButton"> + <div class='control-button js_discount'> + <i class='fa fa-tag' /> Discount + </div> + </t> + +</templates> diff --git a/addons/pos_discount/views/templates.xml b/addons/pos_discount/views/templates.xml new file mode 100644 index 00000000000..0dbf22eb0fe --- /dev/null +++ b/addons/pos_discount/views/templates.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + + <template id="assets_frontend" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/pos_discount/static/src/js/discount.js"></script> + </xpath> + </template> + + </data> +</openerp> diff --git a/addons/pos_discount/views/views.xml b/addons/pos_discount/views/views.xml new file mode 100644 index 00000000000..3448e0b2b6f --- /dev/null +++ b/addons/pos_discount/views/views.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<openerp> + <data> + + <record model="ir.ui.view" id="view_pos_config_form"> + <field name="name">pos.config.form.view</field> + <field name="model">pos.config</field> + <field name="inherit_id" ref="point_of_sale.view_pos_config_form" /> + <field name="arch" type="xml"> + <xpath expr="//group[@string='Receipt']" position="after"> + <group string="Discounts" col="4" > + <field name='discount_pc' /> + <field name="discount_product_id" /> + </group> + </xpath> + </field> + </record> + + </data> +</openerp> diff --git a/addons/restaurant/__init__.py b/addons/restaurant/__init__.py new file mode 100644 index 00000000000..381469e26fc --- /dev/null +++ b/addons/restaurant/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + +import restaurant + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/restaurant/__openerp__.py b/addons/restaurant/__openerp__.py new file mode 100644 index 00000000000..dbf735b800e --- /dev/null +++ b/addons/restaurant/__openerp__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + + +{ + 'name': 'Restaurant', + 'version': '1.0', + 'category': 'Point of Sale', + 'sequence': 6, + 'summary': 'Restaurant extensions for the Point of Sale ', + 'description': """ + +======================= + +This module adds several restaurant features to the Point of Sale: +- Bill Printing: Allows you to print a receipt before the order is paid +- Bill Splitting: Allows you to split an order into different orders +- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers + +""", + 'author': 'OpenERP SA', + 'depends': ['point_of_sale'], + 'data': [ + 'restaurant_view.xml', + 'security/ir.model.access.csv', + 'views/templates.xml', + ], + 'qweb':[ + 'static/src/xml/multiprint.xml', + 'static/src/xml/splitbill.xml', + 'static/src/xml/printbill.xml', + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/restaurant/restaurant.py b/addons/restaurant/restaurant.py new file mode 100644 index 00000000000..c7d02a336c1 --- /dev/null +++ b/addons/restaurant/restaurant.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + +import logging + +import openerp +from openerp import tools +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +_logger = logging.getLogger(__name__) + +class restaurant_printer(osv.osv): + _name = 'restaurant.printer' + + _columns = { + 'name' : fields.char('Printer Name', size=32, required=True, help='An internal identification of the printer'), + 'proxy_ip': fields.char('Proxy IP Address', size=32, help="The IP Address or hostname of the Printer's hardware proxy"), + 'product_categories_ids': fields.many2many('pos.category','printer_category_rel', 'printer_id','category_id',string='Printed Product Categories'), + } + + _defaults = { + 'name' : 'Printer', + } + +class pos_config(osv.osv): + _inherit = 'pos.config' + _columns = { + 'iface_splitbill': fields.boolean('Bill Splitting', help='Enables Bill Splitting in the Point of Sale'), + 'iface_printbill': fields.boolean('Bill Printing', help='Allows to print the Bill before payment'), + 'printer_ids': fields.many2many('restaurant.printer','pos_config_printer_rel', 'config_id','printer_id',string='Order Printers'), + } + _defaults = { + 'iface_splitbill': False, + 'iface_printbill': False, + } + diff --git a/addons/restaurant/restaurant_view.xml b/addons/restaurant/restaurant_view.xml new file mode 100644 index 00000000000..84adc883b94 --- /dev/null +++ b/addons/restaurant/restaurant_view.xml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<openerp> + <data> + <record model="ir.ui.view" id="view_restaurant_printer_form"> + <field name="name">Order Printer</field> + <field name="model">restaurant.printer</field> + <field name="arch" type="xml"> + <form string="POS Printer" version="7.0"> + <group col="2"> + <field name="name" /> + <field name="proxy_ip" /> + <field name="product_categories_ids" /> + </group> + </form> + </field> + </record> + + <record model="ir.actions.act_window" id="action_restaurant_printer_form"> + <field name="name">Order Printers</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">restaurant.printer</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click to add a Restaurant Order Printer. + </p><p> + Order Printers are used by restaurants and bars to print the + order updates in the kitchen/bar when the waiter updates the order. + </p><p> + Each Order Printer has an IP Address that defines the PosBox/Hardware + Proxy where the printer can be found, and a list of product categories. + An Order Printer will only print updates for prodcuts belonging to one of + its categories. + </p> + </field> + </record> + + <record model="ir.ui.view" id="view_restaurant_printer"> + <field name="name">Order Printers</field> + <field name="model">restaurant.printer</field> + <field name="arch" type="xml"> + <tree string="Restaurant Order Printers"> + <field name="name" /> + <field name="proxy_ip" /> + <field name="product_categories_ids" /> + </tree> + </field> + </record> + + <menuitem + parent="point_of_sale.menu_point_config_product" + action="action_restaurant_printer_form" + id="menu_restaurant_printer_all" + sequence="30" + groups="point_of_sale.group_pos_manager"/> + + <record model="ir.ui.view" id="view_pos_config_form"> + <field name="name">pos.config.form.view.inherit</field> + <field name="model">pos.config</field> + <field name="inherit_id" ref="point_of_sale.view_pos_config_form"></field> + <field name="arch" type="xml"> + <sheet position='inside'> + <group string="Bar & Restaurant" > + <field name="iface_splitbill" /> + <field name="iface_printbill" /> + <field name="printer_ids" /> + </group> + </sheet> + </field> + </record> + + </data> +</openerp> diff --git a/addons/restaurant/security/ir.model.access.csv b/addons/restaurant/security/ir.model.access.csv new file mode 100644 index 00000000000..31c7f956f86 --- /dev/null +++ b/addons/restaurant/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_restaurant_printer,restaurant.printer.user,model_restaurant_printer,point_of_sale.group_pos_user,1,0,0,0 +access_restaurant_printer_manager,restaurant.printer.manager,model_restaurant_printer,point_of_sale.group_pos_manager,1,0,0,0 diff --git a/addons/restaurant/static/src/css/restaurant.css b/addons/restaurant/static/src/css/restaurant.css new file mode 100644 index 00000000000..0ffebfc0a46 --- /dev/null +++ b/addons/restaurant/static/src/css/restaurant.css @@ -0,0 +1,2 @@ +/* --- Restaurant Specific CSS --- */ + diff --git a/addons/restaurant/static/src/js/main.js b/addons/restaurant/static/src/js/main.js new file mode 100644 index 00000000000..4b8d25368a5 --- /dev/null +++ b/addons/restaurant/static/src/js/main.js @@ -0,0 +1,11 @@ +openerp.restaurant = function(instance){ + + var module = instance.point_of_sale; + + openerp_restaurant_multiprint(instance,module); + + openerp_restaurant_splitbill(instance,module); + + openerp_restaurant_printbill(instance,module); + +}; diff --git a/addons/restaurant/static/src/js/multiprint.js b/addons/restaurant/static/src/js/multiprint.js new file mode 100644 index 00000000000..4a86e4e5a31 --- /dev/null +++ b/addons/restaurant/static/src/js/multiprint.js @@ -0,0 +1,216 @@ +function openerp_restaurant_multiprint(instance,module){ + var QWeb = instance.web.qweb; + var _t = instance.web._t; + + module.Printer = instance.web.Class.extend(openerp.PropertiesMixin,{ + init: function(parent,options){ + openerp.PropertiesMixin.init.call(this,parent); + var self = this; + options = options || {}; + var url = options.url || 'http://localhost:8069'; + this.connection = new instance.web.Session(undefined,url, { use_cors: true}); + this.host = url; + this.receipt_queue = []; + }, + print: function(receipt){ + var self = this; + if(receipt){ + this.receipt_queue.push(receipt); + } + var aborted = false; + function send_printing_job(){ + if(self.receipt_queue.length > 0){ + var r = self.receipt_queue.shift(); + self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000}) + .then(function(){ + send_printing_job(); + },function(){ + self.receipt_queue.unshift(r); + }); + } + } + send_printing_job(); + }, + }); + + module.PosModel.prototype.models.push({ + model: 'restaurant.printer', + fields: ['name','proxy_ip','product_categories_ids'], + domain: null, + loaded: function(self,printers){ + var active_printers = {}; + for (var i = 0; i < self.config.printer_ids.length; i++) { + active_printers[self.config.printer_ids[i]] = true; + } + + self.printers = []; + for(var i = 0; i < printers.length; i++){ + if(active_printers[printers[i].id]){ + var printer = new module.Printer(self,{url:'http://'+printers[i].proxy_ip+':8069'}); + printer.config = printers[i]; + self.printers.push(printer); + } + } + }, + }); + + module.Order = module.Order.extend({ + lineResume: function(){ + var resume = {}; + this.get('orderLines').each(function(item){ + var line = item.export_as_JSON(); + if( typeof resume[line.product_id] === 'undefined'){ + resume[line.product_id] = line.qty; + }else{ + resume[line.product_id] += line.qty; + } + }); + return resume; + }, + saveChanges: function(){ + this.old_resume = this.lineResume(); + }, + computeChanges: function(categories){ + var current = this.lineResume(); + var old = this.old_resume || {}; + var json = this.export_as_JSON(); + var add = []; + var rem = []; + + for( product in current){ + if (typeof old[product] === 'undefined'){ + add.push({ + 'id': product, + 'name': this.pos.db.get_product_by_id(product).name, + 'quantity': current[product], + }); + }else if( old[product] < current[product]){ + add.push({ + 'id': product, + 'name': this.pos.db.get_product_by_id(product).name, + 'quantity': current[product] - old[product], + }); + }else if( old[product] > current[product]){ + rem.push({ + 'id': product, + 'name': this.pos.db.get_product_by_id(product).name, + 'quantity': old[product] - current[product], + }); + } + } + + for( product in old){ + if(typeof current[product] === 'undefined'){ + rem.push({ + 'id': product, + 'name': this.pos.db.get_product_by_id(product).name, + 'quantity': old[product], + }); + } + } + + if(categories && categories.length > 0){ + // filter the added and removed orders to only contains + // products that belong to one of the categories supplied as a parameter + + var self = this; + function product_in_category(product_id){ + var cat = self.pos.db.get_product_by_id(product_id).pos_categ_id[0]; + while(cat){ + for(var i = 0; i < categories.length; i++){ + if(cat === categories[i]){ + return true; + } + } + cat = self.pos.db.get_category_parent_id(cat); + } + return false; + } + + var _add = []; + var _rem = []; + + for(var i = 0; i < add.length; i++){ + if(product_in_category(add[i].id)){ + _add.push(add[i]); + } + } + add = _add; + + for(var i = 0; i < rem.length; i++){ + if(product_in_category(rem[i].id)){ + _rem.push(rem[i]); + } + } + rem = _rem; + } + + return { + 'new': add, + 'cancelled': rem, + 'table': json.table || 'unknown table', + 'name': json.name || 'unknown order', + 'sequence_number': json.sequence_number || 0, + }; + + }, + printChanges: function(){ + var printers = this.pos.printers; + for(var i = 0; i < printers.length; i++){ + var changes = this.computeChanges(printers[i].config.product_categories_ids); + if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ + var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this}); + printers[i].print(receipt); + } + } + }, + hasChangesToPrint: function(){ + var printers = this.pos.printers; + for(var i = 0; i < printers.length; i++){ + var changes = this.computeChanges(printers[i].config.product_categories_ids); + if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ + return true; + } + } + return false; + }, + }); + + module.PosWidget.include({ + build_widgets: function(){ + var self = this; + this._super(); + + if(this.pos.printers.length){ + var submitorder = $(QWeb.render('SubmitOrderButton')); + + submitorder.click(function(){ + var order = self.pos.get('selectedOrder'); + if(order.hasChangesToPrint()){ + order.printChanges(); + order.saveChanges(); + self.pos_widget.order_widget.update_summary(); + } + }); + + submitorder.appendTo(this.$('.control-buttons')); + this.$('.control-buttons').removeClass('oe_hidden'); + } + }, + + }); + + module.OrderWidget.include({ + update_summary: function(){ + this._super(); + var order = this.pos.get('selectedOrder'); + + if(order.hasChangesToPrint()){ + this.pos_widget.$('.order-submit').addClass('highlight'); + }else{ + this.pos_widget.$('.order-submit').removeClass('highlight'); + } + }, + }); + +} diff --git a/addons/restaurant/static/src/js/printbill.js b/addons/restaurant/static/src/js/printbill.js new file mode 100644 index 00000000000..89bf9db7bc7 --- /dev/null +++ b/addons/restaurant/static/src/js/printbill.js @@ -0,0 +1,28 @@ +function openerp_restaurant_printbill(instance,module){ + var QWeb = instance.web.qweb; + var _t = instance.web._t; + + module.PosWidget.include({ + build_widgets: function(){ + var self = this; + this._super(); + + if(this.pos.config.iface_printbill){ + var printbill = $(QWeb.render('PrintBillButton')); + + printbill.click(function(){ + var order = self.pos.get('selectedOrder'); + if(order.get('orderLines').models.length > 0){ + var receipt = order.export_for_printing(); + self.pos.proxy.print_receipt(QWeb.render('BillReceipt',{ + receipt: receipt, widget: self, + })); + } + }); + + printbill.appendTo(this.$('.control-buttons')); + this.$('.control-buttons').removeClass('oe_hidden'); + } + }, + }); +} diff --git a/addons/restaurant/static/src/js/splitbill.js b/addons/restaurant/static/src/js/splitbill.js new file mode 100644 index 00000000000..81591ccc589 --- /dev/null +++ b/addons/restaurant/static/src/js/splitbill.js @@ -0,0 +1,193 @@ +function openerp_restaurant_splitbill(instance, module){ + var QWeb = instance.web.qweb; + var _t = instance.web._t; + + module.SplitbillScreenWidget = module.ScreenWidget.extend({ + template: 'SplitbillScreenWidget', + + show_leftpane: false, + previous_screen: 'products', + + renderElement: function(){ + var self = this; + this._super(); + var order = this.pos.get('selectedOrder'); + if(!order){ + return; + } + var orderlines = order.get('orderLines').models; + for(var i = 0; i < orderlines.length; i++){ + var line = orderlines[i]; + linewidget = $(QWeb.render('SplitOrderline',{ + widget:this, + line:line, + selected: false, + quantity: 0, + id: line.id, + })); + linewidget.data('id',line.id); + this.$('.orderlines').append(linewidget); + } + this.$('.back').click(function(){ + self.pos_widget.screen_selector.set_current_screen(self.previous_screen); + }); + }, + + lineselect: function($el,order,neworder,splitlines,line_id){ + var split = splitlines[line_id] || {'quantity': 0, line: null}; + var line = order.getOrderline(line_id); + + if( !line.get_unit().groupable ){ + if( split.quantity !== line.get_quantity()){ + split.quantity = line.get_quantity(); + }else{ + split.quantity = 0; + } + }else{ + if( split.quantity < line.get_quantity()){ + split.quantity += line.get_unit().rounding; + if(split.quantity > line.get_quantity()){ + split.quantity = line.get_quantity(); + } + }else{ + split.quantity = 0; + } + } + + if( split.quantity ){ + if ( !split.line ){ + split.line = line.clone(); + neworder.addOrderline(split.line); + } + split.line.set_quantity(split.quantity); + }else if( split.line ) { + neworder.removeOrderline(split.line); + split.line = null; + } + + splitlines[line_id] = split; + $el.replaceWith($(QWeb.render('SplitOrderline',{ + widget: this, + line: line, + selected: split.quantity !== 0, + quantity: split.quantity, + id: line_id, + }))); + this.$('.order-info .subtotal').text(this.format_currency(neworder.getSubtotal())); + }, + + pay: function($el,order,neworder,splitlines,cashregister_id){ + var orderlines = order.get('orderLines').models; + var empty = true; + var full = true; + + for(var i = 0; i < orderlines.length; i++){ + var id = orderlines[i].id; + var split = splitlines[id]; + if(!split){ + full = false; + }else{ + if(split.quantity){ + empty = false; + if(split.quantity !== orderlines[i].get_quantity()){ + full = false; + } + } + } + } + + if(empty){ + return; + } + + for(var i = 0; i < this.pos.cashregisters.length; i++){ + if(this.pos.cashregisters[i].id === cashregister_id){ + var cashregister = this.pos.cashregisters[i]; + break; + } + } + + if(full){ + order.addPaymentline(cashregister); + this.pos_widget.screen_selector.set_current_screen('payment'); + }else{ + for(var id in splitlines){ + var split = splitlines[id]; + var line = order.getOrderline(parseInt(id)); + line.set_quantity(line.get_quantity() - split.quantity); + if(Math.abs(line.get_quantity()) < 0.00001){ + order.removeOrderline(line); + } + delete splitlines[id]; + } + neworder.addPaymentline(cashregister); + neworder.set_screen_data('screen','payment'); + + // for the kitchen printer we assume that everything + // has already been sent to the kitchen before splitting + // the bill. So we save all changes both for the old + // order and for the new one. This is not entirely correct + // but avoids flooding the kitchen with unnecessary orders. + // Not sure what to do in this case. + + if ( neworder.saveChanges ) { + order.saveChanges(); + neworder.saveChanges(); + } + + this.pos.get('orders').add(neworder); + this.pos.set('selectedOrder',neworder); + } + }, + show: function(){ + var self = this; + this._super(); + this.renderElement(); + + var order = this.pos.get('selectedOrder'); + var neworder = new module.Order({ + pos: this.pos, + temporary: true, + }); + neworder.set('client',order.get('client')); + + var splitlines = {}; + + this.$('.orderlines').on('click','.orderline',function(){ + var id = parseInt($(this).data('id')); + var $el = $(this); + self.lineselect($el,order,neworder,splitlines,id); + }); + + this.$('.paymentmethod').click(function(){ + var id = parseInt($(this).data('id')); + var $el = $(this); + self.pay($el,order,neworder,splitlines,id); + }); + }, + }); + + module.PosWidget.include({ + build_widgets: function(){ + var self = this; + this._super(); + + if(this.pos.config.iface_splitbill){ + this.splitbill_screen = new module.SplitbillScreenWidget(this,{}); + this.splitbill_screen.appendTo(this.$('.screens')); + this.screen_selector.add_screen('splitbill',this.splitbill_screen); + + var splitbill = $(QWeb.render('SplitbillButton')); + + splitbill.click(function(){ + if(self.pos.get('selectedOrder').get('orderLines').models.length > 0){ + self.pos_widget.screen_selector.set_current_screen('splitbill'); + } + }); + + splitbill.appendTo(this.$('.control-buttons')); + this.$('.control-buttons').removeClass('oe_hidden'); + } + }, + }); +} diff --git a/addons/restaurant/static/src/xml/multiprint.xml b/addons/restaurant/static/src/xml/multiprint.xml new file mode 100644 index 00000000000..63176f8938f --- /dev/null +++ b/addons/restaurant/static/src/xml/multiprint.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SubmitOrderButton"> + <span class="control-button order-submit"> + <i class="fa fa-cutlery"></i> + Order + </span> + </t> + + <t t-name="OrderChangeReceipt"> + <receipt + align='center' + width='40' + size='double-height' + line-ratio='0.6' + value-decimals='3' + value-thousands-separator='' + value-autoint='on' + > + <div>#<t t-esc="changes.sequence_number" /></div> + <div size='normal'><t t-esc="changes.name" /></div> + <br /> + <br /> + <t t-if="changes.cancelled.length > 0"> + <div color='red'> + <div bold='on' size='double'>CANCELLED</div> + <br /> + <br /> + <t t-foreach="changes.cancelled" t-as="change"> + <line> + <left><t t-esc="change.name" /></left> + <right><value><t t-esc="change.quantity" /></value></right> + </line> + </t> + <br /> + <br /> + </div> + </t> + <t t-if="changes.new.length > 0"> + <div bold='on' size='double'>NEW</div> + <br /> + <br /> + <t t-foreach="changes.new" t-as="change"> + <line> + <left><t t-esc="change.name" /></left> + <right><value><t t-esc="change.quantity" /></value></right> + </line> + </t> + <br /> + <br /> + </t> + </receipt> + </t> + +</templates> diff --git a/addons/restaurant/static/src/xml/printbill.xml b/addons/restaurant/static/src/xml/printbill.xml new file mode 100644 index 00000000000..8727743a2ad --- /dev/null +++ b/addons/restaurant/static/src/xml/printbill.xml @@ -0,0 +1,143 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="PrintBillButton"> + <span class="control-button order-printbill"> + <i class="fa fa-print"></i> + Bill + </span> + </t> + + <t t-name="BillReceipt"> + <receipt align='center' width='40' value-thousands-separator='' > + <t t-if='receipt.company.logo'> + <img t-att-src='receipt.company.logo' /> + <br/> + </t> + <t t-if='!receipt.company.logo'> + <h1><t t-esc='receipt.company.name' /></h1> + <br/> + </t> + <div font='b'> + <t t-if='receipt.shop.name'> + <div><t t-esc='receipt.shop.name' /></div> + </t> + <t t-if='receipt.company.contact_address'> + <div><t t-esc='receipt.company.contact_address' /></div> + </t> + <t t-if='receipt.company.phone'> + <div>Tel:<t t-esc='receipt.company.phone' /></div> + </t> + <t t-if='receipt.company.vat'> + <div>VAT:<t t-esc='receipt.company.vat' /></div> + </t> + <t t-if='receipt.company.email'> + <div><t t-esc='receipt.company.email' /></div> + </t> + <t t-if='receipt.company.website'> + <div><t t-esc='receipt.company.website' /></div> + </t> + <t t-if='receipt.header'> + <div><t t-esc='receipt.header' /></div> + </t> + <t t-if='receipt.cashier'> + <div>--------------------------------</div> + <div>Served by <t t-esc='receipt.cashier' /></div> + </t> + </div> + <br /><br /> + + <!-- Orderlines --> + + <div line-ratio='0.6'> + <t t-foreach='receipt.orderlines' t-as='line'> + <t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' /> + <t t-if='simple'> + <line> + <left><t t-esc='line.product_name' /></left> + <right><value><t t-esc='line.price_display' /></value></right> + </line> + </t> + <t t-if='!simple'> + <line><left><t t-esc='line.product_name' /></left></line> + <t t-if='line.discount !== 0'> + <line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line> + </t> + <line indent='1'> + <left> + <value value-decimals='3' value-autoint='on'> + <t t-esc='line.quantity' /> + </value> + <t t-if='line.unit_name !== "Unit(s)"'> + <t t-esc='line.unit_name' /> + </t> + x + <value value-decimals='2'> + <t t-esc='line.price' /> + </value> + </left> + <right> + <value><t t-esc='line.price_display' /></value> + </right> + </line> + </t> + </t> + </div> + + <!-- Subtotal --> + <t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) <= 0.000001' /> + <t t-if='!taxincluded'> + <line><right>--------</right></line> + <line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line> + <t t-foreach='receipt.tax_details' t-as='tax'> + <line> + <left><t t-esc='tax.name' /></left> + <right><value><t t-esc='tax.amount' /></value></right> + </line> + </t> + </t> + + <!-- Total --> + + <line><right>--------</right></line> + <line size='double-height'> + <left><pre> TOTAL</pre></left> + <right><value><t t-esc='receipt.total_with_tax' /></value></right> + </line> + <br/><br/> + + <!-- Extra Payment Info --> + + <t t-if='receipt.total_discount'> + <line> + <left>Discounts</left> + <right><value><t t-esc='receipt.total_discount'/></value></right> + </line> + </t> + <t t-if='taxincluded'> + <t t-foreach='receipt.tax_details' t-as='tax'> + <line> + <left><t t-esc='tax.name' /></left> + <right><value><t t-esc='tax.amount' /></value></right> + </line> + </t> + </t> + + <!-- Footer --> + <t t-if='receipt.footer'> + <br/> + <pre><t t-esc='receipt.footer' /></pre> + <br/> + <br/> + </t> + + <br/> + <div font='b'> + <div><t t-esc='receipt.name' /></div> + <div><t t-esc='receipt.date.localestring' /></div> + </div> + + </receipt> + </t> + +</templates> diff --git a/addons/restaurant/static/src/xml/splitbill.xml b/addons/restaurant/static/src/xml/splitbill.xml new file mode 100644 index 00000000000..d56f01aaa9b --- /dev/null +++ b/addons/restaurant/static/src/xml/splitbill.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SplitbillButton"> + <span class="control-button order-split"> + <i class="fa fa-copy"></i> + Split + </span> + </t> + + <t t-name="SplitOrderline"> + + <li t-attf-class="orderline #{ selected ? 'selected' : ''} #{ quantity !== line.get_quantity() ? 'partially' : '' }" + t-att-data-id="id"> + <span class="product-name"> + <t t-esc="line.get_product().name"/> + </span> + <span class="price"> + <t t-esc="widget.format_currency(line.get_display_price())"/> + </span> + <ul class="info-list"> + <t t-if="line.get_quantity_str() !== '1'"> + <li class="info"> + <t t-if='selected and line.get_unit().groupable'> + <em class='big'> + <t t-esc='quantity' /> + </em> + / + <t t-esc="line.get_quantity_str()" /> + </t> + <t t-if='!(selected and line.get_unit().groupable)'> + <em> + <t t-esc="line.get_quantity_str()" /> + </em> + </t> + <t t-esc="line.get_unit().name" /> + at + <t t-esc="widget.format_currency(line.get_unit_price())" /> + / + <t t-esc="line.get_unit().name" /> + </li> + </t> + <t t-if="line.get_discount_str() !== '0'"> + <li class="info"> + With a + <em> + <t t-esc="line.get_discount_str()" />% + </em> + discount + </li> + </t> + </ul> + </li> + </t> + + <t t-name="SplitbillScreenWidget"> + <div class='splitbill-screen screen'> + <div class='screen-content'> + <div class='top-content'> + <span class='button back'> + <i class='fa fa-angle-double-left'></i> + Back + </span> + <h1>Bill Splitting</h1> + </div> + <div class='left-content touch-scrollable scrollable-y'> + <div class='order'> + <ul class='orderlines'> + </ul> + </div> + </div> + <div class='right-content touch-scrollable scrollable-y'> + <div class='order-info'> + <span class='subtotal'><t t-esc='widget.format_currency(0.0)'/></span> + </div> + <div class='paymentmethods'> + <t t-foreach="widget.pos.cashregisters" t-as="cashregister"> + <div class='button paymentmethod' t-att-data-id="cashregister.id"> + <t t-esc='cashregister.journal.name' /> + </div> + </t> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/restaurant/views/templates.xml b/addons/restaurant/views/templates.xml new file mode 100644 index 00000000000..a0cb45cb8e5 --- /dev/null +++ b/addons/restaurant/views/templates.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- vim:fdn=3: +--> +<openerp> + <data> + + <template id="index" inherit_id='point_of_sale.index' name="Restaurant Index"><!DOCTYPE html> + <xpath expr="//link[@id='pos-stylesheet']" position="after"> + <link rel="stylesheet" href="/restaurant/static/src/css/restaurant.css" /> + </xpath> + </template> + + <template id="assets_frontend" inherit_id="web.assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/restaurant/static/src/js/multiprint.js"></script> + <script type="text/javascript" src="/restaurant/static/src/js/splitbill.js"></script> + <script type="text/javascript" src="/restaurant/static/src/js/printbill.js"></script> + <script type="text/javascript" src="/restaurant/static/src/js/main.js"></script> + </xpath> + </template> + + </data> +</openerp> |
