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 var qweb_template = function(template) {
23 return function(ctx) {
24 return QWeb.render(template, _.extend({}, ctx,{
25 'currency': pos.get('currency'),
26 'format_amount': function(amount) {
27 if (pos.get('currency').position == 'after') {
28 return amount + ' ' + pos.get('currency').symbol;
30 return pos.get('currency').symbol + ' ' + amount;
39 add_operation: function(operation) {},
40 remove_operation: function(id) {},
41 get_operations: function() {},
43 var LocalStorageDAO = db.web.Class.extend({
44 add_operation: function(operation) {
46 return $.async_when().pipe(function() {
47 var tmp = self._get('oe_pos_operations', []);
48 var last_id = self._get('oe_pos_operations_sequence', 1);
49 tmp.push({'id': last_id, 'data': operation});
50 self._set('oe_pos_operations', tmp);
51 self._set('oe_pos_operations_sequence', last_id + 1);
54 remove_operation: function(id) {
56 return $.async_when().pipe(function() {
57 var tmp = self._get('oe_pos_operations', []);
58 tmp = _.filter(tmp, function(el) {
61 self._set('oe_pos_operations', tmp);
64 get_operations: function() {
66 return $.async_when().pipe(function() {
67 return self._get('oe_pos_operations', []);
70 _get: function(key, default_) {
71 var txt = localStorage[key];
74 return JSON.parse(txt);
76 _set: function(key, value) {
77 localStorage[key] = JSON.stringify(value);
81 var fetch = function(osvModel, fields, domain) {
83 dataSetSearch = new db.web.DataSetSearch(null, osvModel, {}, domain);
84 return dataSetSearch.read_slice(fields, 0);
88 Gets all the necessary data from the OpenERP web client (session, shop data etc.)
90 var Pos = Backbone.Model.extend({
91 initialize: function(session, attributes) {
92 Backbone.Model.prototype.initialize.call(this, attributes);
93 this.dao = new LocalStorageDAO();
94 this.ready = $.Deferred();
95 this.flush_mutex = new $.Mutex();
96 this.build_tree = _.bind(this.build_tree, this);
97 this.session = session;
98 this.set({'nbr_pending_operations': 0,
99 'currency': {symbol: '$', position: 'after'},
105 var cat_def = fetch('pos.category', ['name', 'parent_id', 'child_id']).pipe(function(result) {
106 return self.set({'categories': result});
108 var prod_def = fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id',
109 'product_image_small'], [['pos_categ_id', '!=', 'false']]).then(function(result) {
110 return self.set({'product_list': result});
112 var bank_def = fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
113 [['state', '=', 'open'], ['user_id', '=', this.session.uid]]).then(function(result) {
114 return self.set({'bank_statements': result});
116 var tax_def = fetch('account.tax', ['amount', 'price_include', 'type']).then(function(result) {
117 return self.set({'taxes': result});
119 $.when(cat_def, prod_def, bank_def, tax_def, this.get_app_data(), this.flush())
120 .pipe(_.bind(this.build_tree, this));
122 get_app_data: function() {
124 return $.when(new db.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
125 self.set({'shop': result[0]});
126 var company_id = result[0]['company_id'][0];
127 return new db.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
128 self.set({'company': result});
129 var currency_id = result['currency_id'][0]
130 return new db.web.Model("res.currency").get_func("read")([currency_id],
131 ['symbol', 'position']).pipe(function(result) {
132 self.set({'currency': result[0]});
136 }), new db.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
137 self.set({'user': result});
140 pushOrder: function(record) {
142 return this.dao.add_operation(record).pipe(function() {
147 return this.flush_mutex.exec(_.bind(function() {
148 return this._int_flush();
151 _int_flush : function() {
153 this.dao.get_operations().pipe(function(ops) {
154 self.set({"nbr_pending_operations": ops.length});
155 if (ops.length === 0)
157 var op = ops[0].data;
158 var op_id = ops[0].id;
159 /* we prevent the default error handler and assume errors
160 * are a normal use case, except we stop the current iteration
162 return new db.web.Model("pos.order").get_func("create_from_ui")([op]).fail(function(unused, event) {
163 event.preventDefault();
165 console.debug('saved 1 record');
166 self.dao.remove_operation(op_id).pipe(function() {
167 return self._int_flush();
175 build_tree: function() {
176 var c, id, _i, _len, _ref, _ref2;
177 _ref = this.get('categories');
178 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
180 this.categories[c.id] = {
183 children: c.child_id,
184 parent: c.parent_id[0],
189 _ref2 = this.categories;
192 this.current_category = c;
193 this.build_ancestors(c.parent);
194 this.build_subtree(c);
196 this.categories[0] = {
198 children: (function() {
199 var _j, _len2, _ref3, _results;
200 _ref3 = this.get('categories');
202 for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
204 if (!(c.parent_id[0] != null)) {
210 subtree: (function() {
211 var _j, _len2, _ref3, _results;
212 _ref3 = this.get('categories');
214 for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
221 return this.ready.resolve();
223 build_ancestors: function(parent) {
224 if (parent != null) {
225 this.current_category.ancestors.unshift(parent);
226 return this.build_ancestors(this.categories[parent].parent);
229 build_subtree: function(category) {
230 var c, _i, _len, _ref, _results;
231 _ref = category.children;
233 for (_i = 0, _len = _ref.length; _i < _len; _i++) {
235 this.current_category.subtree.push(c);
236 _results.push(this.build_subtree(this.categories[c]));
242 /* global variable */
250 var CashRegister = (function() {
251 __extends(CashRegister, Backbone.Model);
252 function CashRegister() {
253 CashRegister.__super__.constructor.apply(this, arguments);
258 var CashRegisterCollection = (function() {
259 __extends(CashRegisterCollection, Backbone.Collection);
260 function CashRegisterCollection() {
261 CashRegisterCollection.__super__.constructor.apply(this, arguments);
264 CashRegisterCollection.prototype.model = CashRegister;
265 return CashRegisterCollection;
267 var Product = (function() {
268 __extends(Product, Backbone.Model);
270 Product.__super__.constructor.apply(this, arguments);
275 var ProductCollection = (function() {
276 __extends(ProductCollection, Backbone.Collection);
277 function ProductCollection() {
278 ProductCollection.__super__.constructor.apply(this, arguments);
281 ProductCollection.prototype.model = Product;
282 return ProductCollection;
284 var Category = (function() {
285 __extends(Category, Backbone.Model);
286 function Category() {
287 Category.__super__.constructor.apply(this, arguments);
292 var CategoryCollection = (function() {
293 __extends(CategoryCollection, Backbone.Collection);
294 function CategoryCollection() {
295 CategoryCollection.__super__.constructor.apply(this, arguments);
298 CategoryCollection.prototype.model = Category;
299 return CategoryCollection;
302 Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
303 There should only ever be one Orderline per distinct product in an Order.
304 To add more of the same product, just update the quantity accordingly.
305 The Order also contains payment information.
307 var Orderline = Backbone.Model.extend({
313 initialize: function(attributes) {
314 Backbone.Model.prototype.initialize.apply(this, arguments);
315 this.bind('change:quantity', function(unused, qty) {
317 this.trigger('killme');
320 incrementQuantity: function() {
322 quantity: (this.get('quantity')) + 1
325 getPriceWithoutTax: function() {
326 return this.getAllPrices().priceWithoutTax;
328 getPriceWithTax: function() {
329 return this.getAllPrices().priceWithTax;
332 return this.getAllPrices().tax;
334 getAllPrices: function() {
336 var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
338 var totalNoTax = base;
340 var product_list = pos.get('product_list');
341 var product = _.detect(product_list, function(el) {return el.id === self.get('id');});
342 var taxes_ids = product.taxes_id;
343 var taxes = pos.get('taxes');
345 _.each(taxes_ids, function(el) {
346 var tax = _.detect(taxes, function(t) {return t.id === el;});
347 if (tax.price_include) {
349 if (tax.type === "percent") {
350 tmp = base - (base / (1 + tax.amount));
351 } else if (tax.type === "fixed") {
352 tmp = tax.amount * self.get('quantity');
354 throw "This type of tax is not supported by the point of sale: " + tax.type;
360 if (tax.type === "percent") {
361 tmp = tax.amount * base;
362 } else if (tax.type === "fixed") {
363 tmp = tax.amount * self.get('quantity');
365 throw "This type of tax is not supported by the point of sale: " + tax.type;
372 "priceWithTax": totalTax,
373 "priceWithoutTax": totalNoTax,
377 exportAsJSON: function() {
380 qty: this.get('quantity'),
381 price_unit: this.get('list_price'),
382 discount: this.get('discount'),
383 product_id: this.get('id')
388 var OrderlineCollection = Backbone.Collection.extend({
392 Every PaymentLine has all the attributes of the corresponding CashRegister.
394 var Paymentline = (function() {
395 __extends(Paymentline, Backbone.Model);
396 function Paymentline() {
397 Paymentline.__super__.constructor.apply(this, arguments);
400 Paymentline.prototype.defaults = {
403 Paymentline.prototype.getAmount = function() {
404 return this.get('amount');
406 Paymentline.prototype.exportAsJSON = function() {
409 name: db.web.datetime_to_str(new Date()),
410 statement_id: this.get('id'),
411 account_id: (this.get('account_id'))[0],
412 journal_id: (this.get('journal_id'))[0],
413 amount: this.getAmount()
419 var PaymentlineCollection = (function() {
420 __extends(PaymentlineCollection, Backbone.Collection);
421 function PaymentlineCollection() {
422 PaymentlineCollection.__super__.constructor.apply(this, arguments);
425 PaymentlineCollection.prototype.model = Paymentline;
426 return PaymentlineCollection;
428 var Order = (function() {
429 __extends(Order, Backbone.Model);
431 Order.__super__.constructor.apply(this, arguments);
434 Order.prototype.defaults = {
438 Order.prototype.initialize = function() {
439 this.set({creationDate: new Date});
441 orderLines: new OrderlineCollection
444 paymentLines: new PaymentlineCollection
446 this.bind('change:validated', this.validatedChanged);
448 name: "Order " + this.generateUniqueId()
451 Order.prototype.events = {
452 'change:validated': 'validatedChanged'
454 Order.prototype.validatedChanged = function() {
455 if (this.get("validated") && !this.previous("validated")) {
456 this.set({'step': 'receipt'});
459 Order.prototype.generateUniqueId = function() {
460 return new Date().getTime();
462 Order.prototype.addProduct = function(product) {
464 existing = (this.get('orderLines')).get(product.id);
465 if (existing != null) {
466 existing.incrementQuantity();
468 var line = new Orderline(product.toJSON());
469 this.get('orderLines').add(line);
470 line.bind('killme', function() {
471 this.get('orderLines').remove(line);
475 Order.prototype.addPaymentLine = function(cashRegister) {
477 newPaymentline = new Paymentline(cashRegister);
478 /* TODO: Should be 0 for cash-like accounts */
480 amount: this.getDueLeft()
482 return (this.get('paymentLines')).add(newPaymentline);
484 Order.prototype.getName = function() {
485 return this.get('name');
487 Order.prototype.getTotal = function() {
488 return (this.get('orderLines')).reduce((function(sum, orderLine) {
489 return sum + orderLine.getPriceWithTax();
492 Order.prototype.getTotalTaxExcluded = function() {
493 return (this.get('orderLines')).reduce((function(sum, orderLine) {
494 return sum + orderLine.getPriceWithoutTax();
497 Order.prototype.getTax = function() {
498 return (this.get('orderLines')).reduce((function(sum, orderLine) {
499 return sum + orderLine.getTax();
502 Order.prototype.getPaidTotal = function() {
503 return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
504 return sum + paymentLine.getAmount();
507 Order.prototype.getChange = function() {
508 return this.getPaidTotal() - this.getTotal();
510 Order.prototype.getDueLeft = function() {
511 return this.getTotal() - this.getPaidTotal();
513 Order.prototype.exportAsJSON = function() {
514 var orderLines, paymentLines, result;
516 (this.get('orderLines')).each(_.bind( function(item) {
517 return orderLines.push([0, 0, item.exportAsJSON()]);
520 (this.get('paymentLines')).each(_.bind( function(item) {
521 return paymentLines.push([0, 0, item.exportAsJSON()]);
524 name: this.getName(),
525 amount_paid: this.getPaidTotal(),
526 amount_total: this.getTotal(),
527 amount_tax: this.getTax(),
528 amount_return: this.getChange(),
530 statement_ids: paymentLines
536 var OrderCollection = (function() {
537 __extends(OrderCollection, Backbone.Collection);
538 function OrderCollection() {
539 OrderCollection.__super__.constructor.apply(this, arguments);
542 OrderCollection.prototype.model = Order;
543 return OrderCollection;
545 var Shop = (function() {
546 __extends(Shop, Backbone.Model);
548 Shop.__super__.constructor.apply(this, arguments);
551 Shop.prototype.initialize = function() {
553 orders: new OrderCollection(),
554 products: new ProductCollection()
557 cashRegisters: new CashRegisterCollection(pos.get('bank_statements')),
559 return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
560 if ((this.get('orders')).isEmpty()) {
561 this.addAndSelectOrder(new Order);
563 if ((this.get('selectedOrder')) === removedOrder) {
565 selectedOrder: (this.get('orders')).last()
570 Shop.prototype.addAndSelectOrder = function(newOrder) {
571 (this.get('orders')).add(newOrder);
573 selectedOrder: newOrder
579 The numpad handles both the choice of the property currently being modified
580 (quantity, price or discount) and the edition of the corresponding numeric value.
582 var NumpadState = Backbone.Model.extend({
587 appendNewChar: function(newChar) {
589 oldBuffer = this.get('buffer');
590 if (oldBuffer === '0') {
594 } else if (oldBuffer === '-0') {
596 buffer: "-" + newChar
600 buffer: (this.get('buffer')) + newChar
605 deleteLastChar: function() {
607 tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
608 if (isNaN(tempNewBuffer)) {
612 buffer: tempNewBuffer
616 switchSign: function() {
618 oldBuffer = this.get('buffer');
620 buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
624 changeMode: function(newMode) {
636 updateTarget: function() {
637 var bufferContent, params;
638 bufferContent = this.get('buffer');
639 if (bufferContent && !isNaN(bufferContent)) {
640 this.trigger('setValue', parseFloat(bufferContent));
649 var NumpadWidget = db.web.OldWidget.extend({
650 init: function(parent, options) {
652 this.state = new NumpadState();
655 this.state.bind('change:mode', this.changedMode, this);
657 this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
658 this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
659 this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
660 this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
662 clickDeleteLastChar: function() {
663 return this.state.deleteLastChar();
665 clickSwitchSign: function() {
666 return this.state.switchSign();
668 clickAppendNewChar: function(event) {
670 newChar = event.currentTarget.innerText || event.currentTarget.textContent;
671 return this.state.appendNewChar(newChar);
673 clickChangeMode: function(event) {
674 var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
675 return this.state.changeMode(newMode);
677 changedMode: function() {
678 var mode = this.state.get('mode');
679 $('.selected-mode').removeClass('selected-mode');
680 $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
684 Gives access to the payment methods (aka. 'cash registers')
686 var PaypadWidget = db.web.OldWidget.extend({
687 init: function(parent, options) {
689 this.shop = options.shop;
692 this.$element.find('button').click(_.bind(this.performPayment, this));
694 performPayment: function(event) {
695 if (this.shop.get('selectedOrder').get('step') === 'receipt')
697 var cashRegister, cashRegisterCollection, cashRegisterId;
698 /* set correct view */
699 this.shop.get('selectedOrder').set({'step': 'payment'});
701 cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
702 cashRegisterCollection = this.shop.get('cashRegisters');
703 cashRegister = cashRegisterCollection.find(_.bind( function(item) {
704 return (item.get('id')) === parseInt(cashRegisterId, 10);
706 return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
708 renderElement: function() {
709 this.$element.empty();
710 return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
711 var button = new PaymentButtonWidget();
712 button.model = cashRegister;
713 button.appendTo(this.$element);
717 var PaymentButtonWidget = db.web.OldWidget.extend({
718 template_fct: qweb_template('pos-payment-button-template'),
719 renderElement: function() {
720 this.$element.html(this.template_fct({
721 id: this.model.get('id'),
722 name: (this.model.get('journal_id'))[1]
728 There are 3 steps in a POS workflow:
729 1. prepare the order (i.e. chose products, quantities etc.)
730 2. choose payment method(s) and amount(s)
731 3. validae order and print receipt
732 It should be possible to go back to any step as long as step 3 hasn't been completed.
733 Modifying an order after validation shouldn't be allowed.
735 var StepSwitcher = db.web.OldWidget.extend({
736 init: function(parent, options) {
738 this.shop = options.shop;
740 this.shop.bind('change:selectedOrder', this.change_order, this);
742 change_order: function() {
743 if (this.selected_order) {
744 this.selected_order.unbind('change:step', this.change_step);
746 this.selected_order = this.shop.get('selectedOrder');
747 if (this.selected_order) {
748 this.selected_order.bind('change:step', this.change_step, this);
752 change_step: function() {
753 var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
754 $('.step-screen').hide();
755 $('#' + new_step + '-screen').show();
761 var OrderlineWidget = db.web.OldWidget.extend({
763 template_fct: qweb_template('pos-orderline-template'),
764 init: function(parent, options) {
766 this.model = options.model;
767 this.model.bind('change', _.bind( function() {
770 this.model.bind('remove', _.bind( function() {
771 this.$element.remove();
773 this.order = options.order;
776 this.$element.click(_.bind(this.clickHandler, this));
779 clickHandler: function() {
782 renderElement: function() {
783 this.$element.html(this.template_fct(this.model.toJSON()));
786 refresh: function() {
787 this.renderElement();
788 var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
789 heights.push($('#current-order thead').outerHeight());
790 var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
791 $('#current-order').scrollTop(position);
794 $('tr.selected').removeClass('selected');
795 this.$element.addClass('selected');
796 this.order.selected = this.model;
799 on_selected: function() {},
801 var OrderWidget = db.web.OldWidget.extend({
802 init: function(parent, options) {
804 this.shop = options.shop;
805 this.setNumpadState(options.numpadState);
806 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
807 this.bindOrderLineEvents();
809 setNumpadState: function(numpadState) {
810 if (this.numpadState) {
811 this.numpadState.unbind('setValue', this.setValue);
813 this.numpadState = numpadState;
814 if (this.numpadState) {
815 this.numpadState.bind('setValue', this.setValue, this);
816 this.numpadState.reset();
819 setValue: function(val) {
821 param[this.numpadState.get('mode')] = val;
822 var order = this.shop.get('selectedOrder');
823 if (order.get('orderLines').length !== 0) {
824 order.selected.set(param);
826 this.shop.get('selectedOrder').destroy();
829 changeSelectedOrder: function() {
830 this.currentOrderLines.unbind();
831 this.bindOrderLineEvents();
832 this.renderElement();
834 bindOrderLineEvents: function() {
835 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
836 this.currentOrderLines.bind('add', this.addLine, this);
837 this.currentOrderLines.bind('remove', this.renderElement, this);
839 addLine: function(newLine) {
840 var line = new OrderlineWidget(null, {
842 order: this.shop.get('selectedOrder')
844 line.on_selected.add(_.bind(this.selectedLine, this));
846 line.appendTo(this.$element);
847 this.updateSummary();
849 selectedLine: function() {
851 if (this.currentSelected !== this.shop.get('selectedOrder').selected) {
854 this.currentSelected = this.shop.get('selectedOrder').selected;
855 if (reset && this.numpadState)
856 this.numpadState.reset();
857 this.updateSummary();
859 renderElement: function() {
860 this.$element.empty();
861 this.currentOrderLines.each(_.bind( function(orderLine) {
862 var line = new OrderlineWidget(null, {
864 order: this.shop.get('selectedOrder')
866 line.on_selected.add(_.bind(this.selectedLine, this));
867 line.appendTo(this.$element);
869 this.updateSummary();
871 updateSummary: function() {
872 var currentOrder, tax, total, totalTaxExcluded;
873 currentOrder = this.shop.get('selectedOrder');
874 total = currentOrder.getTotal();
875 totalTaxExcluded = currentOrder.getTotalTaxExcluded();
876 tax = currentOrder.getTax();
877 $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
878 $('#tax').html(tax.toFixed(2)).hide().fadeIn();
879 $('#total').html(total.toFixed(2)).hide().fadeIn();
885 var CategoryWidget = db.web.OldWidget.extend({
887 this.$element.find(".oe_pos_categories_list a").click(_.bind(this.changeCategory, this));
889 template_fct: qweb_template('pos-category-template'),
890 renderElement: function() {
893 this.$element.html(this.template_fct({
894 breadcrumb: (function() {
895 var _i, _len, _results;
897 for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
898 c = self.ancestors[_i];
899 _results.push(pos.categories[c]);
903 categories: (function() {
904 var _i, _len, _results;
906 for (_i = 0, _len = self.children.length; _i < _len; _i++) {
907 c = self.children[_i];
908 _results.push(pos.categories[c]);
914 changeCategory: function(a) {
915 var id = $(a.target).data("category-id");
916 this.on_change_category(id);
918 on_change_category: function(id) {},
920 var ProductWidget = db.web.OldWidget.extend({
922 template_fct: qweb_template('pos-product-template'),
923 init: function(parent, options) {
925 this.model = options.model;
926 this.shop = options.shop;
928 start: function(options) {
929 $("a", this.$element).click(_.bind(this.addToOrder, this));
931 addToOrder: function(event) {
932 /* Preserve the category URL */
933 event.preventDefault();
934 return (this.shop.get('selectedOrder')).addProduct(this.model);
936 renderElement: function() {
937 this.$element.addClass("product");
938 this.$element.html(this.template_fct(this.model.toJSON()));
942 var ProductListWidget = db.web.OldWidget.extend({
943 init: function(parent, options) {
945 this.model = options.model;
946 this.shop = options.shop;
947 this.shop.get('products').bind('reset', this.renderElement, this);
949 renderElement: function() {
950 this.$element.empty();
951 (this.shop.get('products')).each(_.bind( function(product) {
952 var p = new ProductWidget(null, {
956 p.appendTo(this.$element);
964 var PaymentlineWidget = db.web.OldWidget.extend({
966 template_fct: qweb_template('pos-paymentline-template'),
967 init: function(parent, options) {
969 this.model = options.model;
970 this.model.bind('change', this.changedAmount, this);
972 on_delete: function() {},
973 changeAmount: function(event) {
975 newAmount = event.currentTarget.value;
976 if (newAmount && !isNaN(newAmount)) {
977 this.amount = parseFloat(newAmount);
983 changedAmount: function() {
984 if (this.amount !== this.model.get('amount'))
985 this.renderElement();
987 renderElement: function() {
988 this.amount = this.model.get('amount');
989 this.$element.html(this.template_fct({
990 name: (this.model.get('journal_id'))[1],
993 this.$element.addClass('paymentline');
994 $('input', this.$element).keyup(_.bind(this.changeAmount, this));
995 $('.delete-payment-line', this.$element).click(this.on_delete);
998 var PaymentWidget = db.web.OldWidget.extend({
999 init: function(parent, options) {
1000 this._super(parent);
1001 this.model = options.model;
1002 this.shop = options.shop;
1003 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
1004 this.bindPaymentLineEvents();
1005 this.bindOrderLineEvents();
1007 paymentLineList: function() {
1008 return this.$element.find('#paymentlines');
1011 $('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
1012 $('.oe_back_to_products', this.$element).click(_.bind(this.back, this));
1015 this.shop.get('selectedOrder').set({"step": "products"});
1017 validateCurrentOrder: function() {
1018 var callback, currentOrder;
1019 currentOrder = this.shop.get('selectedOrder');
1020 $('button#validate-order', this.$element).attr('disabled', 'disabled');
1021 pos.pushOrder(currentOrder.exportAsJSON()).then(_.bind(function() {
1022 $('button#validate-order', this.$element).removeAttr('disabled');
1023 return currentOrder.set({
1028 bindPaymentLineEvents: function() {
1029 this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
1030 this.currentPaymentLines.bind('add', this.addPaymentLine, this);
1031 this.currentPaymentLines.bind('remove', this.renderElement, this);
1032 this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
1034 bindOrderLineEvents: function() {
1035 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
1036 this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
1038 changeSelectedOrder: function() {
1039 this.currentPaymentLines.unbind();
1040 this.bindPaymentLineEvents();
1041 this.currentOrderLines.unbind();
1042 this.bindOrderLineEvents();
1043 this.renderElement();
1045 addPaymentLine: function(newPaymentLine) {
1046 var x = new PaymentlineWidget(null, {
1047 model: newPaymentLine
1049 x.on_delete.add(_.bind(this.deleteLine, this, x));
1050 x.appendTo(this.paymentLineList());
1052 renderElement: function() {
1053 this.paymentLineList().empty();
1054 this.currentPaymentLines.each(_.bind( function(paymentLine) {
1055 this.addPaymentLine(paymentLine);
1057 this.updatePaymentSummary();
1059 deleteLine: function(lineWidget) {
1060 this.currentPaymentLines.remove([lineWidget.model]);
1062 updatePaymentSummary: function() {
1063 var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
1064 currentOrder = this.shop.get('selectedOrder');
1065 paidTotal = currentOrder.getPaidTotal();
1066 dueTotal = currentOrder.getTotal();
1067 this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
1068 this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
1069 remainingAmount = dueTotal - paidTotal;
1070 remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
1071 $('#payment-remaining').html(remaining);
1073 setNumpadState: function(numpadState) {
1074 if (this.numpadState) {
1075 this.numpadState.unbind('setValue', this.setValue);
1076 this.numpadState.unbind('change:mode', this.setNumpadMode);
1078 this.numpadState = numpadState;
1079 if (this.numpadState) {
1080 this.numpadState.bind('setValue', this.setValue, this);
1081 this.numpadState.bind('change:mode', this.setNumpadMode, this);
1082 this.numpadState.reset();
1083 this.setNumpadMode();
1086 setNumpadMode: function() {
1087 this.numpadState.set({mode: 'payment'});
1089 setValue: function(val) {
1090 this.currentPaymentLines.last().set({amount: val});
1093 var ReceiptWidget = db.web.OldWidget.extend({
1094 init: function(parent, options) {
1095 this._super(parent);
1096 this.model = options.model;
1097 this.shop = options.shop;
1098 this.user = pos.get('user');
1099 this.company = pos.get('company');
1100 this.shop_obj = pos.get('shop');
1103 this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
1104 this.changeSelectedOrder();
1106 renderElement: function() {
1107 this.$element.html(qweb_template('pos-receipt-view'));
1108 $('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
1109 $('button#print-the-ticket', this.$element).click(_.bind(this.print, this));
1114 finishOrder: function() {
1115 this.shop.get('selectedOrder').destroy();
1117 changeSelectedOrder: function() {
1118 if (this.currentOrderLines)
1119 this.currentOrderLines.unbind();
1120 this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
1121 this.currentOrderLines.bind('add', this.refresh, this);
1122 this.currentOrderLines.bind('change', this.refresh, this);
1123 this.currentOrderLines.bind('remove', this.refresh, this);
1124 if (this.currentPaymentLines)
1125 this.currentPaymentLines.unbind();
1126 this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
1127 this.currentPaymentLines.bind('all', this.refresh, this);
1130 refresh: function() {
1131 this.currentOrder = this.shop.get('selectedOrder');
1132 $('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
1135 var OrderButtonWidget = db.web.OldWidget.extend({
1137 template_fct: qweb_template('pos-order-selector-button-template'),
1138 init: function(parent, options) {
1139 this._super(parent);
1140 this.order = options.order;
1141 this.shop = options.shop;
1142 this.order.bind('destroy', _.bind( function() {
1145 this.shop.bind('change:selectedOrder', _.bind( function(shop) {
1147 selectedOrder = shop.get('selectedOrder');
1148 if (this.order === selectedOrder) {
1149 this.setButtonSelected();
1154 $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
1155 $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
1157 selectOrder: function(event) {
1159 selectedOrder: this.order
1162 setButtonSelected: function() {
1163 $('.selected-order').removeClass('selected-order');
1164 this.$element.addClass('selected-order');
1166 closeOrder: function(event) {
1167 this.order.destroy();
1169 renderElement: function() {
1170 this.$element.html(this.template_fct({widget:this}));
1171 this.$element.addClass('order-selector-button');
1174 var ShopWidget = db.web.OldWidget.extend({
1175 init: function(parent, options) {
1176 this._super(parent);
1177 this.shop = options.shop;
1180 $('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
1182 (this.shop.get('orders')).bind('add', this.orderAdded, this);
1183 (this.shop.get('orders')).add(new Order);
1184 this.productListView = new ProductListWidget(null, {
1187 this.productListView.$element = $("#products-screen-ol");
1188 this.productListView.renderElement();
1189 this.productListView.start();
1190 this.paypadView = new PaypadWidget(null, {
1193 this.paypadView.$element = $('#paypad');
1194 this.paypadView.renderElement();
1195 this.paypadView.start();
1196 this.numpadView = new NumpadWidget(null);
1197 this.numpadView.$element = $('#numpad');
1198 this.numpadView.start();
1199 this.orderView = new OrderWidget(null, {
1202 this.orderView.$element = $('#current-order-content');
1203 this.orderView.start();
1204 this.paymentView = new PaymentWidget(null, {
1207 this.paymentView.$element = $('#payment-screen');
1208 this.paymentView.renderElement();
1209 this.paymentView.start();
1210 this.receiptView = new ReceiptWidget(null, {
1213 this.receiptView.replace($('#receipt-screen'));
1214 this.stepSwitcher = new StepSwitcher(this, {shop: this.shop});
1215 this.shop.bind('change:selectedOrder', this.changedSelectedOrder, this);
1216 this.changedSelectedOrder();
1218 createNewOrder: function() {
1220 newOrder = new Order;
1221 (this.shop.get('orders')).add(newOrder);
1223 selectedOrder: newOrder
1226 orderAdded: function(newOrder) {
1228 newOrderButton = new OrderButtonWidget(null, {
1232 newOrderButton.appendTo($('#orders'));
1233 newOrderButton.selectOrder();
1235 changedSelectedOrder: function() {
1236 if (this.currentOrder) {
1237 this.currentOrder.unbind('change:step', this.changedStep);
1239 this.currentOrder = this.shop.get('selectedOrder');
1240 this.currentOrder.bind('change:step', this.changedStep, this);
1243 changedStep: function() {
1244 var step = this.currentOrder.get('step');
1245 this.orderView.setNumpadState(null);
1246 this.paymentView.setNumpadState(null);
1247 if (step === 'products') {
1248 this.orderView.setNumpadState(this.numpadView.state);
1249 } else if (step === 'payment') {
1250 this.paymentView.setNumpadState(this.numpadView.state);
1254 var App = (function() {
1255 function App($element) {
1256 this.initialize($element);
1259 App.prototype.initialize = function($element) {
1260 this.shop = new Shop;
1261 this.shopView = new ShopWidget(null, {
1264 this.shopView.$element = $element;
1265 this.shopView.start();
1266 this.categoryView = new CategoryWidget(null, 'products-screen-categories');
1267 this.categoryView.on_change_category.add_last(_.bind(this.category, this));
1270 App.prototype.category = function(id) {
1271 var c, product_list;
1275 c = pos.categories[id];
1276 this.categoryView.ancestors = c.ancestors;
1277 this.categoryView.children = c.children;
1278 this.categoryView.renderElement();
1279 this.categoryView.start();
1280 product_list = pos.get('product_list').filter( function(p) {
1282 return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
1284 (this.shop.get('products')).reset(product_list);
1286 $('.searchbox input').keyup(function() {
1288 s = $(this).val().toLowerCase();
1290 m = product_list.filter( function(p) {
1291 return p.name.toLowerCase().indexOf(s) != -1;
1293 $('.search-clear').fadeIn();
1296 $('.search-clear').fadeOut();
1298 return (self.shop.get('products')).reset(m);
1300 return $('.search-clear').click( function() {
1301 (self.shop.get('products')).reset(product_list);
1302 $('.searchbox input').val('').focus();
1303 return $('.search-clear').fadeOut();
1309 db.point_of_sale.SynchNotification = db.web.OldWidget.extend({
1310 template: "pos-synch-notification",
1312 this._super.apply(this, arguments);
1313 this.nbr_pending = 0;
1315 renderElement: function() {
1316 this._super.apply(this, arguments);
1317 $('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
1319 on_change_nbr_pending: function(nbr_pending) {
1320 this.nbr_pending = nbr_pending;
1321 this.renderElement();
1323 on_synch: function() {}
1326 db.web.client_actions.add('pos.ui', 'db.point_of_sale.PointOfSale');
1327 db.point_of_sale.PointOfSale = db.web.OldWidget.extend({
1329 this._super.apply(this, arguments);
1332 throw "It is not possible to instantiate multiple instances "+
1333 "of the point of sale at the same time.";
1334 pos = new Pos(this.session);
1338 return pos.ready.then(_.bind(function() {
1339 this.renderElement();
1340 this.synch_notification = new db.point_of_sale.SynchNotification(this);
1341 this.synch_notification.replace($('.oe_pos_synch-notification', this.$element));
1342 this.synch_notification.on_synch.add(_.bind(pos.flush, pos));
1344 pos.bind('change:nbr_pending_operations', this.changed_pending_operations, this);
1345 this.changed_pending_operations();
1347 this.$element.find("#loggedas button").click(function() {
1351 pos.app = new App(self.$element);
1352 db.webclient.set_content_full_screen(true);
1354 if (pos.get('bank_statements').length === 0)
1355 return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_open_statement']], ['res_id']).pipe(
1356 _.bind(function(res) {
1357 return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1358 var action = result.result;
1359 this.do_action(action);
1364 render: function() {
1365 return qweb_template("PointOfSale")();
1367 changed_pending_operations: function () {
1368 this.synch_notification.on_change_nbr_pending(pos.get('nbr_pending_operations'));
1370 try_close: function() {
1371 pos.flush().then(_.bind(function() {
1372 var close = _.bind(this.close, this);
1373 if (pos.get('nbr_pending_operations') > 0) {
1374 var confirm = false;
1375 $(QWeb.render('pos-close-warning')).dialog({
1383 $( this ).dialog( "close" );
1386 $( this ).dialog( "close" );
1400 return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
1401 _.bind(function(res) {
1402 return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1403 var action = result.result;
1404 action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'default_home'}});
1405 this.do_action(action);
1409 destroy: function() {
1410 db.webclient.set_content_full_screen(false);