[WIP] point_of_sale: first test of new TPE API

bzr revid: fva@openerp.com-20120918133732-q7za0r3dcggem8mw
This commit is contained in:
Frédéric van der Essen 2012-09-18 15:37:32 +02:00
parent 8920abbb65
commit 968325cb39
5 changed files with 222 additions and 96 deletions

View File

@ -119,21 +119,21 @@ class PointOfSaleController(openerpweb.Controller):
return return
@openerpweb.jsonrequest @openerpweb.jsonrequest
def payment_request(self, request, price, method, info): def payment_request(self, request, price):
""" """
The PoS will activate the method payment The PoS will activate the method payment
""" """
print "payment_request: price:"+str(price)+" method:"+str(method)+" info:"+str(info) print "payment_request: price:"+str(price)
return return 'ok'
@openerpweb.jsonrequest @openerpweb.jsonrequest
def is_payment_accepted(self, request): def payment_status(self, request):
print "is_payment_accepted" print "payment_status"
return 'waiting_for_payment' return { 'status':'waiting' }
@openerpweb.jsonrequest @openerpweb.jsonrequest
def payment_canceled(self, request): def payment_cancel(self, request):
print "payment_canceled" print "payment_cancel"
return return
@openerpweb.jsonrequest @openerpweb.jsonrequest

View File

