Merge pull request #1983 from odoo-dev/master-pos-new-payment-screen-fva
authorFrédéric Van der Essen <fva@openerp.com>
Mon, 25 Aug 2014 15:36:12 +0000 (17:36 +0200)
committerFrédéric Van der Essen <fva@openerp.com>
Mon, 25 Aug 2014 15:36:12 +0000 (17:36 +0200)
Point of Sale : New Payment Screen & Receipt Screens

35 files changed:
addons/point_of_sale/point_of_sale.py
addons/point_of_sale/point_of_sale_view.xml
addons/point_of_sale/static/src/css/pos.css
addons/point_of_sale/static/src/img/ios-share-icon.png [new file with mode: 0644]
addons/point_of_sale/static/src/js/db.js
addons/point_of_sale/static/src/js/main.js
addons/point_of_sale/static/src/js/models.js
addons/point_of_sale/static/src/js/screens.js
addons/point_of_sale/static/src/js/tests.js
addons/point_of_sale/static/src/js/widget_base.js
addons/point_of_sale/static/src/js/widgets.js
addons/point_of_sale/static/src/sounds/error.wav [new file with mode: 0644]
addons/point_of_sale/static/src/xml/pos.xml
addons/point_of_sale/test/test_frontend.py [new file with mode: 0644]
addons/pos_discount/__init__.py [new file with mode: 0644]
addons/pos_discount/__openerp__.py [new file with mode: 0644]
addons/pos_discount/discount.py [new file with mode: 0644]
addons/pos_discount/static/src/js/discount.js [new file with mode: 0644]
addons/pos_discount/static/src/xml/discount.xml [new file with mode: 0644]
addons/pos_discount/views/templates.xml [new file with mode: 0644]
addons/pos_discount/views/views.xml [new file with mode: 0644]
addons/restaurant/__init__.py [new file with mode: 0644]
addons/restaurant/__openerp__.py [new file with mode: 0644]
addons/restaurant/restaurant.py [new file with mode: 0644]
addons/restaurant/restaurant_view.xml [new file with mode: 0644]
addons/restaurant/security/ir.model.access.csv [new file with mode: 0644]
addons/restaurant/static/src/css/restaurant.css [new file with mode: 0644]
addons/restaurant/static/src/js/main.js [new file with mode: 0644]
addons/restaurant/static/src/js/multiprint.js [new file with mode: 0644]
addons/restaurant/static/src/js/printbill.js [new file with mode: 0644]
addons/restaurant/static/src/js/splitbill.js [new file with mode: 0644]
addons/restaurant/static/src/xml/multiprint.xml [new file with mode: 0644]
addons/restaurant/static/src/xml/printbill.xml [new file with mode: 0644]
addons/restaurant/static/src/xml/splitbill.xml [new file with mode: 0644]
addons/restaurant/views/templates.xml [new file with mode: 0644]

index 03e5680..69fe4bc 100644 (file)
@@ -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']
index 2202627..d9d777d 100644 (file)
                         </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>
index 3093963..1609e1b 100644 (file)
 .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
new file mode 100644 (file)
index 0000000..8588657
Binary files /dev/null and b/addons/point_of_sale/static/src/img/ios-share-icon.png differ
index 38230fb..08d4f87 100644 (file)
@@ -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',[]);
         },
index 90fa356..373a5f6 100644 (file)
@@ -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');
 };
 
index 92276db..e57353f 100644 (file)
@@ -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];
index 2f2f5e5..fd1d1f6 100644 (file)
@@ -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);
-            }
         },
     });
+
 }
index 018b343..08aa6e8 100644 (file)
@@ -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();
-    }
-
-}
index 648ab72..c3f7d2b 100644 (file)
@@ -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(){
index 5db1216..b5db7ee 100644 (file)
@@ -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
new file mode 100644 (file)
index 0000000..472f391
Binary files /dev/null and b/addons/point_of_sale/static/src/sounds/error.wav differ
index 929212c..77dea42 100644 (file)
                             <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>
 
                                 </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>
 
         <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>
 
         </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>
         </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">
         </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
         </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
         </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
         </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">
                     <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>
         </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'>
         </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 (file)
index 0000000..7e51c5a
--- /dev/null
@@ -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 (file)
index 0000000..5487d14
--- /dev/null
@@ -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 (file)
index 0000000..d367db1
--- /dev/null
@@ -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 (file)
index 0000000..f8d24eb
--- /dev/null
@@ -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 (file)
index 0000000..5eaab36
--- /dev/null
@@ -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 (file)
index 0000000..65a38af
--- /dev/null
@@ -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 (file)
index 0000000..0dbf22e
--- /dev/null
@@ -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 (file)
index 0000000..3448e0b
--- /dev/null
@@ -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 (file)
index 0000000..381469e
--- /dev/null
@@ -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 (file)
index 0000000..dbf735b
--- /dev/null
@@ -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 (file)
index 0000000..c7d02a3
--- /dev/null
@@ -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 (file)
index 0000000..84adc88
--- /dev/null
@@ -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 &amp; 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 (file)
index 0000000..31c7f95
--- /dev/null
@@ -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 (file)
index 0000000..0ffebfc
--- /dev/null
@@ -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 (file)
index 0000000..4b8d253
--- /dev/null
@@ -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 (file)
index 0000000..4a86e4e
--- /dev/null
@@ -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 (file)
index 0000000..89bf9db
--- /dev/null
@@ -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 (file)
index 0000000..81591cc
--- /dev/null
@@ -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 (file)
index 0000000..63176f8
--- /dev/null
@@ -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 (file)
index 0000000..8727743
--- /dev/null
@@ -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) &lt;= 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 (file)
index 0000000..d56f01a
--- /dev/null
@@ -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 (file)
index 0000000..a0cb45c
--- /dev/null
@@ -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">&lt;!DOCTYPE html&gt;
+            <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>