'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),
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):
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']
</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>
.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 {
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;
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;
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 ********* */
.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;
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;
overflow-x: hidden;
overflow-y: auto;
}
+.screen .right-content.pc60{
+ left:34%
+}
.screen .centered-content{
position: absolute;
right:25%; top: 64px; bottom: 0px;
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 {
}
@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,
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;
}
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;
cursor: pointer;
text-align: center;
}
+.splitbill-screen .paymentmethod.active,
+.payment-screen .paymentmethod.active {
+ background: #6EC89B;
+ color: white;
+ border-color: #6EC89B;
+}
+
+
/* ********* The ActionBarWidget ********* */
.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;
});
this.save('orders',orders);
},
+ remove_all_orders: function(){
+ this.save('orders',[]);
+ },
get_orders: function(){
return this.load('orders',[]);
},
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');
};
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;
},
// 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;
},
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
// 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');
// 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();
});
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;
}
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();
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(){
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){
}
}
},
+ 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];
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){
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.
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);
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
});
+ 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(){
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);
},
});
+ module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
+ template:'ErrorTracebackPopupWidget',
+ });
module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorSessionPopupWidget',
show: function(barcode){
this._super();
this.$('.barcode').text(barcode);
+
},
});
},
});
- 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',
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,
}
this.$('.client-list-contents').delegate('.client-line','click',function(event){
- console.log('uh');
self.line_select(event,$(this),parseInt($(this).data('id')));
});
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(){
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
// 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);
- }
},
});
+
}
-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();
- }
-
-}
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 }
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;
}else{
return this.currency.symbol + ' ' + amount;
}
- }
+ };
},
show: function(){
},
});
- // 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({
},
});
- 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){
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);
self.set_status(synch.state, synch.pending);
});
this.$el.click(function(){
- self.pos.flush();
+ self.pos.push_order();
});
},
});
// - 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
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)+'%'});
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,{
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'));
'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',
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();
}
}
},
<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">
--- /dev/null
+
+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")
+
--- /dev/null
+# -*- 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:
+
--- /dev/null
+# -*- 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:
--- /dev/null
+# -*- 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,
+ }
+
--- /dev/null
+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');
+ },
+ });
+
+};
+
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+# -*- 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:
+
--- /dev/null
+# -*- 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:
--- /dev/null
+# -*- 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,
+ }
+
--- /dev/null
+<?xml version="1.0"?>
+<openerp>
+ <data>
+ <record model="ir.ui.view" id="view_restaurant_printer_form">
+ <field name="name">Order Printer</field>
+ <field name="model">restaurant.printer</field>
+ <field name="arch" type="xml">
+ <form string="POS Printer" version="7.0">
+ <group col="2">
+ <field name="name" />
+ <field name="proxy_ip" />
+ <field name="product_categories_ids" />
+ </group>
+ </form>
+ </field>
+ </record>
+
+ <record model="ir.actions.act_window" id="action_restaurant_printer_form">
+ <field name="name">Order Printers</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">restaurant.printer</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to add a Restaurant Order Printer.
+ </p><p>
+ Order Printers are used by restaurants and bars to print the
+ order updates in the kitchen/bar when the waiter updates the order.
+ </p><p>
+ Each Order Printer has an IP Address that defines the PosBox/Hardware
+ Proxy where the printer can be found, and a list of product categories.
+ An Order Printer will only print updates for prodcuts belonging to one of
+ its categories.
+ </p>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_restaurant_printer">
+ <field name="name">Order Printers</field>
+ <field name="model">restaurant.printer</field>
+ <field name="arch" type="xml">
+ <tree string="Restaurant Order Printers">
+ <field name="name" />
+ <field name="proxy_ip" />
+ <field name="product_categories_ids" />
+ </tree>
+ </field>
+ </record>
+
+ <menuitem
+ parent="point_of_sale.menu_point_config_product"
+ action="action_restaurant_printer_form"
+ id="menu_restaurant_printer_all"
+ sequence="30"
+ groups="point_of_sale.group_pos_manager"/>
+
+ <record model="ir.ui.view" id="view_pos_config_form">
+ <field name="name">pos.config.form.view.inherit</field>
+ <field name="model">pos.config</field>
+ <field name="inherit_id" ref="point_of_sale.view_pos_config_form"></field>
+ <field name="arch" type="xml">
+ <sheet position='inside'>
+ <group string="Bar & Restaurant" >
+ <field name="iface_splitbill" />
+ <field name="iface_printbill" />
+ <field name="printer_ids" />
+ </group>
+ </sheet>
+ </field>
+ </record>
+
+ </data>
+</openerp>
--- /dev/null
+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
--- /dev/null
+/* --- Restaurant Specific CSS --- */
+
--- /dev/null
+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);
+
+};
--- /dev/null
+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');
+ }
+ },
+ });
+
+}
--- /dev/null
+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');
+ }
+ },
+ });
+}
--- /dev/null
+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');
+ }
+ },
+ });
+}
--- /dev/null
+<?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>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="PrintBillButton">
+ <span class="control-button order-printbill">
+ <i class="fa fa-print"></i>
+ Bill
+ </span>
+ </t>
+
+ <t t-name="BillReceipt">
+ <receipt align='center' width='40' value-thousands-separator='' >
+ <t t-if='receipt.company.logo'>
+ <img t-att-src='receipt.company.logo' />
+ <br/>
+ </t>
+ <t t-if='!receipt.company.logo'>
+ <h1><t t-esc='receipt.company.name' /></h1>
+ <br/>
+ </t>
+ <div font='b'>
+ <t t-if='receipt.shop.name'>
+ <div><t t-esc='receipt.shop.name' /></div>
+ </t>
+ <t t-if='receipt.company.contact_address'>
+ <div><t t-esc='receipt.company.contact_address' /></div>
+ </t>
+ <t t-if='receipt.company.phone'>
+ <div>Tel:<t t-esc='receipt.company.phone' /></div>
+ </t>
+ <t t-if='receipt.company.vat'>
+ <div>VAT:<t t-esc='receipt.company.vat' /></div>
+ </t>
+ <t t-if='receipt.company.email'>
+ <div><t t-esc='receipt.company.email' /></div>
+ </t>
+ <t t-if='receipt.company.website'>
+ <div><t t-esc='receipt.company.website' /></div>
+ </t>
+ <t t-if='receipt.header'>
+ <div><t t-esc='receipt.header' /></div>
+ </t>
+ <t t-if='receipt.cashier'>
+ <div>--------------------------------</div>
+ <div>Served by <t t-esc='receipt.cashier' /></div>
+ </t>
+ </div>
+ <br /><br />
+
+ <!-- Orderlines -->
+
+ <div line-ratio='0.6'>
+ <t t-foreach='receipt.orderlines' t-as='line'>
+ <t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
+ <t t-if='simple'>
+ <line>
+ <left><t t-esc='line.product_name' /></left>
+ <right><value><t t-esc='line.price_display' /></value></right>
+ </line>
+ </t>
+ <t t-if='!simple'>
+ <line><left><t t-esc='line.product_name' /></left></line>
+ <t t-if='line.discount !== 0'>
+ <line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
+ </t>
+ <line indent='1'>
+ <left>
+ <value value-decimals='3' value-autoint='on'>
+ <t t-esc='line.quantity' />
+ </value>
+ <t t-if='line.unit_name !== "Unit(s)"'>
+ <t t-esc='line.unit_name' />
+ </t>
+ x
+ <value value-decimals='2'>
+ <t t-esc='line.price' />
+ </value>
+ </left>
+ <right>
+ <value><t t-esc='line.price_display' /></value>
+ </right>
+ </line>
+ </t>
+ </t>
+ </div>
+
+ <!-- Subtotal -->
+ <t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) <= 0.000001' />
+ <t t-if='!taxincluded'>
+ <line><right>--------</right></line>
+ <line><left>Subtotal</left><right> <value><t t-esc="receipt.subtotal" /></value></right></line>
+ <t t-foreach='receipt.tax_details' t-as='tax'>
+ <line>
+ <left><t t-esc='tax.name' /></left>
+ <right><value><t t-esc='tax.amount' /></value></right>
+ </line>
+ </t>
+ </t>
+
+ <!-- Total -->
+
+ <line><right>--------</right></line>
+ <line size='double-height'>
+ <left><pre> TOTAL</pre></left>
+ <right><value><t t-esc='receipt.total_with_tax' /></value></right>
+ </line>
+ <br/><br/>
+
+ <!-- Extra Payment Info -->
+
+ <t t-if='receipt.total_discount'>
+ <line>
+ <left>Discounts</left>
+ <right><value><t t-esc='receipt.total_discount'/></value></right>
+ </line>
+ </t>
+ <t t-if='taxincluded'>
+ <t t-foreach='receipt.tax_details' t-as='tax'>
+ <line>
+ <left><t t-esc='tax.name' /></left>
+ <right><value><t t-esc='tax.amount' /></value></right>
+ </line>
+ </t>
+ </t>
+
+ <!-- Footer -->
+ <t t-if='receipt.footer'>
+ <br/>
+ <pre><t t-esc='receipt.footer' /></pre>
+ <br/>
+ <br/>
+ </t>
+
+ <br/>
+ <div font='b'>
+ <div><t t-esc='receipt.name' /></div>
+ <div><t t-esc='receipt.date.localestring' /></div>
+ </div>
+
+ </receipt>
+ </t>
+
+</templates>
--- /dev/null
+<?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>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- vim:fdn=3:
+-->
+<openerp>
+ <data>
+
+ <template id="index" inherit_id='point_of_sale.index' name="Restaurant Index"><!DOCTYPE html>
+ <xpath expr="//link[@id='pos-stylesheet']" position="after">
+ <link rel="stylesheet" href="/restaurant/static/src/css/restaurant.css" />
+ </xpath>
+ </template>
+
+ <template id="assets_frontend" inherit_id="web.assets_common">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/restaurant/static/src/js/multiprint.js"></script>
+ <script type="text/javascript" src="/restaurant/static/src/js/splitbill.js"></script>
+ <script type="text/javascript" src="/restaurant/static/src/js/printbill.js"></script>
+ <script type="text/javascript" src="/restaurant/static/src/js/main.js"></script>
+ </xpath>
+ </template>
+
+ </data>
+</openerp>