@ -14,7 +14,14 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.weighting = false; this.weighting = false;
this.paying = false; this.paying = false;
this.payment_status = 'waiting_for_payment'; this.default_payment_status = {
status: 'waiting',
message: '',
payment_method: undefined,
receipt_client: undefined,
receipt_shop: undefined,
};
this.custom_payment_status = this.default_payment_status;
this.connection = new instance.web.JsonRPC(); this.connection = new instance.web.JsonRPC();
this.connection.setup(url); this.connection.setup(url);
@ -23,16 +30,21 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.notifications = {}; this.notifications = {};
}, },
message : function(name,params,success_callback, error_callback){ message : function(name,params){
success_callback = success_callback || function(){}; var ret = new $.Deferred();
error_callback = error_callback || function(){};
var callbacks = this.notifications[name] || []; var callbacks = this.notifications[name] || [];
for(var i = 0; i < callbacks.length; i++){ for(var i = 0; i < callbacks.length; i++){
callbacks[i](params); callbacks[i](params);
} }
this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback); this.connection.rpc('/pos/'+name, params || {},
function(result){
ret.resolve(result);
},
function(error){
ret.reject(error);
});
return ret;
}, },
// this allows the client to be notified when a proxy call is made. The notification // this allows the client to be notified when a proxy call is made. The notification
@ -47,23 +59,23 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
//a product has been scanned and recognized with success //a product has been scanned and recognized with success
// ean is a parsed ean object // ean is a parsed ean object
scan_item_success: function(ean){ scan_item_success: function(ean){
this.message('scan_item_success',{ean: ean}); return this.message('scan_item_success',{ean: ean});
}, },
// a product has been scanned but not recognized // a product has been scanned but not recognized
// ean is a parsed ean object // ean is a parsed ean object
scan_item_error_unrecognized: function(ean){ scan_item_error_unrecognized: function(ean){
this.message('scan_item_error_unrecognized',{ean: ean}); return this.message('scan_item_error_unrecognized',{ean: ean});
}, },
//the client is asking for help //the client is asking for help
help_needed: function(){ help_needed: function(){
this.message('help_needed'); return this.message('help_needed');
}, },
//the client does not need help anymore //the client does not need help anymore
help_canceled: function(){ help_canceled: function(){
this.message('help_canceled'); return this.message('help_canceled');
}, },
//the client is starting to weight //the client is starting to weight
@ -72,7 +84,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.weight = 0; this.weight = 0;
this.weighting = true; this.weighting = true;
this.bypass_proxy = false; this.bypass_proxy = false;
this.message('weighting_start'); return this.message('weighting_start');
} }
}, },
@ -84,11 +96,12 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
if(this.bypass_proxy){ if(this.bypass_proxy){
return this.weight; return this.weight;
}else{ }else{
this.message('weighting_read_kg',{},function(weight){ this.message('weighting_read_kg',{})
if(self.weighting && !self.bypass_proxy){ .then(function(weight){
self.weight = weight; if(self.weighting && !self.bypass_proxy){
} self.weight = weight;
}); }
});
return this.weight; return this.weight;
} }
}, },
@ -104,77 +117,76 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.weight = 0; this.weight = 0;
this.weighting = false; this.weighting = false;
this.bypass_proxy = false; this.bypass_proxy = false;
this.message('weighting_end'); return this.message('weighting_end');
}, },
// the pos asks the client to pay 'price' units // the pos asks the client to pay 'price' units
// method: 'mastercard' | 'cash' | ... ? TBD payment_request: function(price){
// info: 'extra information to display on the payment terminal' ... ? TBD var ret = new $.Deferred();
payment_request: function(price, method, info){
this.paying = true; this.paying = true;
this.payment_status = 'waiting_for_payment'; this.custom_payment_status = this.default_payment_status;
this.message('payment_request',{'price':price,'method':method,'info':info}); return this.message('payment_request',{'price':price});
}, },
// is called at regular interval after a payment request to see if the client payment_status: function(){
// has paid the required money
// returns 'waiting_for_payment' | 'payment_accepted' | 'payment_rejected'
is_payment_accepted: function(){
var self = this;
if(this.bypass_proxy){ if(this.bypass_proxy){
this.bypass_proxy = false; this.bypass_proxy = false;
return this.payment_status; return (new $.Deferred()).resolve(this.custom_payment_status);
}else{ }else{
this.message('is_payment_accepted', {}, function(payment_status){ return this.message('payment_status');
if(self.paying){
self.payment_status = payment_status;
}
});
return this.payment_status;
} }
}, },
// override what the proxy says and accept the payment // override what the proxy says and accept the payment
debug_accept_payment: function(){ debug_accept_payment: function(){
this.bypass_proxy = true; this.bypass_proxy = true;
this.payment_status = 'payment_accepted'; this.custom_payment_status = {
status: 'paid',
message: 'Successfull Payment, have a nice day',
payment_method: 'AMEX',
receipt_client: '<xml>bla</xml>',
receipt_shop: '<xml>bla</xml>',
};
}, },
// override what the proxy says and reject the payment // override what the proxy says and reject the payment
debug_reject_payment: function(){ debug_reject_payment: function(){
this.bypass_proxy = true; this.bypass_proxy = true;
this.payment_status = 'payment_rejected'; this.custom_payment_status = {
status: 'error-rejected',
message: 'Sorry you don\'t have enough money :(',
};
}, },
// the client cancels his payment // the client cancels his payment
payment_canceled: function(){ payment_cancel: function(){
this.paying = false; this.paying = false;
this.payment_status = 'waiting_for_payment'; this.custom_payment_status = 'waiting_for_payment';
this.message('payment_canceled'); return this.message('payment_cancel');
}, },
// called when the client logs in or starts to scan product // called when the client logs in or starts to scan product
transaction_start: function(){ transaction_start: function(){
this.message('transaction_start'); return this.message('transaction_start');
}, },
// called when the clients has finished his interaction with the machine // called when the clients has finished his interaction with the machine
transaction_end: function(){ transaction_end: function(){
this.message('transaction_end'); return this.message('transaction_end');
}, },
// called when the POS turns to cashier mode // called when the POS turns to cashier mode
cashier_mode_activated: function(){ cashier_mode_activated: function(){
this.message('cashier_mode_activated'); return this.message('cashier_mode_activated');
}, },
// called when the POS turns to client mode // called when the POS turns to client mode
cashier_mode_deactivated: function(){ cashier_mode_deactivated: function(){
this.message('cashier_mode_deactivated'); return this.message('cashier_mode_deactivated');
}, },
// ask for the cashbox (the physical box where you store the cash) to be opened // ask for the cashbox (the physical box where you store the cash) to be opened
open_cashbox: function(){ open_cashbox: function(){
this.message('open_cashbox'); return this.message('open_cashbox');
}, },
/* ask the printer to print a receipt /* ask the printer to print a receipt
@ -216,12 +228,12 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
* } * }
*/ */
print_receipt: function(receipt){ print_receipt: function(receipt){
this.message('print_receipt',{receipt: receipt}); return this.message('print_receipt',{receipt: receipt});
}, },
// asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server ) // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
print_pdf_invoice: function(pdfinvoice){ print_pdf_invoice: function(pdfinvoice){
this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice}); return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
}, },
}); });

View File

