1 openerp.point_of_sale = function(db) {
5 var __extends = function(child, parent) {
6 var __hasProp = Object.prototype.hasOwnProperty;
7 for (var key in parent) {
8 if (__hasProp.call(parent, key))
9 child[key] = parent[key];
12 this.constructor = child;
15 ctor.prototype = parent.prototype;
16 child.prototype = new ctor;
17 child.__super__ = parent.prototype;
21 var QWeb = db.web.qweb;
22 QWeb.add_template("/point_of_sale/static/src/xml/pos.xml");
23 var qweb_template = function(template) {
24 return function(ctx) {
25 return QWeb.render(template, ctx);
31 Local store access. Read once from localStorage upon construction and persist on every change.
32 There should only be one store active at any given time to ensure data consistency.
34 var Store = db.web.Class.extend({
38 get: function(key, _default) {
39 if (this.data[key] === undefined) {
40 var stored = localStorage['oe_pos_' + key];
42 this.data[key] = JSON.parse(stored);
46 return this.data[key];
48 set: function(key, value) {
49 this.data[key] = value;
50 localStorage['oe_pos_' + key] = JSON.stringify(value);
54 Gets all the necessary data from the OpenERP web client (session, shop data etc.)
56 var Pos = Backbone.Model.extend({
57 initialize: function(session, attributes) {
58 Backbone.Model.prototype.initialize.call(this, attributes);
59 this.store = new Store();
60 this.ready = $.Deferred();
61 this.flush_mutex = new $.Mutex();
62 this.build_tree = _.bind(this.build_tree, this);
63 this.session = session;
64 $.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']),
65 this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'img'], [['pos_categ_id', '!=', 'false']]),
66 this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name']),
67 this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type']))
68 .then(this.build_tree);
70 get: function(attribute) {
71 if (attribute === 'pending_operations') {
72 return this.store.get('pending_operations', []);
74 return Backbone.Model.prototype.get.call(this, attribute);
76 set: function(attributes, options) {
77 _.each(attributes, _.bind(function (value, attribute) {
78 if (attribute === 'pending_operations') {
79 this.store.set('pending_operations', value);
82 return Backbone.Model.prototype.set.call(this, attributes, options);
84 fetch: function(osvModel, fields, domain) {
87 dataSetSearch = new db.web.DataSetSearch(this, osvModel, {}, domain);
88 return dataSetSearch.read_slice(fields, 0).then(function(result) {
89 return self.store.set(osvModel, result);
92 push: function(osvModel, record) {
93 var ops = this.get('pending_operations');
94 ops.push({model: osvModel, record: record});
95 this.set({pending_operations: ops});
99 return this.flush_mutex.exec(_.bind(function() {
100 return this._int_flush();
103 _int_flush : function() {
104 var ops = this.get('pending_operations');
105 if (ops.length === 0)
108 var dataSet = new db.web.DataSet(this, op.model, null);
109 /* we prevent the default error handler and assume errors
110 * are a normal use case, except we stop the current iteration
112 return dataSet.create(op.record).fail(function(unused, event) {
113 event.preventDefault();
114 }).pipe(_.bind(function() {
115 console.debug('saved 1 record');
116 var ops2 = this.get('pending_operations');
117 this.set({'pending_operations': _.without(ops2, op)});
118 return this._int_flush();
119 }, this), function() {return $.when()});
122 build_tree: function() {
123 var c, id, _i, _len, _ref, _ref2;
124 _ref = this.store.get('pos.category');
125 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
127 this.categories[c.id] = {
130 children: c.child_id,
131 parent: c.parent_id[0],
136 _ref2 = this.categories;
139 this.current_category = c;
140 this.build_ancestors(c.parent);
141 this.build_subtree(c);
143 this.categories[0] = {
145 children: (function() {
146 var _j, _len2, _ref3, _results;
147 _ref3 = this.store.get('pos.category');
149 for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
151 if (!(c.parent_id[0] != null)) {
157 subtree: (function() {
158 var _j, _len2, _ref3, _results;
159 _ref3 = this.store.get('pos.category');
161 for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
168 return this.ready.resolve();
170 build_ancestors: function(parent) {
171 if (parent != null) {
172 this.current_category.ancestors.unshift(parent);
173 return this.build_ancestors(this.categories[parent].parent);
176 build_subtree: function(category) {
177 var c, _i, _len, _ref, _results;
178 _ref = category.children;
180 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
182 this.current_category.subtree.push(c);
183 _results.push(this.build_subtree(this.categories[c]));
189 /* global variable */
197 var CashRegister = (function() {
198 __extends(CashRegister, Backbone.Model);
199 function CashRegister() {
200 CashRegister.__super__.constructor.apply(this, arguments);
205 var CashRegisterCollection = (function() {
206 __extends(CashRegisterCollection, Backbone.Collection);
207 function CashRegisterCollection() {
208 CashRegisterCollection.__super__.constructor.apply(this, arguments);
211 CashRegisterCollection.prototype.model = CashRegister;
212 return CashRegisterCollection;
214 var Product = (function() {
215 __extends(Product, Backbone.Model);
217 Product.__super__.constructor.apply(this, arguments);
222 var ProductCollection = (function() {
223 __extends(ProductCollection, Backbone.Collection);
224 function ProductCollection() {
225 ProductCollection.__super__.constructor.apply(this, arguments);
228 ProductCollection.prototype.model = Product;
229 return ProductCollection;
231 var Category = (function() {
232 __extends(Category, Backbone.Model);
233 function Category() {
234 Category.__super__.constructor.apply(this, arguments);
239 var CategoryCollection = (function() {
240 __extends(CategoryCollection, Backbone.Collection);
241 function CategoryCollection() {
242 CategoryCollection.__super__.constructor.apply(this, arguments);
245 CategoryCollection.prototype.model = Category;
246 return CategoryCollection;
249 Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
250 There should only ever be one Orderline per distinct product in an Order.
251 To add more of the same product, just update the quantity accordingly.
252 The Order also contains payment information.
254 var Orderline = Backbone.Model.extend({
260 initialize: function(attributes) {
261 Backbone.Model.prototype.initialize.apply(this, arguments);
262 this.bind('change:quantity', function(unused, qty) {
264 this.trigger('killme');
267 incrementQuantity: function() {
269 quantity: (this.get('quantity')) + 1
272 getTotal: function() {
273 return (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
275 exportAsJSON: function() {
278 qty: this.get('quantity'),
279 price_unit: this.get('list_price'),
280 discount: this.get('discount'),
281 product_id: this.get('id')
286 var OrderlineCollection = Backbone.Collection.extend({
290 Every PaymentLine has all the attributes of the corresponding CashRegister.
292 var Paymentline = (function() {
293 __extends(Paymentline, Backbone.Model);
294 function Paymentline() {
295 Paymentline.__super__.constructor.apply(this, arguments);
298 Paymentline.prototype.defaults = {
301 Paymentline.prototype.getAmount = function() {
302 return this.get('amount');
304 Paymentline.prototype.exportAsJSON = function() {
307 name: "Payment line",
308 statement_id: this.get('id'),
309 account_id: (this.get('account_id'))[0],
310 journal_id: (this.get('journal_id'))[0],
311 amount: this.getAmount()
317 var PaymentlineCollection = (function() {
318 __extends(PaymentlineCollection, Backbone.Collection);
319 function PaymentlineCollection() {
320 PaymentlineCollection.__super__.constructor.apply(this, arguments);
323 PaymentlineCollection.prototype.model = Paymentline;
324 return PaymentlineCollection;
326 var Order = (function() {
327 __extends(Order, Backbone.Model);
329 Order.__super__.constructor.apply(this, arguments);
332 Order.prototype.defaults = {
336 Order.prototype.initialize = function() {
338 orderLines: new OrderlineCollection
341 paymentLines: new PaymentlineCollection
343 this.bind('change:validated', this.validatedChanged);
345 name: "Order " + this.generateUniqueId()
348 Order.prototype.events = {
349 'change:validated': 'validatedChanged'
351 Order.prototype.validatedChanged = function() {
352 if (this.get("validated") && !this.previous("validated")) {
353 this.set({'step': 'receipt'});
356 Order.prototype.generateUniqueId = function() {
357 return new Date().getTime();
359 Order.prototype.addProduct = function(product) {
361 existing = (this.get('orderLines')).get(product.id);
362 if (existing != null) {
363 existing.incrementQuantity();
365 var line = new Orderline(product.toJSON());
366 this.get('orderLines').add(line);
367 line.bind('killme', function() {
368 this.get('orderLines').remove(line);
372 Order.prototype.addPaymentLine = function(cashRegister) {
374 newPaymentline = new Paymentline(cashRegister);
375 /* TODO: Should be 0 for cash-like accounts */
377 amount: this.getDueLeft()
379 return (this.get('paymentLines')).add(newPaymentline);
381 Order.prototype.getName = function() {
382 return this.get('name');
384 Order.prototype.getTotal = function() {
385 return (this.get('orderLines')).reduce((function(sum, orderLine) {
386 return sum + orderLine.getTotal();
389 Order.prototype.getTotalTaxExcluded = function() {
390 return this.getTotal() / 1.21;
392 Order.prototype.getTax = function() {
393 return this.getTotal() / 1.21 * 0.21;
395 Order.prototype.getPaidTotal = function() {
396 return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
397 return sum + paymentLine.getAmount();
400 Order.prototype.getChange = function() {
401 return this.getPaidTotal() - this.getTotal();
403 Order.prototype.getDueLeft = function() {
404 return this.getTotal() - this.getPaidTotal();
406 Order.prototype.exportAsJSON = function() {
407 var orderLines, paymentLines, result;
409 (this.get('orderLines')).each(_.bind( function(item) {
410 return orderLines.push([0, 0, item.exportAsJSON()]);
413 (this.get('paymentLines')).each(_.bind( function(item) {
414 return paymentLines.push([0, 0, item.exportAsJSON()]);
417 name: this.getName(),
418 amount_paid: this.getPaidTotal(),
419 amount_total: this.getTotal(),
420 amount_tax: this.getTax(),
421 amount_return: this.getChange(),
423 statement_ids: paymentLines
429 var OrderCollection = (function() {
430 __extends(OrderCollection, Backbone.Collection);
431 function OrderCollection() {
432 OrderCollection.__super__.constructor.apply(this, arguments);
435 OrderCollection.prototype.model = Order;
436 return OrderCollection;
438 var Shop = (function() {
439 __extends(Shop, Backbone.Model);
441 Shop.__super__.constructor.apply(this, arguments);
444 Shop.prototype.initialize = function() {
446 orders: new OrderCollection(),
447 products: new ProductCollection()
450 cashRegisters: new CashRegisterCollection(pos.store.get('account.bank.statement')),
452 return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
453 if ((this.get('orders')).isEmpty()) {
454 this.addAndSelectOrder(new Order);
456 if ((this.get('selectedOrder')) === removedOrder) {
458 selectedOrder: (this.get('orders')).last()
463 Shop.prototype.addAndSelectOrder = function(newOrder) {
464 (this.get('orders')).add(newOrder);
466 selectedOrder: newOrder
472 The numpad handles both the choice of the property currently being modified
473 (quantity, price or discount) and the edition of the corresponding numeric value.
475 var NumpadState = (function() {
476 __extends(NumpadState, Backbone.Model);
477 function NumpadState() {
478 NumpadState.__super__.constructor.apply(this, arguments);
481 NumpadState.prototype.defaults = {
485 NumpadState.prototype.initialize = function(options) {
486 this.shop = options.shop;
487 return this.shop.bind('change:selectedOrder', this.reset, this);
489 NumpadState.prototype.appendNewChar = function(newChar) {
491 oldBuffer = this.get('buffer');
492 if (oldBuffer === '0') {
496 } else if (oldBuffer === '-0') {
498 buffer: "-" + newChar
502 buffer: (this.get('buffer')) + newChar
505 return this.updateTarget();
507 NumpadState.prototype.deleteLastChar = function() {
509 tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
510 if (isNaN(tempNewBuffer)) {
514 buffer: tempNewBuffer
516 return this.updateTarget();
518 NumpadState.prototype.switchSign = function() {
520 oldBuffer = this.get('buffer');
522 buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
524 return this.updateTarget();
526 NumpadState.prototype.changeMode = function(newMode) {
532 NumpadState.prototype.reset = function() {
537 NumpadState.prototype.updateTarget = function() {
538 var bufferContent, params;
539 bufferContent = this.get('buffer');
540 if (bufferContent && !isNaN(bufferContent)) {
542 params[this.get('mode')] = parseFloat(bufferContent);
543 return (this.shop.get('selectedOrder')).selected.set(params);
553 var NumpadWidget = db.web.Widget.extend({
554 init: function(parent, options) {
556 this.state = options.state;
559 this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
560 this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
561 this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
562 this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
564 clickDeleteLastChar: function() {
565 return this.state.deleteLastChar();
567 clickSwitchSign: function() {
568 return this.state.switchSign();
570 clickAppendNewChar: function(event) {
572 newChar = event.currentTarget.innerText;
573 return this.state.appendNewChar(newChar);
575 clickChangeMode: function(event) {
577 $('.selected-mode').removeClass('selected-mode');
578 $(event.currentTarget).addClass('selected-mode');
579 newMode = event.currentTarget.attributes['data-mode'].nodeValue;
580 return this.state.changeMode(newMode);
584 Gives access to the payment methods (aka. 'cash registers')
586 var PaypadWidget = db.web.Widget.extend({
587 init: function(parent, options) {
589 this.shop = options.shop;
592 this.$element.find('button').click(_.bind(this.performPayment, this));
594 performPayment: function(event) {
595 var cashRegister, cashRegisterCollection, cashRegisterId;
596 /* set correct view */
597 this.shop.get('selectedOrder').set({'step': 'payment'});
599 cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
600 cashRegisterCollection = this.shop.get('cashRegisters');
601 cashRegister = cashRegisterCollection.find(_.bind( function(item) {
602 return (item.get('id')) === parseInt(cashRegisterId, 10);
604 return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
606 render_element: function() {
607 this.$element.empty();
608 return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
609 var button = new PaymentButtonWidget();
610 button.model = cashRegister;
611 button.appendTo(this.$element);
615 var PaymentButtonWidget = db.web.Widget.extend({
616 template_fct: qweb_template('pos-payment-button-template'),
617 render_element: function() {
618 this.$element.html(this.template_fct({
619 id: this.model.get('id'),
620 name: (this.model.get('journal_id'))[1]
626 There are 3 steps in a POS workflow:
627 1. prepare the order (i.e. chose products, quantities etc.)
628 2. choose payment method(s) and amount(s)
629 3. validae order and print receipt
630 It should be possible to go back to any step as long as step 3 hasn't been completed.
631 Modifying an order after validation shouldn't be allowed.
633 var StepsWidget = db.web.Widget.extend({
634 init: function(parent, options) {
636 this.shop = options.shop;
638 this.shop.bind('change:selectedOrder', this.change_order, this);
640 change_order: function() {
641 if (this.selected_order) {
642 this.selected_order.unbind('change:step', this.change_step);
644 this.selected_order = this.shop.get('selectedOrder');
645 if (this.selected_order) {
646 this.selected_order.bind('change:step', this.change_step, this);
650 change_step: function() {
651 var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
652 $('.step-screen').hide();
653 $('#' + new_step + '-screen').show();
659 var OrderlineWidget = db.web.Widget.extend({
661 template_fct: qweb_template('pos-orderline-template'),
662 init: function(parent, options) {
664 this.model = options.model;
665 this.model.bind('change', _.bind( function() {
666 this.$element.hide();
667 this.render_element();
669 this.model.bind('remove', _.bind( function() {
670 return this.$element.remove();
672 this.order = options.order;
673 this.numpadState = options.numpadState;
676 this.$element.click(_.bind(this.clickHandler, this));
678 clickHandler: function() {
679 this.numpadState.reset();
680 return this.select();
682 render_element: function() {
684 return this.$element.html(this.template_fct(this.model.toJSON())).fadeIn(400, function() {
685 return $('#current-order').scrollTop($(this).offset().top);
689 $('tr.selected').removeClass('selected');
690 this.$element.addClass('selected');
691 return this.order.selected = this.model;
694 var OrderWidget = db.web.Widget.extend({
695 init: function(parent, options) {
697 this.shop = options.shop;
698 this.numpadState = options.numpadState;
699 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
700 this.bindOrderLineEvents();
702 changeSelectedOrder: function() {
703 this.currentOrderLines.unbind();
704 this.bindOrderLineEvents();
705 this.render_element();
707 bindOrderLineEvents: function() {
708 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
709 this.currentOrderLines.bind('add', this.addLine, this);
710 this.currentOrderLines.bind('remove', this.render_element, this);
712 addLine: function(newLine) {
713 var line = new OrderlineWidget(null, {
715 order: this.shop.get('selectedOrder'),
716 numpadState: this.numpadState
718 line.appendTo(this.$element);
719 this.updateSummary();
721 render_element: function() {
722 this.$element.empty();
723 this.currentOrderLines.each(_.bind( function(orderLine) {
724 var line = new OrderlineWidget(null, {
726 order: this.shop.get('selectedOrder'),
727 numpadState: this.numpadState
729 line.appendTo(this.$element);
731 this.updateSummary();
733 updateSummary: function() {
734 var currentOrder, tax, total, totalTaxExcluded;
735 currentOrder = this.shop.get('selectedOrder');
736 total = currentOrder.getTotal();
737 totalTaxExcluded = currentOrder.getTotalTaxExcluded();
738 tax = currentOrder.getTax();
739 $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
740 $('#tax').html(tax.toFixed(2)).hide().fadeIn();
741 $('#total').html(total.toFixed(2)).hide().fadeIn();
747 var CategoryWidget = db.web.Widget.extend({
749 this.$element.find(".oe-pos-categories-list a").click(_.bind(this.changeCategory, this));
751 template_fct: qweb_template('pos-category-template'),
752 render_element: function() {
755 this.$element.html(this.template_fct({
756 breadcrumb: (function() {
757 var _i, _len, _results;
759 for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
760 c = self.ancestors[_i];
761 _results.push(pos.categories[c]);
765 categories: (function() {
766 var _i, _len, _results;
768 for (_i = 0, _len = self.children.length; _i < _len; _i++) {
769 c = self.children[_i];
770 _results.push(pos.categories[c]);
776 changeCategory: function(a) {
777 var id = $(a.target).data("category-id");
778 this.on_change_category(id);
780 on_change_category: function(id) {},
782 var ProductWidget = db.web.Widget.extend({
784 template_fct: qweb_template('pos-product-template'),
785 init: function(parent, options) {
787 this.model = options.model;
788 this.shop = options.shop;
790 start: function(options) {
791 $("a", this.$element).click(_.bind(this.addToOrder, this));
793 addToOrder: function(event) {
794 /* Preserve the category URL */
795 event.preventDefault();
796 return (this.shop.get('selectedOrder')).addProduct(this.model);
798 render_element: function() {
799 this.$element.addClass("product");
800 this.$element.html(this.template_fct(this.model.toJSON()));
804 var ProductListWidget = db.web.Widget.extend({
805 init: function(parent, options) {
807 this.model = options.model;
808 this.shop = options.shop;
809 this.shop.get('products').bind('reset', this.render_element, this);
811 render_element: function() {
812 this.$element.empty();
813 (this.shop.get('products')).each(_.bind( function(product) {
814 var p = new ProductWidget(null, {
818 p.appendTo(this.$element);
826 var PaymentlineWidget = db.web.Widget.extend({
828 template_fct: qweb_template('pos-paymentline-template'),
829 init: function(parent, options) {
831 this.model = options.model;
832 this.model.bind('change', this.render_element, this);
835 this.$element.addClass('paymentline');
836 $('input', this.$element).keyup(_.bind(this.changeAmount, this));
838 changeAmount: function(event) {
840 newAmount = event.currentTarget.value;
841 if (newAmount && !isNaN(newAmount)) {
842 return this.model.set({
843 amount: parseFloat(newAmount)
847 render_element: function() {
848 this.$element.html(this.template_fct({
849 name: (this.model.get('journal_id'))[1],
850 amount: this.model.get('amount')
855 var PaymentWidget = db.web.Widget.extend({
856 init: function(parent, options) {
858 this.model = options.model;
859 this.shop = options.shop;
860 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
861 this.bindPaymentLineEvents();
862 this.bindOrderLineEvents();
864 paymentLineList: function() {
865 return this.$element.find('#paymentlines');
868 $('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
870 validateCurrentOrder: function() {
871 var callback, currentOrder;
872 currentOrder = this.shop.get('selectedOrder');
873 $('button#validate-order', this.$element).attr('disabled', 'disabled');
874 pos.push('pos.order', currentOrder.exportAsJSON()).then(_.bind(function() {
875 $('button#validate-order', this.$element).removeAttr('disabled');
876 return currentOrder.set({
881 bindPaymentLineEvents: function() {
882 this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
883 this.currentPaymentLines.bind('add', this.addPaymentLine, this);
884 this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
886 bindOrderLineEvents: function() {
887 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
888 this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
890 changeSelectedOrder: function() {
891 this.currentPaymentLines.unbind();
892 this.bindPaymentLineEvents();
893 this.currentOrderLines.unbind();
894 this.bindOrderLineEvents();
895 this.render_element();
897 addPaymentLine: function(newPaymentLine) {
898 var x = new PaymentlineWidget(null, {
899 model: newPaymentLine
901 x.appendTo(this.paymentLineList());
903 render_element: function() {
904 this.paymentLineList().empty();
905 this.currentPaymentLines.each(_.bind( function(paymentLine) {
906 var x = new PaymentlineWidget(null, {
909 this.paymentLineList().append(x);
911 this.updatePaymentSummary();
913 updatePaymentSummary: function() {
914 var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
915 currentOrder = this.shop.get('selectedOrder');
916 paidTotal = currentOrder.getPaidTotal();
917 dueTotal = currentOrder.getTotal();
918 this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
919 this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
920 remainingAmount = dueTotal - paidTotal;
921 remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
922 $('#payment-remaining').html(remaining);
928 var ReceiptLineWidget = db.web.Widget.extend({
930 template_fct: qweb_template('pos-receiptline-template'),
931 init: function(parent, options) {
933 this.model = options.model;
934 this.model.bind('change', this.render_element, this);
936 render_element: function() {
937 this.$element.addClass('receiptline');
938 this.$element.html(this.template_fct(this.model.toJSON()));
941 var ReceiptWidget = db.web.Widget.extend({
942 init: function(parent, options) {
944 this.model = options.model;
945 this.shop = options.shop;
946 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
947 this.bindOrderLineEvents();
948 this.bindPaymentLineEvents();
950 finishOrder: function() {
951 this.shop.get('selectedOrder').destroy();
953 receiptLineList: function() {
954 return this.$element.find('#receiptlines');
956 bindOrderLineEvents: function() {
957 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
958 this.currentOrderLines.bind('add', this.addReceiptLine, this);
959 this.currentOrderLines.bind('change', this.render_element, this);
960 this.currentOrderLines.bind('remove', this.render_element, this);
962 bindPaymentLineEvents: function() {
963 this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
964 this.currentPaymentLines.bind('all', this.updateReceiptSummary, this);
966 changeSelectedOrder: function() {
967 this.currentOrderLines.unbind();
968 this.bindOrderLineEvents();
969 this.currentPaymentLines.unbind();
970 this.bindPaymentLineEvents();
971 this.render_element();
973 addReceiptLine: function(newOrderItem) {
974 var x = new ReceiptLineWidget(null, {
977 x.appendTo(this.receiptLineList());
978 this.updateReceiptSummary();
980 render_element: function() {
981 this.$element.html(qweb_template('pos-receipt-view'));
982 $('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
983 this.currentOrderLines.each(_.bind( function(orderItem) {
984 var x = new ReceiptLineWidget(null, {
987 x.appendTo(this.receiptLineList());
989 this.updateReceiptSummary();
991 updateReceiptSummary: function() {
992 var change, currentOrder, tax, total;
993 currentOrder = this.shop.get('selectedOrder');
994 total = currentOrder.getTotal();
995 tax = currentOrder.getTax();
996 change = currentOrder.getPaidTotal() - total;
997 $('#receipt-summary-tax').html(tax.toFixed(2));
998 $('#receipt-summary-total').html(total.toFixed(2));
999 $('#receipt-summary-change').html(change.toFixed(2));
1002 var OrderButtonWidget = db.web.Widget.extend({
1004 template_fct: qweb_template('pos-order-selector-button-template'),
1005 init: function(parent, options) {
1006 this._super(parent);
1007 this.order = options.order;
1008 this.shop = options.shop;
1009 this.order.bind('destroy', _.bind( function() {
1012 this.shop.bind('change:selectedOrder', _.bind( function(shop) {
1014 selectedOrder = shop.get('selectedOrder');
1015 if (this.order === selectedOrder) {
1016 this.setButtonSelected();
1021 $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
1022 $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
1024 selectOrder: function(event) {
1026 selectedOrder: this.order
1029 setButtonSelected: function() {
1030 $('.selected-order').removeClass('selected-order');
1031 this.$element.addClass('selected-order');
1033 closeOrder: function(event) {
1034 this.order.destroy();
1036 render_element: function() {
1037 this.$element.html(this.template_fct(this.order.toJSON()));
1038 this.$element.addClass('order-selector-button');
1041 var ShopWidget = db.web.Widget.extend({
1042 init: function(parent, options) {
1043 this._super(parent);
1044 this.shop = options.shop;
1047 $('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
1049 (this.shop.get('orders')).bind('add', this.orderAdded, this);
1050 (this.shop.get('orders')).add(new Order);
1051 this.numpadState = new NumpadState({
1054 this.productListView = new ProductListWidget(null, {
1057 this.productListView.$element = $("#products-screen-ol");
1058 this.productListView.render_element();
1059 this.productListView.start();
1060 this.paypadView = new PaypadWidget(null, {
1063 this.paypadView.$element = $('#paypad');
1064 this.paypadView.render_element();
1065 this.paypadView.start();
1066 this.orderView = new OrderWidget(null, {
1068 numpadState: this.numpadState
1070 this.orderView.$element = $('#current-order-content');
1071 this.orderView.start();
1072 this.paymentView = new PaymentWidget(null, {
1075 this.paymentView.$element = $('#payment-screen');
1076 this.paymentView.render_element();
1077 this.paymentView.start();
1078 this.receiptView = new ReceiptWidget(null, {
1081 this.receiptView.replace($('#receipt-screen'));
1082 this.numpadView = new NumpadWidget(null, {
1083 state: this.numpadState
1085 this.numpadView.$element = $('#numpad');
1086 this.numpadView.start();
1087 this.stepsView = new StepsWidget(null, {shop: this.shop});
1088 this.stepsView.$element = $('#steps');
1089 this.stepsView.start();
1091 createNewOrder: function() {
1093 newOrder = new Order;
1094 (this.shop.get('orders')).add(newOrder);
1096 selectedOrder: newOrder
1099 orderAdded: function(newOrder) {
1101 newOrderButton = new OrderButtonWidget(null, {
1105 newOrderButton.appendTo($('#orders'));
1106 newOrderButton.selectOrder();
1109 var App = (function() {
1110 function App($element) {
1111 this.initialize($element);
1114 App.prototype.initialize = function($element) {
1115 this.shop = new Shop;
1116 this.shopView = new ShopWidget(null, {
1119 this.shopView.$element = $element;
1120 this.shopView.start();
1121 this.categoryView = new CategoryWidget(null, 'products-screen-categories');
1122 this.categoryView.on_change_category.add_last(_.bind(this.category, this));
1125 App.prototype.category = function(id) {
1130 c = pos.categories[id];
1131 this.categoryView.ancestors = c.ancestors;
1132 this.categoryView.children = c.children;
1133 this.categoryView.render_element();
1134 this.categoryView.start();
1135 products = pos.store.get('product.product').filter( function(p) {
1137 return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
1139 (this.shop.get('products')).reset(products);
1141 $('.searchbox input').keyup(function() {
1143 s = $(this).val().toLowerCase();
1145 m = products.filter( function(p) {
1146 return p.name.toLowerCase().indexOf(s) != -1;
1148 $('.search-clear').fadeIn();
1151 $('.search-clear').fadeOut();
1153 return (self.shop.get('products')).reset(m);
1155 return $('.search-clear').click( function() {
1156 (this.shop.get('products')).reset(products);
1157 $('.searchbox input').val('').focus();
1158 return $('.search-clear').fadeOut();
1164 db.point_of_sale.SynchNotification = db.web.Widget.extend({
1165 template: "pos-synch-notification",
1167 this._super.apply(this, arguments);
1168 this.nbr_pending = 0;
1170 render_element: function() {
1171 this._super.apply(this, arguments);
1172 $('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
1174 on_change_nbr_pending: function(nbr_pending) {
1175 this.nbr_pending = nbr_pending;
1176 this.render_element();
1178 on_synch: function() {}
1181 db.web.client_actions.add('pos.ui', 'db.point_of_sale.PointOfSale');
1182 db.point_of_sale.PointOfSale = db.web.Widget.extend({
1183 template: "PointOfSale",
1188 throw "It is not possible to instantiate multiple instances "+
1189 "of the point of sale at the same time.";
1190 pos = new Pos(this.session);
1192 this.synch_notification = new db.point_of_sale.SynchNotification(this);
1193 this.synch_notification.replace($('.oe_pos_synch-notification', this.$element));
1194 this.synch_notification.on_synch.add(_.bind(pos.flush, pos));
1196 pos.bind('change:pending_operations', this.changed_pending_operations, this);
1197 this.changed_pending_operations();
1199 this.$element.find("#loggedas button").click(function() {
1203 this.$element.find('#steps').buttonset();
1205 $('.oe_toggle_secondary_menu').hide();
1206 $('.oe_footer').hide();
1208 return pos.ready.then( function() {
1209 pos.app = new App(self.$element);
1212 changed_pending_operations: function () {
1213 this.synch_notification.on_change_nbr_pending(pos.get('pending_operations').length);
1215 try_close: function() {
1216 pos.flush().then(_.bind(function() {
1217 var close = _.bind(this.close, this);
1218 if (pos.get('pending_operations').length > 0) {
1219 var confirm = false;
1220 $(QWeb.render('pos-close-warning')).dialog({
1228 $( this ).dialog( "close" );
1231 $( this ).dialog( "close" );
1248 $('.oe_footer').show();
1249 $('.oe_toggle_secondary_menu').show();