diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py
index 03e56800e5a..69fe4bc9ea5 100644
--- a/addons/point_of_sale/point_of_sale.py
+++ b/addons/point_of_sale/point_of_sale.py
@@ -72,6 +72,7 @@ class pos_config(osv.osv):
'iface_scan_via_proxy' : fields.boolean('Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner"),
'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'),
'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'),
+ 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'),
'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"),
'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"),
'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45),
@@ -394,7 +395,7 @@ class pos_session(osv.osv):
for obj in self.browse(cr, uid, ids, context=context):
for statement in obj.statement_ids:
statement.unlink(context=context)
- return True
+ return super(pos_session, self).unlink(cr, uid, ids, context=context)
def open_cb(self, cr, uid, ids, context=None):
@@ -553,6 +554,7 @@ class pos_order(osv.osv):
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
order_ids = []
+
for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice']
order = tmp_order['data']
diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml
index 2202627ae3e..d9d777d5ea3 100644
--- a/addons/point_of_sale/point_of_sale_view.xml
+++ b/addons/point_of_sale/point_of_sale_view.xml
@@ -628,57 +628,80 @@
-
-
+ 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.
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 30939636b23..1609e1b2633 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -20,6 +20,19 @@
.ui-dialog .ui-icon-closethick{
float: right;
}
+div.modal.in {
+ position: absolute;
+ background: white;
+ padding: 20px;
+ box-shadow: 0px 10px 20px black;
+ border-radius: 3px;
+ max-width: 600px;
+ max-height: 400px;
+ margin-top: -200px;
+ margin-left: -300px;
+ top: 50%;
+ left: 50%;
+}
/* --- Generic Restyling and Resets --- */
html {
@@ -483,10 +496,11 @@ td {
margin-right: 4px;
}
-.pos .control-button.highlight{
- background: #6EC89B;
- border: solid 1px #6EC89B;
- color: white;
+.pos .control-button.highlight,
+.pos .button.highlight {
+ background: #6EC89B !important;
+ border: solid 1px #6EC89B !important;
+ color: white !important;
}
.pos .control-button:active {
background: #7F82AC;
@@ -502,20 +516,20 @@ td {
color: inherit;
}
-/* ********* The paypad contains the payment buttons ********* */
+/* ********* The actionpad (payment, set customer) ********* */
-.pos .paypad {
- padding: 8px 4px 8px 8px;
+.pos .actionpad{
+ padding: 8px 3px 8px 19px;
display: inline-block;
text-align: center;
vertical-align: top;
- width: 205px;
+ width: 183px;
max-height: 350px;
overflow-y: auto;
overflow-x: hidden;
}
-.pos .paypad button {
- height: 39px;
+.pos .actionpad .button {
+ height: 50px;
display: block;
width: 100%;
margin: 0px -6px 4px -2px;
@@ -524,13 +538,20 @@ td {
color: #555555;
font-size: 14px;
}
-.pos .paypad button:active,
-.pos .numpad button:active,
-.pos .numpad .selected-mode,
-.pos .popup button:active{
- border: none;
+.pos .actionpad .button.pay {
+ height: 158px;
+}
+.pos .actionpad .button.pay .fa {
+ display: block;
+ font-size: 32px;
+ line-height: 54px;
+ padding-top: 6px;
+ background: rgb(86, 86, 86);
color: white;
- background: #7f82ac;
+ width: 60px;
+ margin: auto;
+ border-radius: 30px;
+ margin-bottom: 10px;
}
/* ********* The Numpad ********* */
@@ -543,7 +564,7 @@ td {
.pos .numpad button {
height: 50px;
width: 50px;
- margin: 0px 0px 4px 0px;
+ margin: 0px 3px 4px 0px;
font-weight: bold;
vertical-align: middle;
color: #555555;
@@ -913,6 +934,9 @@ td {
overflow-y: auto;
border-right: dashed 1px rgb(215,215,215);
}
+.screen .left-content.pc40{
+ right: 66%;
+}
.screen .right-content{
position: absolute;
right:0px; top: 64px; bottom: 0px;
@@ -920,6 +944,9 @@ td {
overflow-x: hidden;
overflow-y: auto;
}
+.screen .right-content.pc60{
+ left:34%
+}
.screen .centered-content{
position: absolute;
right:25%; top: 64px; bottom: 0px;
@@ -966,101 +993,125 @@ td {
position:relative;
}
+
/* b) The payment screen */
-
-.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 .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;
+}
+.pos .paymentlines tbody{
+ background: white;
+ border-radius: 3px;
}
.pos .paymentline{
- position: relative;
- padding: 8px;
- border-box: 3px;
- box-sizing: border-box;
- -moz-box-sizing: border-box;
- border-radius: 3px;
+ font-size: 22px;
}
-.pos .paymentline-name{
- margin-bottom: 8px;
+.pos .paymentline.selected {
+ font-size: 22px;
+ background: #6EC89B;
+ color: white;
}
-.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 .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 .payment-buttons {
+ display: inline-block;
+ width: 50%;
+ box-sizing: border-box;
+ padding: 16px;
+ float: right;
}
-.pos .pos-payment-container .left-block{
- display: inline-block;
- width:49%;
- margin:0;
- padding:0;
- text-align:left;
-}
-.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;
+ border: solid 1px rgb(202, 202, 202);
+ cursor: pointer;
+ text-align: center;
}
-.pos .pos-payment-container .infoline.bigger{
- opacity: 1;
- font-size: 20px;
+.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 .pos-payment-container .right-block{
- display: inline-block;
- width:49%;
- margin:0;
- padding:0;
- text-align:right;
-}
-.pos .paymentline.selected{
- background: rgb(220,220,220);
+.payment-screen .paymentlines-empty .message {
+ text-align: center;
}
/* c) The receipt screen */
+.pos .receipt-screen .centered-content .button {
+ line-height: 40px;
+ padding: 3px 13px;
+ font-size: 20px;
+ text-align: center;
+ background: rgb(230, 230, 230);
+ margin: 16px;
+ margin-bottom: 0px;
+ border-radius: 3px;
+ border: solid 1px rgb(209, 209, 209);
+ cursor: pointer;
+}
.pos .pos-receipt-container {
font-size: 0.75em;
+ text-align: center;
}
.pos .pos-sale-ticket {
@@ -1095,10 +1146,16 @@ td {
}
@media print {
+ body {
+ margin: 0;
+ }
.oe_leftbar,
.pos .pos-topheader,
.pos .pos-leftpane,
- .pos .keyboard_frame {
+ .pos .keyboard_frame,
+ .pos .receipt-screen header,
+ .pos .receipt-screen .top-content,
+ .pos .receipt-screen .centered-content .button {
display: none !important;
}
.pos,
@@ -1119,12 +1176,17 @@ td {
left: 0px !important;
background-color: white;
}
- .pos .receipt-screen header {
- display: none !important;
- }
.pos .receipt-screen {
text-align: left;
}
+ .pos .receipt-screen .centered-content{
+ position: static;
+ border: none;
+ margin: none;
+ }
+ .pos .pos-receipt-container {
+ text-align: left;
+ }
.pos-actionbar {
display: none !important;
}
@@ -1469,10 +1531,12 @@ td {
font-size: 24px;
vertical-align: top;
}
-.splitbill-screen .paymentmethods {
+.splitbill-screen .paymentmethods,
+.payment-screen .paymentmethods {
margin: 16px;
}
-.splitbill-screen .paymentmethod {
+.splitbill-screen .paymentmethod,
+.payment-screen .paymentmethod {
background: rgb(221, 221, 221);
line-height: 40px;
margin-bottom: 4px;
@@ -1482,6 +1546,14 @@ td {
cursor: pointer;
text-align: center;
}
+.splitbill-screen .paymentmethod.active,
+.payment-screen .paymentmethod.active {
+ background: #6EC89B;
+ color: white;
+ border-color: #6EC89B;
+}
+
+
/* ********* The ActionBarWidget ********* */
@@ -1663,7 +1735,17 @@ td {
.pos .popup .comment{
font-weight: normal;
font-size: 18px;
- margin: 0px 16px;
+ margin: 0px 16px 16px 16px;
+}
+.pos .popup .comment.traceback {
+ height: 264px;
+ overflow: auto;
+ font-size: 14px;
+ text-align: left;
+ font-family: 'Inconsolata';
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ user-select: text;
}
.pos .popup .footer{
position:absolute;
diff --git a/addons/point_of_sale/static/src/img/ios-share-icon.png b/addons/point_of_sale/static/src/img/ios-share-icon.png
new file mode 100644
index 00000000000..8588657e123
Binary files /dev/null and b/addons/point_of_sale/static/src/img/ios-share-icon.png differ
diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js
index 38230fb0ca2..08d4f879968 100644
--- a/addons/point_of_sale/static/src/js/db.js
+++ b/addons/point_of_sale/static/src/js/db.js
@@ -345,6 +345,9 @@ function openerp_pos_db(instance, module){
});
this.save('orders',orders);
},
+ remove_all_orders: function(){
+ this.save('orders',[]);
+ },
get_orders: function(){
return this.load('orders',[]);
},
diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js
index 90fa3567cda..373a5f66761 100644
--- a/addons/point_of_sale/static/src/js/main.js
+++ b/addons/point_of_sale/static/src/js/main.js
@@ -19,8 +19,6 @@ openerp.point_of_sale = function(instance) {
openerp_pos_widgets(instance,module); // import pos_widgets.js
- openerp_pos_tests(instance,module); // import pos_tests.js
-
instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 92276db60de..e57353fd231 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -114,180 +114,250 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return done;
},
- // helper function to load data from the server
+ // helper function to load data from the server. Obsolete use the models loader below.
fetch: function(model, fields, domain, ctx){
this._load_progress = (this._load_progress || 0) + 0.05;
this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress);
return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
},
+ // 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;
+ }
+ 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';
+
+ return logo_loaded;
+ },
+ },
+ ],
+
// 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
- 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 );
- }
- self.units_by_id = units_by_id;
+ 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;
- return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
- }).then(function(users){
- self.users = users;
-
- 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 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;
-
- 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
+ try{
+ load_model(0);
+ }catch(err){
+ loaded.reject(err);
+ }
- 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';
-
- return logo_loaded;
- });
-
return loaded;
},
@@ -324,20 +394,20 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
push_order: function(order) {
var self = this;
- this.proxy.log('push_order',order.export_as_JSON());
- var order_id = this.db.add_order(order.export_as_JSON());
+
+ if(order){
+ this.proxy.log('push_order',order.export_as_JSON());
+ this.db.add_order(order.export_as_JSON());
+ }
+
var pushed = new $.Deferred();
- this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
-
this.flush_mutex.exec(function(){
- var flushed = self._flush_all_orders();
+ var flushed = self._flush_orders(self.db.get_orders());
- flushed.always(function(){
+ flushed.always(function(ids){
pushed.resolve();
});
-
- return flushed;
});
return pushed;
},
@@ -361,8 +431,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var order_id = this.db.add_order(order.export_as_JSON());
- this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
-
this.flush_mutex.exec(function(){
var done = new $.Deferred(); // holds the mutex
@@ -373,7 +441,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// things will happen as a duplicate will be sent next time
// so we must make sure the server detects and ignores duplicated orders
- var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true});
+ var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true});
transfer.fail(function(){
invoiced.reject('error-transfer');
@@ -382,10 +450,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// on success, get the order id generated by the server
transfer.pipe(function(order_server_id){
+
// generate the pdf and download it
self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{
active_ids:order_server_id,
}});
+
invoiced.resolve();
done.resolve();
});
@@ -397,62 +467,33 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return invoiced;
},
- // attemps to send all pending orders ( stored in the pos_db ) to the server,
- // and remove the successfully sent ones from the db once
- // it has been confirmed that they have been sent correctly.
- flush: function() {
+ // wrapper around the _save_to_server that updates the synch status widget
+ _flush_orders: function(orders, options) {
var self = this;
- var flushed = new $.Deferred();
- this.flush_mutex.exec(function(){
- var done = new $.Deferred();
+ this.set('synch',{ state: 'connecting', pending: orders.length});
- self._flush_all_orders()
- .done( function(){ flushed.resolve();})
- .fail( function(){ flushed.reject(); })
- .always(function(){ done.resolve(); });
-
- return done;
- });
-
- return flushed;
- },
-
- // attempts to send the locally stored order of id 'order_id'
- // the sending is asynchronous and can take some time to decide if it is successful or not
- // it is therefore important to only call this method from inside a mutex
- // this method returns a deferred indicating wether the sending was successful or not
- // there is a timeout parameter which is set to 2 seconds by default.
- _flush_order: function( order_id, options) {
- return this._flush_all_orders([this.db.get_order(order_id)], options);
- },
-
- // attempts to send all the locally stored orders. As with _flush_order, it should only be
- // called from within a mutex.
- // this method returns a deferred that always succeeds when all orders have been tried to be sent,
- // even if none of them could actually be sent.
- _flush_all_orders: function () {
- var self = this;
- self.set('synch', {
- state: 'connecting',
- pending: self.get('synch').pending
- });
- return self._save_to_server(self.db.get_orders()).done(function () {
+ return self._save_to_server(orders, options).done(function (server_ids) {
var pending = self.db.get_orders().length;
+
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
+
+ return server_ids;
});
},
// send an array of orders to the server
// available options:
// - timeout: timeout for the rpc call in ms
+ // returns a deferred that resolves with the list of
+ // server generated ids for the sent orders
_save_to_server: function (orders, options) {
if (!orders || !orders.length) {
var result = $.Deferred();
- result.resolve();
+ result.resolve([]);
return result;
}
@@ -474,11 +515,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
shadow: !options.to_invoice,
timeout: timeout
}
- ).then(function () {
+ ).then(function (server_ids) {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
- }).fail(function (unused, event){
+ return server_ids;
+ }).fail(function (error, event){
+ if(error.code === 200 ){ // Business Logic Error, not a connection problem
+ self.pos_widget.screen_selector.show_popup('error-traceback',{
+ message: error.data.message,
+ comment: error.data.debug
+ });
+ }
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
@@ -816,6 +864,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
this.temporary = attributes.temporary || false;
this.sequence_number = this.pos.pos_session.sequence_number++;
+ this.to_invoice = false;
return this;
},
is_empty: function(){
@@ -948,11 +997,47 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return sum + paymentLine.get_amount();
}), 0);
},
- getChange: function() {
- return this.getPaidTotal() - this.getTotalTaxIncluded();
+ getChange: function(paymentline) {
+ if (!paymentline) {
+ var change = this.getPaidTotal() - this.getTotalTaxIncluded();
+ } else {
+ var change = -this.getTotalTaxIncluded();
+ var lines = this.get('paymentLines').models;
+ for (var i = 0; i < lines.length; i++) {
+ change += lines[i].get_amount();
+ if (lines[i] === paymentline) {
+ break;
+ }
+ }
+ }
+ return round_pr(Math.max(0,change), this.pos.currency.rounding);
},
- getDueLeft: function() {
- return this.getTotalTaxIncluded() - this.getPaidTotal();
+ 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);
+ },
+ isPaid: function(){
+ return this.getDueLeft() === 0;
+ },
+ isPaidWithCash: function(){
+ return !!this.get('paymentLines').find( function(pl){
+ return pl.cashregister.journal.type === 'cash';
+ });
+ },
+ finalize: function(){
+ this.destroy();
},
// sets the type of receipt 'receipt'(default) or 'invoice'
set_receipt_type: function(type){
@@ -984,6 +1069,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}
}
},
+ set_to_invoice: function(to_invoice) {
+ this.to_invoice = to_invoice;
+ },
+ is_to_invoice: function(){
+ return this.to_invoice;
+ },
+ // remove all the paymentlines with zero money in it
+ clean_empty_paymentlines: function() {
+ var lines = this.get('paymentLines').models;
+ var empty = [];
+ for ( var i = 0; i < lines.length; i++) {
+ if (!lines[i].get_amount()) {
+ empty.push(lines[i]);
+ }
+ }
+ for ( var i = 0; i < empty.length; i++) {
+ this.removePaymentline(empty[i]);
+ }
+ },
//see set_screen_data
get_screen_data: function(key){
return this.screen_data[key];
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 2f2f5e57312..fd1d1f69584 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -73,8 +73,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
load_saved_screen: function(){
this.close_popup();
var selectedOrder = this.pos.get('selectedOrder');
- // this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
- this.set_current_screen(this.default_screen,null,'refresh');
+ // FIXME : this changing screen behaviour is sometimes confusing ...
+ this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
+ //this.set_current_screen(this.default_screen,null,'refresh');
},
set_user_mode: function(user_mode){
@@ -210,26 +211,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
barcode_error_action: function(code){
this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
},
- // shows an action bar on the screen. The actionbar is automatically shown when you add a button
- // with add_action_button()
- show_action_bar: function(){
- this.pos_widget.action_bar.show();
- },
-
- // hides the action bar. The actionbar is automatically hidden when it is empty
- hide_action_bar: function(){
- this.pos_widget.action_bar.hide();
- },
-
- // adds a new button to the action bar. The button definition takes three parameters, all optional :
- // - label: the text below the button
- // - icon: a small icon that will be shown
- // - click: a callback that will be executed when the button is clicked.
- // the method returns a reference to the button widget, and automatically show the actionbar.
- add_action_button: function(button_def){
- this.show_action_bar();
- return this.pos_widget.action_bar.add_new_button(button_def);
- },
// this method shows the screen and sets up all the widget related to this screen. Extend this method
// if you want to alter the behavior of the screen.
@@ -241,12 +222,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.removeClass('oe_hidden');
}
- if(this.pos_widget.action_bar.get_button_count() > 0){
- this.show_action_bar();
- }else{
- this.hide_action_bar();
- }
-
var self = this;
this.pos_widget.set_numpad_visible(this.show_numpad);
@@ -269,7 +244,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
if(this.pos.barcode_reader){
this.pos.barcode_reader.reset_action_callbacks();
}
- this.pos_widget.action_bar.destroy_buttons();
},
// this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
@@ -314,6 +288,26 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
+ module.FullscreenPopup = module.PopUpWidget.extend({
+ template:'FullscreenPopupWidget',
+ show: function(){
+ var self = this;
+ this._super();
+ this.renderElement();
+ this.$('.button.fullscreen').off('click').click(function(){
+ window.document.body.webkitRequestFullscreen();
+ self.pos_widget.screen_selector.close_popup();
+ });
+ this.$('.button.cancel').off('click').click(function(){
+ self.pos_widget.screen_selector.close_popup();
+ });
+ },
+ ismobile: function(){
+ return typeof window.orientation !== 'undefined';
+ }
+ });
+
+
module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
template:'ChooseReceiptPopupWidget',
show: function(){
@@ -348,6 +342,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var self = this;
this._super();
+ $('body').append('');
+
if( text && (text.message || text.comment) ){
this.$('.message').text(text.message);
this.$('.comment').text(text.comment);
@@ -365,6 +361,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
});
+ module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
+ template:'ErrorTracebackPopupWidget',
+ });
module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorSessionPopupWidget',
@@ -375,6 +374,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(barcode){
this._super();
this.$('.barcode').text(barcode);
+
},
});
@@ -404,14 +404,22 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
});
- module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
- template: 'ErrorNoClientPopupWidget',
- });
-
module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
template: 'ErrorInvoiceTransferPopupWidget',
});
+ module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
+ template: 'UnsentOrdersPopupWidget',
+ show: function(options){
+ var self = this;
+ this._super(options);
+ this.renderElement();
+ this.$('.button.confirm').click(function(){
+ self.pos_widget.screen_selector.close_popup();
+ });
+ },
+ });
+
module.ScaleScreenWidget = module.ScreenWidget.extend({
template:'ScaleScreenWidget',
@@ -557,6 +565,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ClientListScreenWidget = module.ScreenWidget.extend({
template: 'ClientListScreenWidget',
+ init: function(parent, options){
+ this._super(parent, options);
+ this.partner_cache = new module.DomCache();
+ },
+
show_leftpane: false,
auto_back: true,
@@ -587,7 +600,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}
this.$('.client-list-contents').delegate('.client-line','click',function(event){
- console.log('uh');
self.line_select(event,$(this),parseInt($(this).data('id')));
});
@@ -628,14 +640,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$('.searchbox input').focus();
},
render_list: function(partners){
- var contents = this.$('.client-list-contents');
- contents.empty();
+ var contents = this.$el[0].querySelector('.client-list-contents');
+ contents.innerHtml = "";
for(var i = 0, len = partners.length; i < len; i++){
- var clientline = $(QWeb.render('ClientLine',{partner:partners[i]}));
- if( partners[i] === this.new_client ){
- clientline.addClass('highlight');
+ var partner = partners[i];
+ var clientline = this.partner_cache.get_node(partner.id);
+ if(!clientline){
+ var clientline_html = QWeb.render('ClientLine',{partner:partners[i]});
+ var clientline = document.createElement('tbody');
+ clientline.innerHTML = clientline_html;
+ clientline = clientline.childNodes[1];
+ this.partner_cache.cache_node(partner.id,clientline);
}
- contents.append(clientline);
+ if( partners === this.new_client ){
+ clientline.classList.add('highlight');
+ }else{
+ clientline.classList.remove('highlight');
+ }
+ contents.appendChild(clientline);
}
},
save_changes: function(){
@@ -730,30 +752,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ReceiptScreenWidget = module.ScreenWidget.extend({
template: 'ReceiptScreenWidget',
-
- show_numpad: true,
- show_leftpane: true,
+ show_numpad: false,
+ show_leftpane: false,
show: function(){
this._super();
var self = this;
- var print_button = this.add_action_button({
- label: _t('Print'),
- icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
- click: function(){ self.print(); },
- });
-
- var finish_button = this.add_action_button({
- label: _t('Next Order'),
- icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
- click: function() { self.finishOrder(); },
- });
-
this.refresh();
this.print();
- //
// The problem is that in chrome the print() is asynchronous and doesn't
// execute until all rpc are finished. So it conflicts with the rpc used
// to send the orders to the backend, and the user is able to go to the next
@@ -770,372 +778,345 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
// 2 seconds is the same as the default timeout for sending orders and so the dialog
// should have appeared before the timeout... so yeah that's not ultra reliable.
- finish_button.set_disabled(true);
+ this.lock_screen(true);
setTimeout(function(){
- finish_button.set_disabled(false);
+ self.lock_screen(false);
}, 2000);
},
+ lock_screen: function(locked) {
+ this._locked = locked;
+ if (locked) {
+ this.$('.next').removeClass('highlight');
+ } else {
+ this.$('.next').addClass('highlight');
+ }
+ },
print: function() {
window.print();
},
- finishOrder: function() {
- this.pos.get('selectedOrder').destroy();
+ finish_order: function() {
+ if (!this._locked) {
+ this.pos.get_order().finalize();
+ }
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+ this.$('.next').click(function(){
+ self.finish_order();
+ });
+ this.$('.button.print').click(function(){
+ self.print();
+ });
},
refresh: function() {
- var order = this.pos.get('selectedOrder');
- $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
+ var order = this.pos.get_order();
+ this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
widget:this,
order: order,
orderlines: order.get('orderLines').models,
paymentlines: order.get('paymentLines').models,
}));
},
- close: function(){
- this._super();
- }
});
-
module.PaymentScreenWidget = module.ScreenWidget.extend({
- template: 'PaymentScreenWidget',
- back_screen: 'products',
- next_screen: 'receipt',
+ template: 'PaymentScreenWidget',
+ back_screen: 'product',
+ next_screen: 'receipt',
+ show_leftpane: false,
+ show_numpad: false,
init: function(parent, options) {
var self = this;
- this._super(parent,options);
+ this._super(parent, options);
this.pos.bind('change:selectedOrder',function(){
- this.bind_events();
this.renderElement();
+ this.watch_order_changes();
},this);
+ this.watch_order_changes();
- this.bind_events();
-
- this.line_delete_handler = function(event){
- var node = this;
- while(node && !node.classList.contains('paymentline')){
- node = node.parentNode;
- }
- if(node){
- self.pos.get('selectedOrder').removePaymentline(node.line)
- }
- event.stopPropagation();
- };
-
- this.line_change_handler = function(event){
- var node = this;
- while(node && !node.classList.contains('paymentline')){
- node = node.parentNode;
- }
- if(node){
- node.line.set_amount(this.value);
- }
- };
-
- this.line_click_handler = function(event){
- 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){
+ this.inputbuffer = "";
+ this.firstinput = true;
+ this.keyboard_handler = function(event){
+ var key = '';
+ if ( event.keyCode === 13 ) { // Enter
self.validate_order();
- }else if(event.which === 27){
- self.back();
+ } 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 = '+';
}
+
+ self.payment_input(key);
+
};
+ },
+ // 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 = "";
+ }
+ },
+ // 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.firstinput = false;
+
+ 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);
+ }
+ }
+ },
+ click_numpad: function(button) {
+ this.payment_input(button.data('action'));
+ },
+ 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;
+ }
+ }
+ },
+ 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;
+ }
+ }
+ },
+ render_paymentlines: function() {
+ var self = this;
+ var order = this.pos.get_order();
+ var lines = order.get('paymentLines').models;
+
+ this.$('.paymentlines-container').empty();
+ var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
+ widget: this,
+ order: order,
+ paymentlines: lines,
+ }));
+
+ 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();
+ },
+ 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');
+ }
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+
+ var numpad = this.render_numpad();
+ numpad.appendTo(this.$('.payment-numpad'));
+
+ 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();
+ });
},
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();
+ },
+ hide: function(){
+ window.document.body.removeEventListener('keydown',this.keyboard_handler);
+ this._super();
+ },
+ // sets up listeners to watch for order changes
+ watch_order_changes: function() {
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});
- },
- });
- }
-
- 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();
- },
- });
- }
-
- this.update_payment_summary();
-
- },
- 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);
- }
- }
- },
- back: function() {
- this.remove_empty_lines();
- this.pos_widget.screen_selector.set_current_screen(this.back_screen);
- },
- bind_events: function() {
+ var order = this.pos.get_order();
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);
-
+ order.bind('all',function(){
+ self.order_changes();
+ });
this.old_order = order;
-
- if(this.old_paymentlines){
- this.old_paymentlines.unbind(null,null,this);
- }
- var paymentlines = order.get('paymentLines');
- paymentlines.bind('add', this.add_paymentline, this);
- paymentlines.bind('change:selected', this.rerender_paymentline, this);
- paymentlines.bind('change:amount', function(line){
- if(!line.selected && line.node){
- line.node.value = line.amount.toFixed(2);
- }
- this.update_payment_summary();
- },this);
- paymentlines.bind('remove', this.remove_paymentline, this);
- paymentlines.bind('all', this.update_payment_summary, this);
-
- this.old_paymentlines = paymentlines;
-
- if(this.old_orderlines){
- this.old_orderlines.unbind(null,null,this);
- }
- var orderlines = order.get('orderLines');
- orderlines.bind('all', this.update_payment_summary, this);
-
- this.old_orderlines = orderlines;
},
- focus_selected_line: function(){
- var line = this.pos.get('selectedOrder').selected_paymentline;
- if(line){
- var input = line.node.querySelector('input');
- if(!input){
- return;
- }
- var value = input.value;
- input.focus();
-
- if(this.numpad_state){
- this.numpad_state.reset();
- }
-
- if(Number(value) === 0){
- input.value = '';
- }else{
- input.value = value;
- input.select();
- }
- }
- },
- add_paymentline: function(line) {
- var list_container = this.el.querySelector('.payment-lines');
- list_container.appendChild(this.render_paymentline(line));
-
- if(this.numpad_state){
- this.numpad_state.reset();
- }
- },
- render_paymentline: function(line){
- var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line});
- el_html = _.str.trim(el_html);
-
- var el_node = document.createElement('tbody');
- el_node.innerHTML = el_html;
- el_node = el_node.childNodes[0];
- el_node.line = line;
- el_node.querySelector('.paymentline-delete')
- .addEventListener('click', this.line_delete_handler);
- el_node.addEventListener('click', this.line_click_handler);
- el_node.querySelector('input')
- .addEventListener('keyup', this.line_change_handler);
-
- line.node = el_node;
-
- return el_node;
- },
- rerender_paymentline: function(line){
- var old_node = line.node;
- var new_node = this.render_paymentline(line);
-
- old_node.parentNode.replaceChild(new_node,old_node);
- },
- remove_paymentline: function(line){
- line.node.parentNode.removeChild(line.node);
- line.node = undefined;
- },
- renderElement: function(){
- this._super();
-
- var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models;
- var list_container = this.el.querySelector('.payment-lines');
-
- for(var i = 0; i < paymentlines.length; i++){
- list_container.appendChild(this.render_paymentline(paymentlines[i]));
- }
-
- this.update_payment_summary();
- },
- update_payment_summary: function() {
- var currentOrder = this.pos.get('selectedOrder');
- var paidTotal = currentOrder.getPaidTotal();
- var dueTotal = currentOrder.getTotalTaxIncluded();
- var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
- var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
-
- this.$('.payment-due-total').html(this.format_currency(dueTotal));
- this.$('.payment-paid-total').html(this.format_currency(paidTotal));
- this.$('.payment-remaining').html(this.format_currency(remaining));
- this.$('.payment-change').html(this.format_currency(change));
- if(currentOrder.selected_orderline === undefined){
- remaining = 1; // What is this ?
- }
-
- if(this.pos_widget.action_bar){
- this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
- this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
- }
- },
- is_paid: function(){
- var currentOrder = this.pos.get('selectedOrder');
- return (currentOrder.getTotalTaxIncluded() < 0.000001
- || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
-
- },
- validate_order: function(options) {
+ // 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');
+ }
+ },
+ // Check if the order is paid, then sends it to the backend,
+ // and complete the sale process
+ validate_order: function() {
var self = this;
- options = options || {};
- var currentOrder = this.pos.get('selectedOrder');
+ var order = this.pos.get_order();
- if(!this.is_paid()){
+ if (!order.isPaid() || this.invoicing) {
return;
}
- if( this.pos.config.iface_cashdrawer
- && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){
- return pl.cashregister.journal.type === 'cash';
- })){
+ if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) {
this.pos.proxy.open_cashbox();
}
- if(options.invoice){
- // deactivate the validation button while we try to send the order
- this.pos_widget.action_bar.set_button_disabled('validation',true);
- this.pos_widget.action_bar.set_button_disabled('invoice',true);
-
- var invoiced = this.pos.push_and_invoice_order(currentOrder);
+ if (order.is_to_invoice()) {
+ var invoiced = this.pos.push_and_invoice_order(order);
+ this.invoicing = true;
invoiced.fail(function(error){
- if(error === 'error-no-client'){
- self.pos_widget.screen_selector.show_popup('error-no-client');
- }else{
+ self.invoicing = false;
+ if (error === 'error-no-client') {
+ self.pos_widget.screen_selector.show_popup('confirm',{
+ message: _t('Please select the Customer'),
+ comment: _t('You need to select the customer before you can invoice an order.'),
+ confirm: function(){
+ self.pos_widget.screen_selector.set_current_screen('clientlist');
+ },
+ });
+ } else {
self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
}
- self.pos_widget.action_bar.set_button_disabled('validation',false);
- self.pos_widget.action_bar.set_button_disabled('invoice',false);
});
invoiced.done(function(){
- self.pos_widget.action_bar.set_button_disabled('validation',false);
- self.pos_widget.action_bar.set_button_disabled('invoice',false);
- self.pos.get('selectedOrder').destroy();
+ self.invoicing = false;
+ order.finalize();
});
-
- }else{
- this.pos.push_order(currentOrder)
- if(this.pos.config.iface_print_via_proxy){
+ } else {
+ this.pos.push_order(order)
+ if (this.pos.config.iface_print_via_proxy) {
var receipt = currentOrder.export_for_printing();
this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
receipt: receipt, widget: self,
}));
- this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
- }else{
+ order.finalize(); //finish order and go back to scan screen
+ } else {
this.pos_widget.screen_selector.set_current_screen(this.next_screen);
}
}
-
- // hide onscreen (iOS) keyboard
- setTimeout(function(){
- document.activeElement.blur();
- $("input").blur();
- },250);
- },
- enable_numpad: function(){
- this.disable_numpad(); //ensure we don't register the callbacks twice
- this.numpad_state = this.pos_widget.numpad.state;
- if(this.numpad_state){
- this.numpad_state.reset();
- this.numpad_state.changeMode('payment');
- this.numpad_state.bind('set_value', this.set_value, this);
- this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
- }
-
- },
- disable_numpad: function(){
- if(this.numpad_state){
- this.numpad_state.unbind('set_value', this.set_value);
- this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
- }
- },
- set_mode_back_to_payment: function() {
- this.numpad_state.set({mode: 'payment'});
- },
- set_value: function(val) {
- var selected_line =this.pos.get('selectedOrder').selected_paymentline;
- if(selected_line){
- selected_line.set_amount(val);
- selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);
- }
},
});
+
}
diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js
index 018b343de58..08aa6e87213 100644
--- a/addons/point_of_sale/static/src/js/tests.js
+++ b/addons/point_of_sale/static/src/js/tests.js
@@ -1,93 +1,89 @@
-function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
+(function() {
+ 'use strict';
- // Various UI Tests to measure performance and memory leaks.
- module.UiTester = function(){
- var running = false;
- var queue = new module.JobQueue();
+ openerp.Tour.register({
+ id: 'pos_basic_order',
+ name: 'Complete a basic order trough the Front-End',
+ path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening',
+ mode: 'test',
+ steps: [
+ {
+ title: 'Wait fot the bloody screen to be ready',
+ wait: 200,
+ },
+ {
+ title: 'Load the Session',
+ waitNot: '.oe_loading:visible',
+ element: 'span:contains("Resume Session"),span:contains("Start Session")',
+ },
+ {
+ title: 'Loading the Loading Screen',
+ waitFor: '.loader'
+ },
+ {
+ title: 'Waiting for the end of loading...',
+ waitFor: '.loader:hidden',
+ },
+ {
+ title: 'Loading The Point of Sale',
+ waitFor: '.pos',
+ },
+ {
+ title: 'On va manger des CHIPS!',
+ element: '.product-list .product-name:contains("250g Lays Pickels")',
+ },
+ {
+ title: 'The chips have been added to the Order',
+ waitFor: '.order .product-name:contains("250g Lays Pickels")',
+ },
+ {
+ title: 'The order total has been updated to the correct value',
+ wait: 2000,
+ waitFor: '.order .total .value:contains("1.48 €")',
+ },
+ {
+ title: "Let's buy more chips",
+ element: '.product-list .product-name:contains("250g Lays Pickels")',
+ },
+ {
+ title: "Let's veryify we pay the correct price for two bags of chips",
+ waitFor: '.order .total .value:contains("2.96 €")',
+ },
+ {
+ title: "Let's pay with a debit card",
+ element: ".paypad-button:contains('Bank')",
+ },
+ {
+ title: "Let's accept the payment",
+ onload: function(){
+ // The test cannot validate or cancel the print() ... so we replace it by a noop !.
+ window._print = window.print;
+ window.print = function(){ console.log('Print!') };
+ },
+ element: ".button .iconlabel:contains('Validate'):visible",
+ },
+ {
+ title: "Let's finish the order",
+ element: ".button:not(.disabled) .iconlabel:contains('Next'):visible",
+ },
+ {
+ onload: function(){
+ window.print = window._print;
+ window._print = undefined;
+ },
+ title: "Let's wait for the order posting",
+ waitFor: ".oe_status.js_synch .js_connected:visible",
+ },
+ {
+ title: "Let's close the Point of Sale",
+ element: ".header-button:contains('Close')",
+ },
+ {
+ title: "Wait for the backend to ready itself",
+ element: 'span:contains("Resume Session"),span:contains("Start Session")',
+ },
+ ],
+ });
- // stop the currently running test
- this.stop = function(){
- queue.clear();
- };
+})();
- // randomly switch product categories
- this.category_switch = function(interval){
- queue.schedule(function(){
- var breadcrumbs = $('.breadcrumb-button');
- var categories = $('.category-button');
- if(categories.length > 0){
- var rnd = Math.floor(Math.random()*categories.length);
- categories.eq(rnd).click();
- }else{
- var rnd = Math.floor(Math.random()*breadcrumbs.length);
- breadcrumbs.eq(rnd).click();
- }
- },{repeat:true, duration:interval});
- };
-
- // randomly order products then resets the order
- this.order_products = function(interval){
-
- queue.schedule(function(){
- var def = new $.Deferred();
- var order_queue = new module.JobQueue();
- var order_size = 1 + Math.floor(Math.random()*10);
-
- while(order_size--){
- order_queue.schedule(function(){
- var products = $('.product');
- if(products.length > 0){
- var rnd = Math.floor(Math.random()*products.length);
- products.eq(rnd).click();
- }
- },{duration:20});
- }
- order_queue.finished().then(function(){
- $('.deleteorder-button').click();
- def.resolve();
- });
- return def;
- },{repeat:true, duration: interval});
-
- };
-
- // makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
- this.full_order_cycle = function(interval){
- queue.schedule(function(){
- var def = new $.Deferred();
- var order_queue = new module.JobQueue();
- var order_size = 1 + Math.floor(Math.random()*50);
-
- while(order_size--){
- order_queue.schedule(function(){
- var products = $('.product');
- if(products.length > 0){
- var rnd = Math.floor(Math.random()*products.length);
- products.eq(rnd).click();
- }
- },{duration:50});
- }
- order_queue.schedule(function(){
- $('.paypad-button:first').click();
- },{duration:250});
- order_queue.schedule(function(){
- $('.paymentline-input:first').val(10000);
- $('.paymentline-input:first').keydown();
- $('.paymentline-input:first').keyup();
- },{duration:250});
- order_queue.schedule(function(){
- $('.pos-actionbar-button-list .button:eq(2)').click();
- },{duration:250});
- order_queue.schedule(function(){
- def.resolve();
- });
- return def;
- },{repeat: true, duration: interval});
- };
- };
-
- if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
- window.pos_test_ui = new module.UiTester();
- }
-
-}
diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js
index 648ab725f9e..c3f7d2baf5f 100644
--- a/addons/point_of_sale/static/src/js/widget_base.js
+++ b/addons/point_of_sale/static/src/js/widget_base.js
@@ -1,5 +1,7 @@
function openerp_pos_basewidget(instance, module){ //module is instance.point_of_sale
+ var round_pr = instance.web.round_precision
+
// This is a base class for all Widgets in the POS. It exposes relevant data to the
// templates :
// - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after }
@@ -27,6 +29,12 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10)));
+ this.format_currency_no_symbol = function(amount){
+ amount = round_pr(amount,this.currency.rounding);
+ amount = amount.toFixed(decimals);
+ return amount;
+ };
+
this.format_currency = function(amount){
if(typeof amount === 'number'){
amount = Math.round(amount*100)/100;
@@ -37,7 +45,7 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
}else{
return this.currency.symbol + ' ' + amount;
}
- }
+ };
},
show: function(){
diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js
index 5db12165737..b5db7ee47e1 100644
--- a/addons/point_of_sale/static/src/js/widgets.js
+++ b/addons/point_of_sale/static/src/js/widgets.js
@@ -83,46 +83,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
- // The paypad allows to select the payment method (cashregisters)
- // used to pay the order.
- module.PaypadWidget = module.PosBaseWidget.extend({
- template: 'PaypadWidget',
+ // The action pads contains the payment button and the customer selection button.
+ module.ActionpadWidget = module.PosBaseWidget.extend({
+ template: 'ActionpadWidget',
renderElement: function() {
var self = this;
this._super();
-
- _.each(this.pos.cashregisters,function(cashregister) {
- var button = new module.PaypadButtonWidget(self,{
- pos: self.pos,
- pos_widget : self.pos_widget,
- cashregister: cashregister,
- });
- button.appendTo(self.$el);
+ this.$('.pay').click(function(){
+ self.pos.pos_widget.screen_selector.set_current_screen('payment');
+ });
+ this.$('.set-customer').click(function(){
+ self.pos.pos_widget.screen_selector.set_current_screen('clientlist');
});
}
});
- 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');
- });
- },
- });
-
module.OrderWidget = module.PosBaseWidget.extend({
template:'OrderWidget',
init: function(parent, options) {
@@ -318,90 +293,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
- module.ActionButtonWidget = instance.web.Widget.extend({
- template:'ActionButtonWidget',
- icon_template:'ActionButtonWidgetWithIcon',
- init: function(parent, options){
- this._super(parent, options);
- this.label = options.label || 'button';
- this.rightalign = options.rightalign || false;
- this.click_action = options.click;
- this.disabled = options.disabled || false;
- if(options.icon){
- this.icon = options.icon;
- this.template = this.icon_template;
- }
- },
- set_disabled: function(disabled){
- if(this.disabled != disabled){
- this.disabled = !!disabled;
- this.renderElement();
- }
- },
- renderElement: function(){
- this._super();
- if(this.click_action && !this.disabled){
- this.$el.click(_.bind(this.click_action, this));
- }
- },
- });
-
- module.ActionBarWidget = instance.web.Widget.extend({
- template:'ActionBarWidget',
- init: function(parent, options){
- this._super(parent,options);
- this.button_list = [];
- this.buttons = {};
- this.visibility = {};
- },
- set_element_visible: function(element, visible, action){
- if(visible != this.visibility[element]){
- this.visibility[element] = !!visible;
- if(visible){
- this.$('.'+element).removeClass('oe_hidden');
- }else{
- this.$('.'+element).addClass('oe_hidden');
- }
- }
- if(visible && action){
- this.action[element] = action;
- this.$('.'+element).off('click').click(action);
- }
- },
- set_button_disabled: function(name, disabled){
- var b = this.buttons[name];
- if(b){
- b.set_disabled(disabled);
- }
- },
- destroy_buttons:function(){
- for(var i = 0; i < this.button_list.length; i++){
- this.button_list[i].destroy();
- }
- this.button_list = [];
- this.buttons = {};
- return this;
- },
- get_button_count: function(){
- return this.button_list.length;
- },
- add_new_button: function(button_options){
- var button = new module.ActionButtonWidget(this,button_options);
- this.button_list.push(button);
- if(button_options.name){
- this.buttons[button_options.name] = button;
- }
- button.appendTo(this.$('.pos-actionbar-button-list'));
- return button;
- },
- show:function(){
- this.$el.removeClass('oe_hidden');
- },
- hide:function(){
- this.$el.addClass('oe_hidden');
- },
- });
-
module.ProductCategoriesWidget = module.PosBaseWidget.extend({
template: 'ProductCategoriesWidget',
init: function(parent, options){
@@ -797,6 +688,19 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.$('.button.reference').click(function(){
self.pos.barcode_reader.scan(self.$('input.ean').val());
});
+ this.$('.button.show_orders').click(function(){
+ self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
+ });
+ this.$('.button.delete_orders').click(function(){
+ self.pos.pos_widget.screen_selector.show_popup('confirm',{
+ message: _t('Delete Unsent Orders ?'),
+ comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'),
+ confirm: function(){
+ self.pos.db.remove_all_orders();
+ self.pos.set({synch: { state:'connected', pending: 0 }});
+ },
+ });
+ });
_.each(this.eans, function(ean, name){
self.$('.button.'+name).click(function(){
self.$('input.ean').val(ean);
@@ -840,7 +744,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.set_status(synch.state, synch.pending);
});
this.$el.click(function(){
- self.pos.flush();
+ self.pos.push_order();
});
},
});
@@ -903,7 +807,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
// - a header, containing the list of orders
// - a leftpane, containing the list of bought products (orderlines)
// - a rightpane, containing the screens (see pos_screens.js)
- // - an actionbar on the bottom, containing various action buttons
// - popups
// - an onscreen keyboard
// a screen_selector which controls the switching between screens and the showing/closing of popups
@@ -1001,23 +904,50 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
}else if(!self.pos.config){
self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
+ }else if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
+ window.screen.availWidth > window.innerWidth ||
+ window.screen.availHeight > window.innerHeight )){
+ self.screen_selector.show_popup('fullscreen');
}
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
- self.pos.flush();
+ self.pos.push_order();
- }).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));
+ }).fail(function(err){ // error when loading models data from the backend
+ self.loading_error(err);
});
},
+ loading_error: function(err){
+ var self = this;
+
+ var message = err.message;
+ var comment = err.stack;
+
+ if(err.message === 'XmlHttpRequestError '){
+ message = 'Network Failure (XmlHttpRequestError)';
+ comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
+ }else if(err.message === 'OpenERP Server Error'){
+ message = err.data.message;
+ comment = err.data.debug;
+ }
+
+ if( typeof comment !== 'string' ){
+ comment = 'Traceback not available.';
+ }
+
+ var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
+ widget: { message: message, comment: comment },
+ }));
+
+ popup.find('.button').click(function(){
+ self.close();
+ });
+
+ popup.css({ zindex: 9001 });
+
+ popup.appendTo(this.$el);
+ },
loading_progress: function(fac){
this.$('.loader .loader-feedback').removeClass('oe_hidden');
this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
@@ -1076,15 +1006,21 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
this.choose_receipt_popup.appendTo(this.$el);
- this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
- this.error_no_client_popup.appendTo(this.$el);
-
this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
this.error_invoice_transfer_popup.appendTo(this.$el);
+ this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
+ this.error_traceback_popup.appendTo(this.$el);
+
this.confirm_popup = new module.ConfirmPopupWidget(this,{});
this.confirm_popup.appendTo(this.$el);
+ this.fullscreen_popup = new module.FullscreenPopup(this,{});
+ this.fullscreen_popup.appendTo(this.$el);
+
+ this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
+ this.unsent_orders_popup.appendTo(this.$el);
+
// -------- Misc ---------
this.close_button = new module.HeaderButtonWidget(this,{
@@ -1104,11 +1040,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.username = new module.UsernameWidget(this,{});
this.username.replace(this.$('.placeholder-UsernameWidget'));
- this.action_bar = new module.ActionBarWidget(this);
- this.action_bar.replace(this.$(".placeholder-RightActionBar"));
-
- this.paypad = new module.PaypadWidget(this, {});
- this.paypad.replace(this.$('.placeholder-PaypadWidget'));
+ this.actionpad = new module.ActionpadWidget(this, {});
+ this.actionpad.replace(this.$('.placeholder-ActionpadWidget'));
this.numpad = new module.NumpadWidget(this);
this.numpad.replace(this.$('.placeholder-NumpadWidget'));
@@ -1137,9 +1070,11 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'error-barcode': this.error_barcode_popup,
'error-session': this.error_session_popup,
'choose-receipt': this.choose_receipt_popup,
- 'error-no-client': this.error_no_client_popup,
'error-invoice-transfer': this.error_invoice_transfer_popup,
+ 'error-traceback': this.error_traceback_popup,
'confirm': this.confirm_popup,
+ 'fullscreen': this.fullscreen_popup,
+ 'unsent-orders': this.unsent_orders_popup,
},
default_screen: 'products',
default_mode: 'cashier',
@@ -1164,10 +1099,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.numpad_visible = visible;
if(visible){
this.numpad.show();
- this.paypad.show();
+ this.actionpad.show();
}else{
this.numpad.hide();
- this.paypad.hide();
+ this.actionpad.hide();
}
}
},
diff --git a/addons/point_of_sale/static/src/sounds/error.wav b/addons/point_of_sale/static/src/sounds/error.wav
new file mode 100644
index 00000000000..472f3910719
Binary files /dev/null and b/addons/point_of_sale/static/src/sounds/error.wav differ
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index 929212cce8d..77dea42c5db 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -36,19 +36,12 @@
-
+
-
-
-
-
-
-
-
@@ -60,14 +53,6 @@
-
-
-
-
-
-
-
-
@@ -144,8 +129,14 @@
There are pending operations that could not be saved into the database, are you sure you want to exit?
-
-
+
+
+
+
@@ -383,41 +374,179 @@
-
-
-
-
-
-
-
-
-
-
- Paid:
-
-
-
-
-
- Remaining:
-
-
-
-
-
- Change:
-
-
-
+
+
+
+
+
+
+
+ Please select a payment method.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Due
+
Tendered
+
Change
+
Method
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
Payment
+
+ Validate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Invoice
+
+
+
+
+ Open Cashbox
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
Receipt
+
+ Next Order
+
+
+
+
+
+ Print
+
+
+
@@ -586,23 +715,55 @@
-
+
-
-
Sorry, we could not create a session for this user.
+
+
Fullscreen Setup
+
+
+
+ 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
+ and then Add to Homescreen
+
+
+ This also works on Android with the Chrome Beta Browser, using the Add to Homescreen option
+ in the browser's menu.
+
+
+ If you want to work in fullscreen just this time tap the Go Fullscreen button.
+
+
+
+
+
+ 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.
+
+
+ If you want to work in fullscreen just this time, click the Go Fullscreen button.
+
+
+
+
-
+
-
-
An anonymous order cannot be invoiced
-
+
+
Sorry, we could not create a session for this user.
-
-
diff --git a/addons/point_of_sale/test/test_frontend.py b/addons/point_of_sale/test/test_frontend.py
new file mode 100644
index 00000000000..7e51c5ae643
--- /dev/null
+++ b/addons/point_of_sale/test/test_frontend.py
@@ -0,0 +1,9 @@
+
+import openerp.tests
+
+@openerp.tests.common.at_install(False)
+@openerp.tests.common.post_install(True)
+class TestUi(openerp.tests.HttpCase):
+ def test_01_pos_basic_order(self):
+ self.phantom_js("/", "openerp.Tour.run('pos_basic_order', 'test')", "openerp.Tour.tours.pos_basic_order", login="admin")
+
diff --git a/addons/pos_discount/__init__.py b/addons/pos_discount/__init__.py
new file mode 100644
index 00000000000..5487d145a9b
--- /dev/null
+++ b/addons/pos_discount/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+import discount
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/addons/pos_discount/__openerp__.py b/addons/pos_discount/__openerp__.py
new file mode 100644
index 00000000000..d367db148cd
--- /dev/null
+++ b/addons/pos_discount/__openerp__.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+
+{
+ 'name': 'Point of Sale Discounts',
+ 'version': '1.0',
+ 'category': 'Point of Sale',
+ 'sequence': 6,
+ 'summary': 'Simple Discounts in the Point of Sale ',
+ 'description': """
+
+=======================
+
+This module allows the cashier to quickly give a percentage
+sale discount to a customer.
+
+""",
+ 'author': 'OpenERP SA',
+ 'depends': ['point_of_sale'],
+ 'data': [
+ 'views/views.xml',
+ 'views/templates.xml'
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/pos_discount/discount.py b/addons/pos_discount/discount.py
new file mode 100644
index 00000000000..f8d24eb638b
--- /dev/null
+++ b/addons/pos_discount/discount.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+import logging
+
+import openerp
+
+from openerp import tools
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+class pos_config(osv.osv):
+ _inherit = 'pos.config'
+ _columns = {
+ 'discount_pc': fields.float('Discount Percentage', help='The discount percentage'),
+ 'discount_product_id': fields.many2one('product.product','Discount Product', help='The product used to model the discount'),
+ }
+ _defaults = {
+ 'discount_pc': 10,
+ }
+
diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js
new file mode 100644
index 00000000000..5eaab362d20
--- /dev/null
+++ b/addons/pos_discount/static/src/js/discount.js
@@ -0,0 +1,34 @@
+openerp.pos_discount = function(instance){
+ var module = instance.point_of_sale;
+ var round_pr = instance.web.round_precision
+ var QWeb = instance.web.qweb;
+
+ QWeb.add_template('/pos_discount/static/src/xml/discount.xml');
+
+ module.PosWidget.include({
+ build_widgets: function(){
+ var self = this;
+ this._super();
+
+ if(!this.pos.config.discount_product_id){
+ return;
+ }
+
+ var discount = $(QWeb.render('DiscountButton'));
+
+ discount.click(function(){
+ var order = self.pos.get('selectedOrder');
+ var product = self.pos.db.get_product_by_id(self.pos.config.discount_product_id[0]);
+ var discount = - self.pos.config.discount_pc/ 100.0 * order.getTotalTaxIncluded();
+ if( discount < 0 ){
+ order.addProduct(product, { price: discount });
+ }
+ });
+
+ discount.appendTo(this.$('.control-buttons'));
+ this.$('.control-buttons').removeClass('oe_hidden');
+ },
+ });
+
+};
+
diff --git a/addons/pos_discount/static/src/xml/discount.xml b/addons/pos_discount/static/src/xml/discount.xml
new file mode 100644
index 00000000000..65a38afb97d
--- /dev/null
+++ b/addons/pos_discount/static/src/xml/discount.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ Discount
+
+
+
+
diff --git a/addons/pos_discount/views/templates.xml b/addons/pos_discount/views/templates.xml
new file mode 100644
index 00000000000..0dbf22eb0fe
--- /dev/null
+++ b/addons/pos_discount/views/templates.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/pos_discount/views/views.xml b/addons/pos_discount/views/views.xml
new file mode 100644
index 00000000000..3448e0b2b6f
--- /dev/null
+++ b/addons/pos_discount/views/views.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ pos.config.form.view
+ pos.config
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/restaurant/__init__.py b/addons/restaurant/__init__.py
new file mode 100644
index 00000000000..381469e26fc
--- /dev/null
+++ b/addons/restaurant/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+import restaurant
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/addons/restaurant/__openerp__.py b/addons/restaurant/__openerp__.py
new file mode 100644
index 00000000000..dbf735b800e
--- /dev/null
+++ b/addons/restaurant/__openerp__.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+
+{
+ 'name': 'Restaurant',
+ 'version': '1.0',
+ 'category': 'Point of Sale',
+ 'sequence': 6,
+ 'summary': 'Restaurant extensions for the Point of Sale ',
+ 'description': """
+
+=======================
+
+This module adds several restaurant features to the Point of Sale:
+- Bill Printing: Allows you to print a receipt before the order is paid
+- Bill Splitting: Allows you to split an order into different orders
+- Kitchen Order Printing: allows you to print orders updates to kitchen or bar printers
+
+""",
+ 'author': 'OpenERP SA',
+ 'depends': ['point_of_sale'],
+ 'data': [
+ 'restaurant_view.xml',
+ 'security/ir.model.access.csv',
+ 'views/templates.xml',
+ ],
+ 'qweb':[
+ 'static/src/xml/multiprint.xml',
+ 'static/src/xml/splitbill.xml',
+ 'static/src/xml/printbill.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/restaurant/restaurant.py b/addons/restaurant/restaurant.py
new file mode 100644
index 00000000000..c7d02a336c1
--- /dev/null
+++ b/addons/restaurant/restaurant.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+import logging
+
+import openerp
+from openerp import tools
+from openerp.osv import fields, osv
+from openerp.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+class restaurant_printer(osv.osv):
+ _name = 'restaurant.printer'
+
+ _columns = {
+ 'name' : fields.char('Printer Name', size=32, required=True, help='An internal identification of the printer'),
+ 'proxy_ip': fields.char('Proxy IP Address', size=32, help="The IP Address or hostname of the Printer's hardware proxy"),
+ 'product_categories_ids': fields.many2many('pos.category','printer_category_rel', 'printer_id','category_id',string='Printed Product Categories'),
+ }
+
+ _defaults = {
+ 'name' : 'Printer',
+ }
+
+class pos_config(osv.osv):
+ _inherit = 'pos.config'
+ _columns = {
+ 'iface_splitbill': fields.boolean('Bill Splitting', help='Enables Bill Splitting in the Point of Sale'),
+ 'iface_printbill': fields.boolean('Bill Printing', help='Allows to print the Bill before payment'),
+ 'printer_ids': fields.many2many('restaurant.printer','pos_config_printer_rel', 'config_id','printer_id',string='Order Printers'),
+ }
+ _defaults = {
+ 'iface_splitbill': False,
+ 'iface_printbill': False,
+ }
+
diff --git a/addons/restaurant/restaurant_view.xml b/addons/restaurant/restaurant_view.xml
new file mode 100644
index 00000000000..84adc883b94
--- /dev/null
+++ b/addons/restaurant/restaurant_view.xml
@@ -0,0 +1,74 @@
+
+
+
+
+ Order Printer
+ restaurant.printer
+
+
+
+
+
+
+ Order Printers
+ ir.actions.act_window
+ restaurant.printer
+ form
+ tree,form
+
+
+ Click to add a Restaurant Order Printer.
+
+ Order Printers are used by restaurants and bars to print the
+ order updates in the kitchen/bar when the waiter updates the order.
+
+ 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.
+
+
+
+
+
+ Order Printers
+ restaurant.printer
+
+
+
+
+
+
+
+
+
+
+
+
+ pos.config.form.view.inherit
+ pos.config
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/restaurant/security/ir.model.access.csv b/addons/restaurant/security/ir.model.access.csv
new file mode 100644
index 00000000000..31c7f956f86
--- /dev/null
+++ b/addons/restaurant/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_restaurant_printer,restaurant.printer.user,model_restaurant_printer,point_of_sale.group_pos_user,1,0,0,0
+access_restaurant_printer_manager,restaurant.printer.manager,model_restaurant_printer,point_of_sale.group_pos_manager,1,0,0,0
diff --git a/addons/restaurant/static/src/css/restaurant.css b/addons/restaurant/static/src/css/restaurant.css
new file mode 100644
index 00000000000..0ffebfc0a46
--- /dev/null
+++ b/addons/restaurant/static/src/css/restaurant.css
@@ -0,0 +1,2 @@
+/* --- Restaurant Specific CSS --- */
+
diff --git a/addons/restaurant/static/src/js/main.js b/addons/restaurant/static/src/js/main.js
new file mode 100644
index 00000000000..4b8d25368a5
--- /dev/null
+++ b/addons/restaurant/static/src/js/main.js
@@ -0,0 +1,11 @@
+openerp.restaurant = function(instance){
+
+ var module = instance.point_of_sale;
+
+ openerp_restaurant_multiprint(instance,module);
+
+ openerp_restaurant_splitbill(instance,module);
+
+ openerp_restaurant_printbill(instance,module);
+
+};
diff --git a/addons/restaurant/static/src/js/multiprint.js b/addons/restaurant/static/src/js/multiprint.js
new file mode 100644
index 00000000000..4a86e4e5a31
--- /dev/null
+++ b/addons/restaurant/static/src/js/multiprint.js
@@ -0,0 +1,216 @@
+function openerp_restaurant_multiprint(instance,module){
+ var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
+
+ module.Printer = instance.web.Class.extend(openerp.PropertiesMixin,{
+ init: function(parent,options){
+ openerp.PropertiesMixin.init.call(this,parent);
+ var self = this;
+ options = options || {};
+ var url = options.url || 'http://localhost:8069';
+ this.connection = new instance.web.Session(undefined,url, { use_cors: true});
+ this.host = url;
+ this.receipt_queue = [];
+ },
+ print: function(receipt){
+ var self = this;
+ if(receipt){
+ this.receipt_queue.push(receipt);
+ }
+ var aborted = false;
+ function send_printing_job(){
+ if(self.receipt_queue.length > 0){
+ var r = self.receipt_queue.shift();
+ self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
+ .then(function(){
+ send_printing_job();
+ },function(){
+ self.receipt_queue.unshift(r);
+ });
+ }
+ }
+ send_printing_job();
+ },
+ });
+
+ module.PosModel.prototype.models.push({
+ model: 'restaurant.printer',
+ fields: ['name','proxy_ip','product_categories_ids'],
+ domain: null,
+ loaded: function(self,printers){
+ var active_printers = {};
+ for (var i = 0; i < self.config.printer_ids.length; i++) {
+ active_printers[self.config.printer_ids[i]] = true;
+ }
+
+ self.printers = [];
+ for(var i = 0; i < printers.length; i++){
+ if(active_printers[printers[i].id]){
+ var printer = new module.Printer(self,{url:'http://'+printers[i].proxy_ip+':8069'});
+ printer.config = printers[i];
+ self.printers.push(printer);
+ }
+ }
+ },
+ });
+
+ module.Order = module.Order.extend({
+ lineResume: function(){
+ var resume = {};
+ this.get('orderLines').each(function(item){
+ var line = item.export_as_JSON();
+ if( typeof resume[line.product_id] === 'undefined'){
+ resume[line.product_id] = line.qty;
+ }else{
+ resume[line.product_id] += line.qty;
+ }
+ });
+ return resume;
+ },
+ saveChanges: function(){
+ this.old_resume = this.lineResume();
+ },
+ computeChanges: function(categories){
+ var current = this.lineResume();
+ var old = this.old_resume || {};
+ var json = this.export_as_JSON();
+ var add = [];
+ var rem = [];
+
+ for( product in current){
+ if (typeof old[product] === 'undefined'){
+ add.push({
+ 'id': product,
+ 'name': this.pos.db.get_product_by_id(product).name,
+ 'quantity': current[product],
+ });
+ }else if( old[product] < current[product]){
+ add.push({
+ 'id': product,
+ 'name': this.pos.db.get_product_by_id(product).name,
+ 'quantity': current[product] - old[product],
+ });
+ }else if( old[product] > current[product]){
+ rem.push({
+ 'id': product,
+ 'name': this.pos.db.get_product_by_id(product).name,
+ 'quantity': old[product] - current[product],
+ });
+ }
+ }
+
+ for( product in old){
+ if(typeof current[product] === 'undefined'){
+ rem.push({
+ 'id': product,
+ 'name': this.pos.db.get_product_by_id(product).name,
+ 'quantity': old[product],
+ });
+ }
+ }
+
+ if(categories && categories.length > 0){
+ // filter the added and removed orders to only contains
+ // products that belong to one of the categories supplied as a parameter
+
+ var self = this;
+ function product_in_category(product_id){
+ var cat = self.pos.db.get_product_by_id(product_id).pos_categ_id[0];
+ while(cat){
+ for(var i = 0; i < categories.length; i++){
+ if(cat === categories[i]){
+ return true;
+ }
+ }
+ cat = self.pos.db.get_category_parent_id(cat);
+ }
+ return false;
+ }
+
+ var _add = [];
+ var _rem = [];
+
+ for(var i = 0; i < add.length; i++){
+ if(product_in_category(add[i].id)){
+ _add.push(add[i]);
+ }
+ }
+ add = _add;
+
+ for(var i = 0; i < rem.length; i++){
+ if(product_in_category(rem[i].id)){
+ _rem.push(rem[i]);
+ }
+ }
+ rem = _rem;
+ }
+
+ return {
+ 'new': add,
+ 'cancelled': rem,
+ 'table': json.table || 'unknown table',
+ 'name': json.name || 'unknown order',
+ 'sequence_number': json.sequence_number || 0,
+ };
+
+ },
+ printChanges: function(){
+ var printers = this.pos.printers;
+ for(var i = 0; i < printers.length; i++){
+ var changes = this.computeChanges(printers[i].config.product_categories_ids);
+ if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
+ var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
+ printers[i].print(receipt);
+ }
+ }
+ },
+ hasChangesToPrint: function(){
+ var printers = this.pos.printers;
+ for(var i = 0; i < printers.length; i++){
+ var changes = this.computeChanges(printers[i].config.product_categories_ids);
+ if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
+ return true;
+ }
+ }
+ return false;
+ },
+ });
+
+ module.PosWidget.include({
+ build_widgets: function(){
+ var self = this;
+ this._super();
+
+ if(this.pos.printers.length){
+ var submitorder = $(QWeb.render('SubmitOrderButton'));
+
+ submitorder.click(function(){
+ var order = self.pos.get('selectedOrder');
+ if(order.hasChangesToPrint()){
+ order.printChanges();
+ order.saveChanges();
+ self.pos_widget.order_widget.update_summary();
+ }
+ });
+
+ submitorder.appendTo(this.$('.control-buttons'));
+ this.$('.control-buttons').removeClass('oe_hidden');
+ }
+ },
+
+ });
+
+ module.OrderWidget.include({
+ update_summary: function(){
+ this._super();
+ var order = this.pos.get('selectedOrder');
+
+ if(order.hasChangesToPrint()){
+ this.pos_widget.$('.order-submit').addClass('highlight');
+ }else{
+ this.pos_widget.$('.order-submit').removeClass('highlight');
+ }
+ },
+ });
+
+}
diff --git a/addons/restaurant/static/src/js/printbill.js b/addons/restaurant/static/src/js/printbill.js
new file mode 100644
index 00000000000..89bf9db7bc7
--- /dev/null
+++ b/addons/restaurant/static/src/js/printbill.js
@@ -0,0 +1,28 @@
+function openerp_restaurant_printbill(instance,module){
+ var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
+
+ module.PosWidget.include({
+ build_widgets: function(){
+ var self = this;
+ this._super();
+
+ if(this.pos.config.iface_printbill){
+ var printbill = $(QWeb.render('PrintBillButton'));
+
+ printbill.click(function(){
+ var order = self.pos.get('selectedOrder');
+ if(order.get('orderLines').models.length > 0){
+ var receipt = order.export_for_printing();
+ self.pos.proxy.print_receipt(QWeb.render('BillReceipt',{
+ receipt: receipt, widget: self,
+ }));
+ }
+ });
+
+ printbill.appendTo(this.$('.control-buttons'));
+ this.$('.control-buttons').removeClass('oe_hidden');
+ }
+ },
+ });
+}
diff --git a/addons/restaurant/static/src/js/splitbill.js b/addons/restaurant/static/src/js/splitbill.js
new file mode 100644
index 00000000000..81591ccc589
--- /dev/null
+++ b/addons/restaurant/static/src/js/splitbill.js
@@ -0,0 +1,193 @@
+function openerp_restaurant_splitbill(instance, module){
+ var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
+
+ module.SplitbillScreenWidget = module.ScreenWidget.extend({
+ template: 'SplitbillScreenWidget',
+
+ show_leftpane: false,
+ previous_screen: 'products',
+
+ renderElement: function(){
+ var self = this;
+ this._super();
+ var order = this.pos.get('selectedOrder');
+ if(!order){
+ return;
+ }
+ var orderlines = order.get('orderLines').models;
+ for(var i = 0; i < orderlines.length; i++){
+ var line = orderlines[i];
+ linewidget = $(QWeb.render('SplitOrderline',{
+ widget:this,
+ line:line,
+ selected: false,
+ quantity: 0,
+ id: line.id,
+ }));
+ linewidget.data('id',line.id);
+ this.$('.orderlines').append(linewidget);
+ }
+ this.$('.back').click(function(){
+ self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
+ });
+ },
+
+ lineselect: function($el,order,neworder,splitlines,line_id){
+ var split = splitlines[line_id] || {'quantity': 0, line: null};
+ var line = order.getOrderline(line_id);
+
+ if( !line.get_unit().groupable ){
+ if( split.quantity !== line.get_quantity()){
+ split.quantity = line.get_quantity();
+ }else{
+ split.quantity = 0;
+ }
+ }else{
+ if( split.quantity < line.get_quantity()){
+ split.quantity += line.get_unit().rounding;
+ if(split.quantity > line.get_quantity()){
+ split.quantity = line.get_quantity();
+ }
+ }else{
+ split.quantity = 0;
+ }
+ }
+
+ if( split.quantity ){
+ if ( !split.line ){
+ split.line = line.clone();
+ neworder.addOrderline(split.line);
+ }
+ split.line.set_quantity(split.quantity);
+ }else if( split.line ) {
+ neworder.removeOrderline(split.line);
+ split.line = null;
+ }
+
+ splitlines[line_id] = split;
+ $el.replaceWith($(QWeb.render('SplitOrderline',{
+ widget: this,
+ line: line,
+ selected: split.quantity !== 0,
+ quantity: split.quantity,
+ id: line_id,
+ })));
+ this.$('.order-info .subtotal').text(this.format_currency(neworder.getSubtotal()));
+ },
+
+ pay: function($el,order,neworder,splitlines,cashregister_id){
+ var orderlines = order.get('orderLines').models;
+ var empty = true;
+ var full = true;
+
+ for(var i = 0; i < orderlines.length; i++){
+ var id = orderlines[i].id;
+ var split = splitlines[id];
+ if(!split){
+ full = false;
+ }else{
+ if(split.quantity){
+ empty = false;
+ if(split.quantity !== orderlines[i].get_quantity()){
+ full = false;
+ }
+ }
+ }
+ }
+
+ if(empty){
+ return;
+ }
+
+ for(var i = 0; i < this.pos.cashregisters.length; i++){
+ if(this.pos.cashregisters[i].id === cashregister_id){
+ var cashregister = this.pos.cashregisters[i];
+ break;
+ }
+ }
+
+ if(full){
+ order.addPaymentline(cashregister);
+ this.pos_widget.screen_selector.set_current_screen('payment');
+ }else{
+ for(var id in splitlines){
+ var split = splitlines[id];
+ var line = order.getOrderline(parseInt(id));
+ line.set_quantity(line.get_quantity() - split.quantity);
+ if(Math.abs(line.get_quantity()) < 0.00001){
+ order.removeOrderline(line);
+ }
+ delete splitlines[id];
+ }
+ neworder.addPaymentline(cashregister);
+ neworder.set_screen_data('screen','payment');
+
+ // for the kitchen printer we assume that everything
+ // has already been sent to the kitchen before splitting
+ // the bill. So we save all changes both for the old
+ // order and for the new one. This is not entirely correct
+ // but avoids flooding the kitchen with unnecessary orders.
+ // Not sure what to do in this case.
+
+ if ( neworder.saveChanges ) {
+ order.saveChanges();
+ neworder.saveChanges();
+ }
+
+ this.pos.get('orders').add(neworder);
+ this.pos.set('selectedOrder',neworder);
+ }
+ },
+ show: function(){
+ var self = this;
+ this._super();
+ this.renderElement();
+
+ var order = this.pos.get('selectedOrder');
+ var neworder = new module.Order({
+ pos: this.pos,
+ temporary: true,
+ });
+ neworder.set('client',order.get('client'));
+
+ var splitlines = {};
+
+ this.$('.orderlines').on('click','.orderline',function(){
+ var id = parseInt($(this).data('id'));
+ var $el = $(this);
+ self.lineselect($el,order,neworder,splitlines,id);
+ });
+
+ this.$('.paymentmethod').click(function(){
+ var id = parseInt($(this).data('id'));
+ var $el = $(this);
+ self.pay($el,order,neworder,splitlines,id);
+ });
+ },
+ });
+
+ module.PosWidget.include({
+ build_widgets: function(){
+ var self = this;
+ this._super();
+
+ if(this.pos.config.iface_splitbill){
+ this.splitbill_screen = new module.SplitbillScreenWidget(this,{});
+ this.splitbill_screen.appendTo(this.$('.screens'));
+ this.screen_selector.add_screen('splitbill',this.splitbill_screen);
+
+ var splitbill = $(QWeb.render('SplitbillButton'));
+
+ splitbill.click(function(){
+ if(self.pos.get('selectedOrder').get('orderLines').models.length > 0){
+ self.pos_widget.screen_selector.set_current_screen('splitbill');
+ }
+ });
+
+ splitbill.appendTo(this.$('.control-buttons'));
+ this.$('.control-buttons').removeClass('oe_hidden');
+ }
+ },
+ });
+}
diff --git a/addons/restaurant/static/src/xml/multiprint.xml b/addons/restaurant/static/src/xml/multiprint.xml
new file mode 100644
index 00000000000..63176f8938f
--- /dev/null
+++ b/addons/restaurant/static/src/xml/multiprint.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Order
+
+
+
+
+
+