@ -154,7 +154,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}, },
barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
barcode_product_error_popup: 'error', //if defined, this popup will be loaded when there's an error in the popup barcode_product_error_popup: 'error-product', //if defined, this popup will be loaded when there's an error in the popup
// what happens when a product is scanned : // what happens when a product is scanned :
// it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
@ -411,6 +411,9 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
self.pos_widget.screen_selector.set_user_mode('cashier'); self.pos_widget.screen_selector.set_user_mode('cashier');
}, },
}); });
this.$('.footer .button').off('click').click(function(){
self.pos_widget.screen_selector.close_popup();
});
}, },
close:function(){ close:function(){
this._super(); this._super();
@ -419,12 +422,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}, },
}); });
module.ErrorProductNotRecognizedPopupWidget = module.ErrorPopupWidget.extend({ module.ProductErrorPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorProductNotRecognizedPopupWidget', template:'ProductErrorPopupWidget',
}); });
module.ErrorNoSessionPopupWidget = module.ErrorPopupWidget.extend({ module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
template:'ErrorNoSessionPopupWidget', template:'ErrorSessionPopupWidget',
}); });
module.ScaleInviteScreenWidget = module.ScreenWidget.extend({ module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
@ -539,6 +542,60 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}, },
}); });
// the JobQueue schedules a sequence of 'jobs'. each job is
// a function returning a deferred. the queue waits for each job to finish
// before launching the next. Each job can also be scheduled with a delay.
// the queue jobqueue is used to prevent parallel requests to the payment terminal.
module.JobQueue = function(){
var queue = [];
var running = false;
var run = function(){
if(queue.length > 0){
running = true;
var job = queue.shift();
setTimeout(function(){
var def = job.fun();
if(def){
def.then(run);
}else{
run();
}
},job.delay || 0);
}else{
running = false;
}
};
// adds a job to the schedule.
this.schedule = function(fun, delay){
queue.push({fun:fun, delay:delay});
if(!running){
run();
}
}
// remove all jobs from the schedule
this.clear = function(){
queue = [];
};
};
/*
module.BasicPaymentScreen = module.ScreenWidget.extend({
queue: new JobQueue(),
start_payment_transaction: function(){
},
update_payment_transaction: function(){
},
cancel_payment_transaction: function(){
},
show: function(){
this._super();
},
});
*/
module.ClientPaymentScreenWidget = module.ScreenWidget.extend({ module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
template:'ClientPaymentScreenWidget', template:'ClientPaymentScreenWidget',
@ -548,50 +605,102 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(){ show: function(){
this._super(); this._super();
var self = this; var self = this;
this.queue = new module.JobQueue();
this.canceled = false;
this.paid = false;
this.pos.proxy.payment_request(this.pos.get('selectedOrder').getDueLeft(),'card','info'); //TODO TOTAL // initiates the connection to the payment terminal and starts the update requests
this.start = function(){
this.intervalID = setInterval(function(){ var def = new $.Deferred();
var payment = self.pos.proxy.is_payment_accepted(); console.log("START");
if(payment === 'payment_accepted'){ self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
clearInterval(this.intervalID); .then(function(ack){
if(ack === 'ok'){
var currentOrder = self.pos.get('selectedOrder'); self.queue.schedule(self.update);
}else if(ack.indexOf('error') === 0){
//we get the first cashregister marked as self-checkout console.error('cannot make payment. TODO');
var selfCheckoutRegisters = []; }else{
for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){ console.error('unknown payment request return value:',ack);
var cashregister = self.pos.get('cashRegisters').models[i];
if(cashregister.self_checkout_payment_method){
selfCheckoutRegisters.push(cashregister);
} }
} console.log("START_END");
def.resolve();
var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; });
currentOrder.addPaymentLine(cashregister); return def;
self.pos.push_order(currentOrder.exportAsJSON()) };
currentOrder.destroy();
self.pos.proxy.transaction_end(); // gets updated status from the payment terminal and performs the appropriate consequences
self.pos_widget.screen_selector.set_current_screen(self.next_screen); this.update = function(){
}else if(payment === 'payment_rejected'){ console.log("UPDATE");
clearInterval(self.intervalID); var def = new $.Deferred();
//TODO show a tryagain thingie ? if(self.canceled){
console.log("UPDATE_END");
return def.resolve();
} }
},500); self.pos.proxy.payment_status()
.then(function(status){
if(status.status === 'paid'){
var currentOrder = self.pos.get('selectedOrder');
//we get the first cashregister marked as self-checkout
var selfCheckoutRegisters = [];
for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){
var cashregister = self.pos.get('cashRegisters').models[i];
if(cashregister.self_checkout_payment_method){
selfCheckoutRegisters.push(cashregister);
}
}
var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0];
currentOrder.addPaymentLine(cashregister);
self.pos.push_order(currentOrder.exportAsJSON())
currentOrder.destroy();
self.pos.proxy.transaction_end();
self.pos_widget.screen_selector.set_current_screen(self.next_screen);
self.paid = true;
}else if(status.status.indexOf('error') === 0){
console.error('error in payment request. TODO');
}else if(status.status === 'waiting'){
self.queue.schedule(self.update,200);
}else{
console.error('unknown status value:',status.status);
}
console.log("UPDATE_END");
def.resolve();
});
return def;
}
// cancels a payment.
this.cancel = function(){
console.log("CANCEL");
if(!self.paid && !self.canceled){
self.canceled = true;
self.pos.proxy.payment_cancel();
self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
self.queue.clear();
}
console.log("CANCEL_END");
return (new $.Deferred()).resolve();
}
this.queue.schedule(this.start);
this.add_action_button({ this.add_action_button({
label: 'back', label: 'back',
icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
click: function(){ click: function(){
clearInterval(this.intervalID); self.queue.schedule(self.cancel);
self.pos.proxy.payment_canceled();
self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
} }
}); });
}, },
close: function(){ close: function(){
if(this.queue){
this.queue.schedule(this.cancel);
}
//TODO CANCEL
this._super(); this._super();
clearInterval(this.intervalID);
}, },
}); });

