1
openerp.point_of_sale = function(db) {
5
var QWeb = db.web.qweb;
6
var qweb_template = function(template) {
8
return QWeb.render(template, _.extend({}, ctx,{
9
'currency': pos.get('currency'),
10
'format_amount': function(amount) {
11
if (pos.get('currency').position == 'after') {
12
return amount + ' ' + pos.get('currency').symbol;
14
return pos.get('currency').symbol + ' ' + amount;
23
Local store access. Read once from localStorage upon construction and persist on every change.
24
There should only be one store active at any given time to ensure data consistency.
26
var Store = db.web.Class.extend({
30
get: function(key, _default) {
31
if (this.data[key] === undefined) {
32
var stored = localStorage['oe_pos_' + key];
34
this.data[key] = JSON.parse(stored);
38
return this.data[key];
40
set: function(key, value) {
41
this.data[key] = value;
42
localStorage['oe_pos_' + key] = JSON.stringify(value);
46
Gets all the necessary data from the OpenERP web client (session, shop data etc.)
48
var Pos = Backbone.Model.extend({
49
initialize: function(session, attributes) {
50
Backbone.Model.prototype.initialize.call(this, attributes);
51
this.store = new Store();
52
this.ready = $.Deferred();
53
this.flush_mutex = new $.Mutex();
54
this.build_tree = _.bind(this.build_tree, this);
55
this.session = session;
57
'pending_operations': [],
58
'currency': {symbol: '$', position: 'after'},
63
_.each(attributes, _.bind(function(def, attr) {
65
to_set[attr] = this.store.get(attr, def);
67
this.bind('change:' + attr, _.bind(function(unused, val) {
68
this.store.set(attr, val);
71
$.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']),
72
this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'product_image_small', 'ean13', 'id'], [['pos_categ_id', '!=', false]]),
73
this.fetch('product.packaging', ['product_id', 'ean']),
74
this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
75
[['state', '=', 'open'], ['user_id', '=', this.session.uid]]),
76
this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type']),
77
this.fetch('account.tax', ['amount', 'price_include', 'type']),
79
.pipe(_.bind(this.build_tree, this));
81
fetch: function(osvModel, fields, domain) {
84
dataSetSearch = new db.web.DataSetSearch(this, osvModel, {}, domain);
85
return dataSetSearch.read_slice(fields, 0).then(function(result) {
86
return self.store.set(osvModel, result);
89
get_app_data: function() {
91
return $.when(new db.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
92
self.set({'shop': result[0]});
93
var company_id = result[0]['company_id'][0];
94
return new db.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
95
self.set({'company': result});
96
var currency_id = result['currency_id'][0]
97
return new db.web.Model("res.currency").get_func("read")([currency_id],
98
['symbol', 'position']).pipe(function(result) {
99
self.set({'currency': result[0]});
103
}), new db.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
104
self.set({'user': result});
107
pushOrder: function(record) {
108
var ops = _.clone(this.get('pending_operations'));
110
this.set({pending_operations: ops});
114
return this.flush_mutex.exec(_.bind(function() {
115
return this._int_flush();
118
_int_flush : function() {
119
var ops = this.get('pending_operations');
120
if (ops.length === 0)
123
/* we prevent the default error handler and assume errors
124
* are a normal use case, except we stop the current iteration
126
return new db.web.Model("pos.order").get_func("create_from_ui")([op]).fail(function(unused, event) {
127
event.preventDefault();
128
}).pipe(_.bind(function() {
129
console.debug('saved 1 record');
130
var ops2 = this.get('pending_operations');
131
this.set({'pending_operations': _.without(ops2, op)});
132
return this._int_flush();
133
}, this), function() {return $.when()});
136
build_tree: function() {
137
var c, id, _i, _len, _ref, _ref2;
138
_ref = this.store.get('pos.category');
139
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
141
this.categories[c.id] = {
144
children: c.child_id,
145
parent: c.parent_id[0],
150
_ref2 = this.categories;
153
this.current_category = c;
154
this.build_ancestors(c.parent);
155
this.build_subtree(c);
157
this.categories[0] = {
159
children: (function() {
160
var _j, _len2, _ref3, _results;
161
_ref3 = this.store.get('pos.category');
163
for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
165
if (!(c.parent_id[0] != null)) {
171
subtree: (function() {
172
var _j, _len2, _ref3, _results;
173
_ref3 = this.store.get('pos.category');
175
for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
182
return this.ready.resolve();
184
build_ancestors: function(parent) {
185
if (parent != null) {
186
this.current_category.ancestors.unshift(parent);
187
return this.build_ancestors(this.categories[parent].parent);
190
build_subtree: function(category) {
191
var c, _i, _len, _ref, _results;
192
_ref = category.children;
194
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
196
this.current_category.subtree.push(c);
197
_results.push(this.build_subtree(this.categories[c]));
203
/* global variable */
212
var CashRegister = Backbone.Model.extend({
215
var CashRegisterCollection = Backbone.Collection.extend({
219
var Product = Backbone.Model.extend({
222
var ProductCollection = Backbone.Collection.extend({
226
var Category = Backbone.Model.extend({
229
var CategoryCollection = Backbone.Collection.extend({
234
Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
235
There should only ever be one Orderline per distinct product in an Order.
236
To add more of the same product, just update the quantity accordingly.
237
The Order also contains payment information.
239
var Orderline = Backbone.Model.extend({
245
initialize: function(attributes) {
246
Backbone.Model.prototype.initialize.apply(this, arguments);
247
this.bind('change:quantity', function(unused, qty) {
249
this.trigger('killme');
252
incrementQuantity: function() {
254
quantity: (this.get('quantity')) + 1
257
getPriceWithoutTax: function() {
258
return this.getAllPrices().priceWithoutTax;
260
getPriceWithTax: function() {
261
return this.getAllPrices().priceWithTax;
264
return this.getAllPrices().tax;
266
getAllPrices: function() {
268
var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
270
var totalNoTax = base;
272
var products = pos.store.get('product.product');
273
var product = _.detect(products, function(el) {return el.id === self.get('id');});
274
var taxes_ids = product.taxes_id;
275
var taxes = pos.store.get('account.tax');
277
_.each(taxes_ids, function(el) {
278
var tax = _.detect(taxes, function(t) {return t.id === el;});
279
if (tax.price_include) {
281
if (tax.type === "percent") {
282
tmp = base - (base / (1 + tax.amount));
283
} else if (tax.type === "fixed") {
284
tmp = tax.amount * self.get('quantity');
286
throw "This type of tax is not supported by the point of sale: " + tax.type;
292
if (tax.type === "percent") {
293
tmp = tax.amount * base;
294
} else if (tax.type === "fixed") {
295
tmp = tax.amount * self.get('quantity');
297
throw "This type of tax is not supported by the point of sale: " + tax.type;
304
"priceWithTax": totalTax,
305
"priceWithoutTax": totalNoTax,
309
exportAsJSON: function() {
311
qty: this.get('quantity'),
312
price_unit: this.get('list_price'),
313
discount: this.get('discount'),
314
product_id: this.get('id')
319
var OrderlineCollection = Backbone.Collection.extend({
323
// Every PaymentLine has all the attributes of the corresponding CashRegister.
324
var Paymentline = Backbone.Model.extend({
328
initialize: function(attributes) {
329
Backbone.Model.prototype.initialize.apply(this, arguments);
331
getAmount: function(){
332
return this.get('amount');
334
exportAsJSON: function(){
336
name: db.web.datetime_to_str(new Date()),
337
statement_id: this.get('id'),
338
account_id: (this.get('account_id'))[0],
339
journal_id: (this.get('journal_id'))[0],
340
amount: this.getAmount()
345
var PaymentlineCollection = Backbone.Collection.extend({
349
var Order = Backbone.Model.extend({
354
initialize: function(attributes){
355
Backbone.Model.prototype.initialize.apply(this, arguments);
357
creationDate: new Date,
358
orderLines: new OrderlineCollection,
359
paymentLines: new PaymentlineCollection,
360
name: "Order " + this.generateUniqueId(),
362
this.bind('change:validated', this.validatedChanged);
366
'change:validated': 'validatedChanged'
368
validatedChanged: function() {
369
if (this.get("validated") && !this.previous("validated")) {
370
this.set({'step': 'receipt'});
373
generateUniqueId: function() {
374
return new Date().getTime();
376
addProduct: function(product) {
378
existing = (this.get('orderLines')).get(product.id);
379
if (existing != null) {
380
existing.incrementQuantity();
382
var line = new Orderline(product.toJSON());
383
this.get('orderLines').add(line);
384
line.bind('killme', function() {
385
this.get('orderLines').remove(line);
389
addPaymentLine: function(cashRegister) {
391
newPaymentline = new Paymentline(cashRegister);
392
/* TODO: Should be 0 for cash-like accounts */
394
amount: this.getDueLeft()
396
return (this.get('paymentLines')).add(newPaymentline);
398
getName: function() {
399
return this.get('name');
401
getTotal: function() {
402
return (this.get('orderLines')).reduce((function(sum, orderLine) {
403
return sum + orderLine.getPriceWithTax();
406
getTotalTaxExcluded: function() {
407
return (this.get('orderLines')).reduce((function(sum, orderLine) {
408
return sum + orderLine.getPriceWithoutTax();
412
return (this.get('orderLines')).reduce((function(sum, orderLine) {
413
return sum + orderLine.getTax();
416
getPaidTotal: function() {
417
return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
418
return sum + paymentLine.getAmount();
421
getChange: function() {
422
return this.getPaidTotal() - this.getTotal();
424
getDueLeft: function() {
425
return this.getTotal() - this.getPaidTotal();
427
exportAsJSON: function() {
428
var orderLines, paymentLines;
430
(this.get('orderLines')).each(_.bind( function(item) {
431
return orderLines.push([0, 0, item.exportAsJSON()]);
434
(this.get('paymentLines')).each(_.bind( function(item) {
435
return paymentLines.push([0, 0, item.exportAsJSON()]);
438
name: this.getName(),
439
amount_paid: this.getPaidTotal(),
440
amount_total: this.getTotal(),
441
amount_tax: this.getTax(),
442
amount_return: this.getChange(),
444
statement_ids: paymentLines
449
var OrderCollection = Backbone.Collection.extend({
453
var Shop = Backbone.Model.extend({
454
initialize: function() {
456
orders: new OrderCollection(),
457
products: new ProductCollection()
460
cashRegisters: new CashRegisterCollection(pos.store.get('account.bank.statement')),
462
return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
463
if ((this.get('orders')).isEmpty()) {
464
this.addAndSelectOrder(new Order);
466
if ((this.get('selectedOrder')) === removedOrder) {
468
selectedOrder: (this.get('orders')).last()
473
addAndSelectOrder: function(newOrder) {
474
(this.get('orders')).add(newOrder);
476
selectedOrder: newOrder
482
The numpad handles both the choice of the property currently being modified
483
(quantity, price or discount) and the edition of the corresponding numeric value.
485
var NumpadState = Backbone.Model.extend({
490
appendNewChar: function(newChar) {
492
oldBuffer = this.get('buffer');
493
if (oldBuffer === '0') {
497
} else if (oldBuffer === '-0') {
499
buffer: "-" + newChar
503
buffer: (this.get('buffer')) + newChar
508
deleteLastChar: function() {
510
tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
511
if (isNaN(tempNewBuffer)) {
515
buffer: tempNewBuffer
519
switchSign: function() {
521
oldBuffer = this.get('buffer');
523
buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
527
changeMode: function(newMode) {
539
updateTarget: function() {
540
var bufferContent, params;
541
bufferContent = this.get('buffer');
542
if (bufferContent && !isNaN(bufferContent)) {
543
this.trigger('setValue', parseFloat(bufferContent));
553
var NumpadWidget = db.web.OldWidget.extend({
554
init: function(parent, options) {
556
this.state = new NumpadState();
559
this.state.bind('change:mode', this.changedMode, this);
561
this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
562
this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
563
this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
564
this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
566
clickDeleteLastChar: function() {
567
return this.state.deleteLastChar();
569
clickSwitchSign: function() {
570
return this.state.switchSign();
572
clickAppendNewChar: function(event) {
574
newChar = event.currentTarget.innerText || event.currentTarget.textContent;
575
return this.state.appendNewChar(newChar);
577
clickChangeMode: function(event) {
578
var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
579
return this.state.changeMode(newMode);
581
changedMode: function() {
582
var mode = this.state.get('mode');
583
$('.selected-mode').removeClass('selected-mode');
584
$(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
588
Gives access to the payment methods (aka. 'cash registers')
590
var PaypadWidget = db.web.OldWidget.extend({
591
init: function(parent, options) {
593
this.shop = options.shop;
596
this.$element.find('button').click(_.bind(this.performPayment, this));
598
performPayment: function(event) {
599
if (this.shop.get('selectedOrder').get('step') === 'receipt')
601
var cashRegister, cashRegisterCollection, cashRegisterId;
602
/* set correct view */
603
this.shop.get('selectedOrder').set({'step': 'payment'});
605
cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
606
cashRegisterCollection = this.shop.get('cashRegisters');
607
cashRegister = cashRegisterCollection.find(_.bind( function(item) {
608
return (item.get('id')) === parseInt(cashRegisterId, 10);
610
return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
612
render_element: function() {
613
this.$element.empty();
614
return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
615
var button = new PaymentButtonWidget();
616
button.model = cashRegister;
617
button.appendTo(this.$element);
621
var PaymentButtonWidget = db.web.OldWidget.extend({
622
template_fct: qweb_template('pos-payment-button-template'),
623
render_element: function() {
624
this.$element.html(this.template_fct({
625
id: this.model.get('id'),
626
name: (this.model.get('journal_id'))[1]
632
There are 3 steps in a POS workflow:
633
1. prepare the order (i.e. chose products, quantities etc.)
634
2. choose payment method(s) and amount(s)
635
3. validae order and print receipt
636
It should be possible to go back to any step as long as step 3 hasn't been completed.
637
Modifying an order after validation shouldn't be allowed.
639
var StepSwitcher = db.web.OldWidget.extend({
640
init: function(parent, options) {
642
this.shop = options.shop;
644
this.shop.bind('change:selectedOrder', this.change_order, this);
646
change_order: function() {
647
if (this.selected_order) {
648
this.selected_order.unbind('change:step', this.change_step);
650
this.selected_order = this.shop.get('selectedOrder');
651
if (this.selected_order) {
652
this.selected_order.bind('change:step', this.change_step, this);
656
change_step: function() {
657
var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
658
$('.step-screen').hide();
659
$('#' + new_step + '-screen').show();
665
var OrderlineWidget = db.web.OldWidget.extend({
667
template_fct: qweb_template('pos-orderline-template'),
668
init: function(parent, options) {
670
this.model = options.model;
671
this.model.bind('change', _.bind( function() {
674
this.model.bind('remove', _.bind( function() {
675
this.$element.remove();
677
this.order = options.order;
680
this.$element.click(_.bind(this.clickHandler, this));
683
clickHandler: function() {
686
render_element: function() {
687
this.$element.html(this.template_fct(this.model.toJSON()));
690
refresh: function() {
691
this.render_element();
692
var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
693
heights.push($('#current-order thead').outerHeight());
694
var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
695
$('#current-order').scrollTop(position);
698
$('tr.selected').removeClass('selected');
699
this.$element.addClass('selected');
700
this.order.selected = this.model;
703
on_selected: function() {},
706
var OrderWidget = db.web.OldWidget.extend({
707
init: function(parent, options) {
709
this.shop = options.shop;
710
this.setNumpadState(options.numpadState);
711
this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
712
this.bindOrderLineEvents();
714
setNumpadState: function(numpadState) {
715
if (this.numpadState) {
716
this.numpadState.unbind('setValue', this.setValue);
718
this.numpadState = numpadState;
719
if (this.numpadState) {
720
this.numpadState.bind('setValue', this.setValue, this);
721
this.numpadState.reset();
724
setValue: function(val) {
726
param[this.numpadState.get('mode')] = val;
727
var order = this.shop.get('selectedOrder');
728
if (order.get('orderLines').length !== 0) {
729
order.selected.set(param);
731
this.shop.get('selectedOrder').destroy();
734
changeSelectedOrder: function() {
735
this.currentOrderLines.unbind();
736
this.bindOrderLineEvents();
737
this.render_element();
739
bindOrderLineEvents: function() {
740
this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
741
this.currentOrderLines.bind('add', this.addLine, this);
742
this.currentOrderLines.bind('remove', this.render_element, this);
744
addLine: function(newLine) {
745
var line = new OrderlineWidget(null, {
747
order: this.shop.get('selectedOrder')
749
line.on_selected.add(_.bind(this.selectedLine, this));
751
line.appendTo(this.$element);
752
this.updateSummary();
754
selectedLine: function() {
756
if (this.currentSelected !== this.shop.get('selectedOrder').selected) {
759
this.currentSelected = this.shop.get('selectedOrder').selected;
760
if (reset && this.numpadState)
761
this.numpadState.reset();
762
this.updateSummary();
764
render_element: function() {
765
this.$element.empty();
766
this.currentOrderLines.each(_.bind( function(orderLine) {
767
var line = new OrderlineWidget(null, {
769
order: this.shop.get('selectedOrder')
771
line.on_selected.add(_.bind(this.selectedLine, this));
772
line.appendTo(this.$element);
774
this.updateSummary();
776
updateSummary: function() {
777
var currentOrder, tax, total, totalTaxExcluded;
778
currentOrder = this.shop.get('selectedOrder');
779
total = currentOrder.getTotal();
780
totalTaxExcluded = currentOrder.getTotalTaxExcluded();
781
tax = currentOrder.getTax();
782
$('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
783
$('#tax').html(tax.toFixed(2)).hide().fadeIn();
784
$('#total').html(total.toFixed(2)).hide().fadeIn();
791
var CategoryWidget = db.web.OldWidget.extend({
793
this.$element.find(".oe-pos-categories-list a").click(_.bind(this.changeCategory, this));
794
$("#products-screen-ol").css("top",$("#products-screen-categories").height()+"px");
796
template_fct: qweb_template('pos-category-template'),
797
render_element: function() {
800
this.$element.html(this.template_fct({
801
breadcrumb: (function() {
802
var _i, _len, _results;
804
for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
805
c = self.ancestors[_i];
806
_results.push(pos.categories[c]);
810
categories: (function() {
811
var _i, _len, _results;
813
for (_i = 0, _len = self.children.length; _i < _len; _i++) {
814
c = self.children[_i];
815
_results.push(pos.categories[c]);
821
changeCategory: function(a) {
822
var id = $(a.target).data("category-id");
823
this.on_change_category(id);
825
on_change_category: function(id) {},
828
var ProductWidget = db.web.OldWidget.extend({
830
template_fct: qweb_template('pos-product-template'),
831
init: function(parent, options) {
833
this.model = options.model;
834
this.shop = options.shop;
836
start: function(options) {
837
$("a", this.$element).click(_.bind(this.addToOrder, this));
839
addToOrder: function(event) {
840
/* Preserve the category URL */
841
event.preventDefault();
842
return (this.shop.get('selectedOrder')).addProduct(this.model);
844
render_element: function() {
845
this.$element.addClass("product");
846
this.$element.html(this.template_fct(this.model.toJSON()));
851
var ProductListWidget = db.web.OldWidget.extend({
852
init: function(parent, options) {
854
this.model = options.model;
855
this.shop = options.shop;
856
this.shop.get('products').bind('reset', this.render_element, this);
858
render_element: function() {
859
this.$element.empty();
860
(this.shop.get('products')).each(_.bind( function(product) {
861
var p = new ProductWidget(null, {
865
p.appendTo(this.$element);
873
var PaymentlineWidget = db.web.OldWidget.extend({
875
template_fct: qweb_template('pos-paymentline-template'),
876
init: function(parent, options) {
878
this.model = options.model;
879
this.model.bind('change', this.changedAmount, this);
881
on_delete: function() {},
882
changeAmount: function(event) {
884
newAmount = event.currentTarget.value;
885
if (newAmount && !isNaN(newAmount)) {
886
this.amount = parseFloat(newAmount);
892
changedAmount: function() {
893
if (this.amount !== this.model.get('amount'))
894
this.render_element();
896
render_element: function() {
897
this.amount = this.model.get('amount');
898
this.$element.html(this.template_fct({
899
name: (this.model.get('journal_id'))[1],
902
this.$element.addClass('paymentline');
903
$('input', this.$element).keyup(_.bind(this.changeAmount, this));
904
$('.delete-payment-line', this.$element).click(this.on_delete);
907
var PaymentWidget = db.web.OldWidget.extend({
908
init: function(parent, options) {
910
this.model = options.model;
911
this.shop = options.shop;
912
this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
913
this.bindPaymentLineEvents();
914
this.bindOrderLineEvents();
916
paymentLineList: function() {
917
return this.$element.find('#paymentlines');
920
$('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
921
$('.oe-back-to-products', this.$element).click(_.bind(this.back, this));
924
this.shop.get('selectedOrder').set({"step": "products"});
926
validateCurrentOrder: function() {
927
var callback, currentOrder;
928
currentOrder = this.shop.get('selectedOrder');
929
$('button#validate-order', this.$element).attr('disabled', 'disabled');
930
pos.pushOrder(currentOrder.exportAsJSON()).then(_.bind(function() {
931
$('button#validate-order', this.$element).removeAttr('disabled');
932
return currentOrder.set({
937
bindPaymentLineEvents: function() {
938
this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
939
this.currentPaymentLines.bind('add', this.addPaymentLine, this);
940
this.currentPaymentLines.bind('remove', this.render_element, this);
941
this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
943
bindOrderLineEvents: function() {
944
this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
945
this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
947
changeSelectedOrder: function() {
948
this.currentPaymentLines.unbind();
949
this.bindPaymentLineEvents();
950
this.currentOrderLines.unbind();
951
this.bindOrderLineEvents();
952
this.render_element();
954
addPaymentLine: function(newPaymentLine) {
955
var x = new PaymentlineWidget(null, {
956
model: newPaymentLine
958
x.on_delete.add(_.bind(this.deleteLine, this, x));
959
x.appendTo(this.paymentLineList());
961
render_element: function() {
962
this.paymentLineList().empty();
963
this.currentPaymentLines.each(_.bind( function(paymentLine) {
964
this.addPaymentLine(paymentLine);
966
this.updatePaymentSummary();
968
deleteLine: function(lineWidget) {
969
this.currentPaymentLines.remove([lineWidget.model]);
971
updatePaymentSummary: function() {
972
var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
973
currentOrder = this.shop.get('selectedOrder');
974
paidTotal = currentOrder.getPaidTotal();
975
dueTotal = currentOrder.getTotal();
976
this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
977
this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
978
remainingAmount = dueTotal - paidTotal;
979
remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
980
$('#payment-remaining').html(remaining);
982
setNumpadState: function(numpadState) {
983
if (this.numpadState) {
984
this.numpadState.unbind('setValue', this.setValue);
985
this.numpadState.unbind('change:mode', this.setNumpadMode);
987
this.numpadState = numpadState;
988
if (this.numpadState) {
989
this.numpadState.bind('setValue', this.setValue, this);
990
this.numpadState.bind('change:mode', this.setNumpadMode, this);
991
this.numpadState.reset();
992
this.setNumpadMode();
995
setNumpadMode: function() {
996
this.numpadState.set({mode: 'payment'});
998
setValue: function(val) {
999
this.currentPaymentLines.last().set({amount: val});
1003
var ReceiptWidget = db.web.OldWidget.extend({
1004
init: function(parent, options) {
1005
this._super(parent);
1006
this.model = options.model;
1007
this.shop = options.shop;
1008
this.user = pos.get('user');
1009
this.company = pos.get('company');
1010
this.shop_obj = pos.get('shop');
1013
this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
1014
this.changeSelectedOrder();
1016
render_element: function() {
1017
this.$element.html(qweb_template('pos-receipt-view'));
1018
$('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
1019
$('button#print-the-ticket', this.$element).click(_.bind(this.print, this));
1024
finishOrder: function() {
1025
this.shop.get('selectedOrder').destroy();
1027
changeSelectedOrder: function() {
1028
if (this.currentOrderLines)
1029
this.currentOrderLines.unbind();
1030
this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
1031
this.currentOrderLines.bind('add', this.refresh, this);
1032
this.currentOrderLines.bind('change', this.refresh, this);
1033
this.currentOrderLines.bind('remove', this.refresh, this);
1034
if (this.currentPaymentLines)
1035
this.currentPaymentLines.unbind();
1036
this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
1037
this.currentPaymentLines.bind('all', this.refresh, this);
1040
refresh: function() {
1041
this.currentOrder = this.shop.get('selectedOrder');
1042
$('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
1046
var OrderButtonWidget = db.web.OldWidget.extend({
1048
template_fct: qweb_template('pos-order-selector-button-template'),
1049
init: function(parent, options) {
1050
this._super(parent);
1051
this.order = options.order;
1052
this.shop = options.shop;
1053
this.order.bind('destroy', _.bind( function() {
1056
this.shop.bind('change:selectedOrder', _.bind( function(shop) {
1058
selectedOrder = shop.get('selectedOrder');
1059
if (this.order === selectedOrder) {
1060
this.setButtonSelected();
1065
$('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
1066
$('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
1068
selectOrder: function(event) {
1070
selectedOrder: this.order
1073
setButtonSelected: function() {
1074
$('.selected-order').removeClass('selected-order');
1075
this.$element.addClass('selected-order');
1077
closeOrder: function(event) {
1078
this.order.destroy();
1080
render_element: function() {
1081
this.$element.html(this.template_fct({widget:this}));
1082
this.$element.addClass('order-selector-button');
1086
var ShopWidget = db.web.OldWidget.extend({
1087
init: function(parent, options) {
1088
this._super(parent);
1089
this.shop = options.shop;
1092
$('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
1094
(this.shop.get('orders')).bind('add', this.orderAdded, this);
1095
(this.shop.get('orders')).add(new Order);
1096
this.productListView = new ProductListWidget(null, {
1099
this.productListView.$element = $("#products-screen-ol");
1100
this.productListView.render_element();
1101
this.productListView.start();
1102
this.paypadView = new PaypadWidget(null, {
1105
this.paypadView.$element = $('#paypad');
1106
this.paypadView.render_element();
1107
this.paypadView.start();
1108
this.numpadView = new NumpadWidget(null);
1109
this.numpadView.$element = $('#numpad');
1110
this.numpadView.start();
1111
this.orderView = new OrderWidget(null, {
1114
this.orderView.$element = $('#current-order-content');
1115
this.orderView.start();
1116
this.paymentView = new PaymentWidget(null, {
1119
this.paymentView.$element = $('#payment-screen');
1120
this.paymentView.render_element();
1121
this.paymentView.start();
1122
this.receiptView = new ReceiptWidget(null, {
1125
this.receiptView.replace($('#receipt-screen'));
1126
this.stepSwitcher = new StepSwitcher(this, {shop: this.shop});
1127
this.shop.bind('change:selectedOrder', this.changedSelectedOrder, this);
1128
this.changedSelectedOrder();
1130
createNewOrder: function() {
1132
newOrder = new Order;
1133
(this.shop.get('orders')).add(newOrder);
1135
selectedOrder: newOrder
1138
orderAdded: function(newOrder) {
1140
newOrderButton = new OrderButtonWidget(null, {
1144
newOrderButton.appendTo($('#orders'));
1145
newOrderButton.selectOrder();
1147
changedSelectedOrder: function() {
1148
if (this.currentOrder) {
1149
this.currentOrder.unbind('change:step', this.changedStep);
1151
this.currentOrder = this.shop.get('selectedOrder');
1152
this.currentOrder.bind('change:step', this.changedStep, this);
1155
changedStep: function() {
1156
var step = this.currentOrder.get('step');
1157
this.orderView.setNumpadState(null);
1158
this.paymentView.setNumpadState(null);
1159
if (step === 'products') {
1160
this.orderView.setNumpadState(this.numpadView.state);
1161
} else if (step === 'payment') {
1162
this.paymentView.setNumpadState(this.numpadView.state);
1167
var App = (function() {
1169
function App($element) {
1170
this.initialize($element);
1173
App.prototype.initialize = function($element) {
1174
this.shop = new Shop;
1175
this.shopView = new ShopWidget(null, {
1178
this.shopView.$element = $element;
1179
this.shopView.start();
1180
this.categoryView = new CategoryWidget(null, 'products-screen-categories');
1181
this.categoryView.on_change_category.add_last(_.bind(this.category, this));
1185
App.prototype.category = function(id) {
1186
var c, products, self = this;
1190
c = pos.categories[id];
1191
this.categoryView.ancestors = c.ancestors;
1192
this.categoryView.children = c.children;
1193
this.categoryView.render_element();
1194
this.categoryView.start();
1195
allProducts = pos.store.get('product.product');
1196
allPackages = pos.store.get('product.packaging');
1197
products = pos.store.get('product.product').filter( function(p) {
1199
return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
1201
(this.shop.get('products')).reset(products);
1204
//returns true if the code is a valid EAN codebar number by checking the control digit.
1205
var checkEan = function(code) {
1206
var st1 = code.slice();
1207
var st2 = st1.slice(0,st1.length-1).reverse();
1208
// some EAN13 barcodes have a length of 12, as they start by 0
1209
while (st2.length < 12) {
1214
$.each(st2, function() {
1215
if (countSt3%2 === 1) {
1223
$.each(st2, function() {
1224
if (countSt4%2 === 0) {
1229
var st5 = st3 + st4;
1230
var cd = (10 - (st5%10)) % 10;
1231
return code[code.length-1] === cd;
1234
var codeNumbers = [];
1236
// returns a product that has a packaging with an EAN matching to provided ean string.
1237
// returns undefined if no such product is found.
1238
var getProductByEAN = function(ean) {
1239
var prefix = ean.substring(0,2);
1240
var scannedProductModel = undefined;
1241
if (prefix in {'02':'', '22':'', '24':'', '26':'', '28':''}) {
1243
var itemCode = ean.substring(0,7);
1244
var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
1245
if (scannedPackaging !== undefined) {
1246
scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
1247
scannedProductModel.list_price = Number(ean.substring(7,12))/100;
1249
} else if (prefix in {'21':'','23':'','27':'','29':'','25':''}) {
1251
var weight = Number(barcode.substring(7,12))/1000;
1252
var itemCode = ean.substring(0,7);
1253
var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
1254
if (scannedPackaging !== undefined) {
1255
scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
1256
scannedProductModel.list_price *= weight;
1257
scannedProductModel.name += ' - ' + weight + ' Kg.';
1261
scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === ean;}); //TODO DOES NOT SCALE
1263
return scannedProductModel;
1266
// The barcode readers acts as a keyboard, we catch all keyup events and try to find a
1267
// barcode sequence in the typed keys, then act accordingly.
1268
$('body').delegate('','keyup', function (e){
1270
//We only care about numbers
1271
if (!isNaN(Number(String.fromCharCode(e.keyCode)))) {
1273
// The barcode reader sends keystrokes with a specific interval.
1274
// We look if the typed keys fit in the interval.
1275
if (codeNumbers.length==0) {
1276
timeStamp = new Date().getTime();
1278
if (lastTimeStamp + 30 < new Date().getTime()) {
1279
// not a barcode reader
1281
timeStamp = new Date().getTime();
1284
codeNumbers.push(e.keyCode - 48);
1285
lastTimeStamp = new Date().getTime();
1286
if (codeNumbers.length == 13) {
1288
if (!checkEan(codeNumbers)) {
1289
// barcode read error, raise warning
1290
$(QWeb.render('pos-scan-warning')).dialog({
1297
$( this ).dialog( "close" );
1303
var selectedOrder = self.shop.get('selectedOrder');
1304
var scannedProductModel = getProductByEAN(codeNumbers.join(''));
1305
if (scannedProductModel === undefined) {
1306
// product not recognized, raise warning
1307
$(QWeb.render('pos-scan-warning')).dialog({
1314
$( this ).dialog( "close" );
1320
selectedOrder.addProduct(new Product(scannedProductModel));
1331
$('.searchbox input').keyup(function() {
1333
s = $(this).val().toLowerCase();
1335
m = products.filter( function(p) {
1336
return p.name.toLowerCase().indexOf(s) != -1;
1338
$('.search-clear').fadeIn();
1341
$('.search-clear').fadeOut();
1343
return (self.shop.get('products')).reset(m);
1345
return $('.search-clear').click( function() {
1346
(self.shop.get('products')).reset(products);
1347
$('.searchbox input').val('').focus();
1348
return $('.search-clear').fadeOut();
1354
db.point_of_sale.SynchNotification = db.web.OldWidget.extend({
1355
template: "pos-synch-notification",
1357
this._super.apply(this, arguments);
1358
this.nbr_pending = 0;
1360
render_element: function() {
1361
this._super.apply(this, arguments);
1362
$('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
1364
on_change_nbr_pending: function(nbr_pending) {
1365
this.nbr_pending = nbr_pending;
1366
this.render_element();
1368
on_synch: function() {}
1371
db.web.client_actions.add('pos.ui', 'db.point_of_sale.PointOfSale');
1372
db.point_of_sale.PointOfSale = db.web.OldWidget.extend({
1374
this._super.apply(this, arguments);
1377
throw "It is not possible to instantiate multiple instances "+
1378
"of the point of sale at the same time.";
1379
pos = new Pos(this.session);
1383
return pos.ready.then(_.bind(function() {
1384
this.render_element();
1385
this.synch_notification = new db.point_of_sale.SynchNotification(this);
1386
this.synch_notification.replace($('.oe_pos_synch-notification', this.$element));
1387
this.synch_notification.on_synch.add(_.bind(pos.flush, pos));
1389
pos.bind('change:pending_operations', this.changed_pending_operations, this);
1390
this.changed_pending_operations();
1392
this.$element.find("#loggedas button").click(function() {
1396
pos.app = new App(self.$element);
1397
$('.oe_toggle_secondary_menu').hide();
1398
$('.oe_footer').hide();
1400
if (pos.store.get('account.bank.statement').length === 0)
1401
return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_open_statement']], ['res_id']).pipe(
1402
_.bind(function(res) {
1403
return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1404
var action = result.result;
1405
this.do_action(action);
1410
render: function() {
1411
return qweb_template("PointOfSale")();
1413
changed_pending_operations: function () {
1414
this.synch_notification.on_change_nbr_pending(pos.get('pending_operations').length);
1416
try_close: function() {
1417
pos.flush().then(_.bind(function() {
1418
var close = _.bind(this.close, this);
1419
if (pos.get('pending_operations').length > 0) {
1420
var confirm = false;
1421
$(QWeb.render('pos-close-warning')).dialog({
1429
$( this ).dialog( "close" );
1432
$( this ).dialog( "close" );
1446
// remove barcode reader event listener
1447
$('body').undelegate('', 'keyup')
1449
return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
1450
_.bind(function(res) {
1451
return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1452
var action = result.result;
1453
action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'default_home'}});
1454
this.do_action(action);
1459
$('.oe_footer').show();
1460
$('.oe_toggle_secondary_menu').show();