View File

@ -687,7 +687,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'print_receipt', 'print_receipt',
'print_pdf_invoice', 'print_pdf_invoice',
'weighting_read_kg', 'weighting_read_kg',
'is_payment_accepted', 'payment_status',
], ],
minimized: false, minimized: false,
start: function(){ start: function(){
@ -898,10 +898,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.error_popup = new module.ErrorPopupWidget(this, {}); this.error_popup = new module.ErrorPopupWidget(this, {});
this.error_popup.appendTo($('.point-of-sale')); this.error_popup.appendTo($('.point-of-sale'));
this.error_product_popup = new module.ErrorProductNotRecognizedPopupWidget(this, {}); this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
this.error_product_popup.appendTo($('.point-of-sale')); this.error_product_popup.appendTo($('.point-of-sale'));
this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {}); this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
this.error_session_popup.appendTo($('.point-of-sale')); this.error_session_popup.appendTo($('.point-of-sale'));
this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {}); this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});

View File

@ -327,15 +327,20 @@
</div> </div>
</t> </t>
<t t-name="ErrorProductNotRecognizedPopupWidget"> <t t-name="ProductErrorPopupWidget">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="popup popup-help"> <div class="popup popup-help">
<p class="message">The scanned product was not recognized<br /> Please wait, a cashier is on the way</p> <p class="message">The scanned product was not recognized<br /> Please wait, a cashier is on the way</p>
</div> </div>
</div> </div>
<div class="footer">
<div class="button">
Ok
</div>
</div>
</t> </t>
<t t-name="ErrorNoSessionPopupWidget"> <t t-name="ErrorSessionPopupWidget">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="popup popup-help"> <div class="popup popup-help">
<p class="message">Sorry, we could not create a session for this user.</p> <p class="message">Sorry, we could not create a session for this user.</p>
@ -455,11 +460,11 @@
<li class="event scan_item_success">Scan Item Success</li> <li class="event scan_item_success">Scan Item Success</li>
<li class="event scan_item_error_unrecognized">Scan Item Unrecognized</li> <li class="event scan_item_error_unrecognized">Scan Item Unrecognized</li>
<li class="event payment_request">Payment Request</li> <li class="event payment_request">Payment Request</li>
<li class="event payment_status">Payment Status</li>
<li class="event open_cashbox">Open Cashbox</li> <li class="event open_cashbox">Open Cashbox</li>
<li class="event print_receipt">Print Receipt</li> <li class="event print_receipt">Print Receipt</li>
<li class="event print_pdf_invoice">Print Invoice</li> <li class="event print_pdf_invoice">Print Invoice</li>
<li class="event weighting_read_kg">Read Weighting Scale</li> <li class="event weighting_read_kg">Read Weighting Scale</li>
<li class="event is_payment_accepted">Check Payment</li>
</ul> </ul>
</div> </div>
</div> </div>