PageRenderTime 60ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/point_of_sale/static/src/js/pos.js

https://bitbucket.org/nagyv/openerp-addons
JavaScript | 1465 lines | 1338 code | 54 blank | 73 comment | 97 complexity | 9092a31d34052c671a0402d1957d3437 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. openerp.point_of_sale = function(db) {
  2. db.point_of_sale = {};
  3. var QWeb = db.web.qweb;
  4. var qweb_template = function(template) {
  5. return function(ctx) {
  6. return QWeb.render(template, _.extend({}, ctx,{
  7. 'currency': pos.get('currency'),
  8. 'format_amount': function(amount) {
  9. if (pos.get('currency').position == 'after') {
  10. return amount + ' ' + pos.get('currency').symbol;
  11. } else {
  12. return pos.get('currency').symbol + ' ' + amount;
  13. }
  14. },
  15. }));
  16. };
  17. };
  18. var _t = db.web._t;
  19. /*
  20. Local store access. Read once from localStorage upon construction and persist on every change.
  21. There should only be one store active at any given time to ensure data consistency.
  22. */
  23. var Store = db.web.Class.extend({
  24. init: function() {
  25. this.data = {};
  26. },
  27. get: function(key, _default) {
  28. if (this.data[key] === undefined) {
  29. var stored = localStorage['oe_pos_' + key];
  30. if (stored)
  31. this.data[key] = JSON.parse(stored);
  32. else
  33. return _default;
  34. }
  35. return this.data[key];
  36. },
  37. set: function(key, value) {
  38. this.data[key] = value;
  39. localStorage['oe_pos_' + key] = JSON.stringify(value);
  40. },
  41. });
  42. /*
  43. Gets all the necessary data from the OpenERP web client (session, shop data etc.)
  44. */
  45. var Pos = Backbone.Model.extend({
  46. initialize: function(session, attributes) {
  47. Backbone.Model.prototype.initialize.call(this, attributes);
  48. this.store = new Store();
  49. this.ready = $.Deferred();
  50. this.flush_mutex = new $.Mutex();
  51. this.build_tree = _.bind(this.build_tree, this);
  52. this.session = session;
  53. var attributes = {
  54. 'pending_operations': [],
  55. 'currency': {symbol: '$', position: 'after'},
  56. 'shop': {},
  57. 'company': {},
  58. 'user': {},
  59. };
  60. _.each(attributes, _.bind(function(def, attr) {
  61. var to_set = {};
  62. to_set[attr] = this.store.get(attr, def);
  63. this.set(to_set);
  64. this.bind('change:' + attr, _.bind(function(unused, val) {
  65. this.store.set(attr, val);
  66. }, this));
  67. }, this));
  68. $.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']),
  69. this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'product_image_small', 'ean13', 'id'], [['pos_categ_id', '!=', false]]),
  70. this.fetch('product.packaging', ['product_id', 'ean']),
  71. this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
  72. [['state', '=', 'open'], ['user_id', '=', this.session.uid]]),
  73. this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type']),
  74. this.fetch('account.tax', ['amount', 'price_include', 'type']),
  75. this.get_app_data())
  76. .pipe(_.bind(this.build_tree, this));
  77. },
  78. fetch: function(osvModel, fields, domain) {
  79. var dataSetSearch;
  80. var self = this;
  81. dataSetSearch = new db.web.DataSetSearch(this, osvModel, {}, domain);
  82. return dataSetSearch.read_slice(fields, 0).then(function(result) {
  83. return self.store.set(osvModel, result);
  84. });
  85. },
  86. get_app_data: function() {
  87. var self = this;
  88. return $.when(new db.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
  89. self.set({'shop': result[0]});
  90. var company_id = result[0]['company_id'][0];
  91. return new db.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
  92. self.set({'company': result});
  93. var currency_id = result['currency_id'][0]
  94. return new db.web.Model("res.currency").get_func("read")([currency_id],
  95. ['symbol', 'position']).pipe(function(result) {
  96. self.set({'currency': result[0]});
  97. });
  98. });
  99. }), new db.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
  100. self.set({'user': result});
  101. }));
  102. },
  103. pushOrder: function(record) {
  104. var ops = _.clone(this.get('pending_operations'));
  105. ops.push(record);
  106. this.set({pending_operations: ops});
  107. return this.flush();
  108. },
  109. flush: function() {
  110. return this.flush_mutex.exec(_.bind(function() {
  111. return this._int_flush();
  112. }, this));
  113. },
  114. _int_flush : function() {
  115. var ops = this.get('pending_operations');
  116. if (ops.length === 0)
  117. return $.when();
  118. var op = ops[0];
  119. /* we prevent the default error handler and assume errors
  120. * are a normal use case, except we stop the current iteration
  121. */
  122. return new db.web.Model("pos.order").get_func("create_from_ui")([op]).fail(function(unused, event) {
  123. event.preventDefault();
  124. }).pipe(_.bind(function() {
  125. console.debug('saved 1 record');
  126. var ops2 = this.get('pending_operations');
  127. this.set({'pending_operations': _.without(ops2, op)});
  128. return this._int_flush();
  129. }, this), function() {return $.when()});
  130. },
  131. categories: {},
  132. build_tree: function() {
  133. var c, id, _i, _len, _ref, _ref2;
  134. _ref = this.store.get('pos.category');
  135. for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  136. c = _ref[_i];
  137. this.categories[c.id] = {
  138. id: c.id,
  139. name: c.name,
  140. children: c.child_id,
  141. parent: c.parent_id[0],
  142. ancestors: [c.id],
  143. subtree: [c.id]
  144. };
  145. }
  146. _ref2 = this.categories;
  147. for (id in _ref2) {
  148. c = _ref2[id];
  149. this.current_category = c;
  150. this.build_ancestors(c.parent);
  151. this.build_subtree(c);
  152. }
  153. this.categories[0] = {
  154. ancestors: [],
  155. children: (function() {
  156. var _j, _len2, _ref3, _results;
  157. _ref3 = this.store.get('pos.category');
  158. _results = [];
  159. for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
  160. c = _ref3[_j];
  161. if (!(c.parent_id[0] != null)) {
  162. _results.push(c.id);
  163. }
  164. }
  165. return _results;
  166. }).call(this),
  167. subtree: (function() {
  168. var _j, _len2, _ref3, _results;
  169. _ref3 = this.store.get('pos.category');
  170. _results = [];
  171. for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
  172. c = _ref3[_j];
  173. _results.push(c.id);
  174. }
  175. return _results;
  176. }).call(this)
  177. };
  178. return this.ready.resolve();
  179. },
  180. build_ancestors: function(parent) {
  181. if (parent != null) {
  182. this.current_category.ancestors.unshift(parent);
  183. return this.build_ancestors(this.categories[parent].parent);
  184. }
  185. },
  186. build_subtree: function(category) {
  187. var c, _i, _len, _ref, _results;
  188. _ref = category.children;
  189. _results = [];
  190. for (_i = 0, _len = _ref.length; _i < _len; _i++) {
  191. c = _ref[_i];
  192. this.current_category.subtree.push(c);
  193. _results.push(this.build_subtree(this.categories[c]));
  194. }
  195. return _results;
  196. }
  197. });
  198. /* global variable */
  199. var pos;
  200. /*
  201. ---
  202. Models
  203. ---
  204. */
  205. var CashRegister = Backbone.Model.extend({
  206. });
  207. var CashRegisterCollection = Backbone.Collection.extend({
  208. model: CashRegister,
  209. });
  210. var Product = Backbone.Model.extend({
  211. });
  212. var ProductCollection = Backbone.Collection.extend({
  213. model: Product,
  214. });
  215. var Category = Backbone.Model.extend({
  216. });
  217. var CategoryCollection = Backbone.Collection.extend({
  218. model: Category,
  219. });
  220. /*
  221. Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
  222. There should only ever be one Orderline per distinct product in an Order.
  223. To add more of the same product, just update the quantity accordingly.
  224. The Order also contains payment information.
  225. */
  226. var Orderline = Backbone.Model.extend({
  227. defaults: {
  228. quantity: 1,
  229. list_price: 0,
  230. discount: 0
  231. },
  232. initialize: function(attributes) {
  233. Backbone.Model.prototype.initialize.apply(this, arguments);
  234. this.bind('change:quantity', function(unused, qty) {
  235. if (qty == 0)
  236. this.trigger('killme');
  237. }, this);
  238. },
  239. incrementQuantity: function() {
  240. return this.set({
  241. quantity: (this.get('quantity')) + 1
  242. });
  243. },
  244. getPriceWithoutTax: function() {
  245. return this.getAllPrices().priceWithoutTax;
  246. },
  247. getPriceWithTax: function() {
  248. return this.getAllPrices().priceWithTax;
  249. },
  250. getTax: function() {
  251. return this.getAllPrices().tax;
  252. },
  253. getAllPrices: function() {
  254. var self = this;
  255. var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
  256. var totalTax = base;
  257. var totalNoTax = base;
  258. var products = pos.store.get('product.product');
  259. var product = _.detect(products, function(el) {return el.id === self.get('id');});
  260. var taxes_ids = product.taxes_id;
  261. var taxes = pos.store.get('account.tax');
  262. var taxtotal = 0;
  263. _.each(taxes_ids, function(el) {
  264. var tax = _.detect(taxes, function(t) {return t.id === el;});
  265. if (tax.price_include) {
  266. var tmp;
  267. if (tax.type === "percent") {
  268. tmp = base - (base / (1 + tax.amount));
  269. } else if (tax.type === "fixed") {
  270. tmp = tax.amount * self.get('quantity');
  271. } else {
  272. throw "This type of tax is not supported by the point of sale: " + tax.type;
  273. }
  274. taxtotal += tmp;
  275. totalNoTax -= tmp;
  276. } else {
  277. var tmp;
  278. if (tax.type === "percent") {
  279. tmp = tax.amount * base;
  280. } else if (tax.type === "fixed") {
  281. tmp = tax.amount * self.get('quantity');
  282. } else {
  283. throw "This type of tax is not supported by the point of sale: " + tax.type;
  284. }
  285. taxtotal += tmp;
  286. totalTax += tmp;
  287. }
  288. });
  289. return {
  290. "priceWithTax": totalTax,
  291. "priceWithoutTax": totalNoTax,
  292. "tax": taxtotal,
  293. };
  294. },
  295. exportAsJSON: function() {
  296. return {
  297. qty: this.get('quantity'),
  298. price_unit: this.get('list_price'),
  299. discount: this.get('discount'),
  300. product_id: this.get('id')
  301. };
  302. },
  303. });
  304. var OrderlineCollection = Backbone.Collection.extend({
  305. model: Orderline,
  306. });
  307. // Every PaymentLine has all the attributes of the corresponding CashRegister.
  308. var Paymentline = Backbone.Model.extend({
  309. defaults: {
  310. amount: 0,
  311. },
  312. initialize: function(attributes) {
  313. Backbone.Model.prototype.initialize.apply(this, arguments);
  314. },
  315. getAmount: function(){
  316. return this.get('amount');
  317. },
  318. exportAsJSON: function(){
  319. return {
  320. name: db.web.datetime_to_str(new Date()),
  321. statement_id: this.get('id'),
  322. account_id: (this.get('account_id'))[0],
  323. journal_id: (this.get('journal_id'))[0],
  324. amount: this.getAmount()
  325. };
  326. },
  327. });
  328. var PaymentlineCollection = Backbone.Collection.extend({
  329. model: Paymentline,
  330. });
  331. var Order = Backbone.Model.extend({
  332. defaults:{
  333. validated: false,
  334. step: 'products',
  335. },
  336. initialize: function(attributes){
  337. Backbone.Model.prototype.initialize.apply(this, arguments);
  338. this.set({
  339. creationDate: new Date,
  340. orderLines: new OrderlineCollection,
  341. paymentLines: new PaymentlineCollection,
  342. name: "Order " + this.generateUniqueId(),
  343. });
  344. this.bind('change:validated', this.validatedChanged);
  345. return this;
  346. },
  347. events: {
  348. 'change:validated': 'validatedChanged'
  349. },
  350. validatedChanged: function() {
  351. if (this.get("validated") && !this.previous("validated")) {
  352. this.set({'step': 'receipt'});
  353. }
  354. },
  355. generateUniqueId: function() {
  356. return new Date().getTime();
  357. },
  358. addProduct: function(product) {
  359. var existing;
  360. existing = (this.get('orderLines')).get(product.id);
  361. if (existing != null) {
  362. existing.incrementQuantity();
  363. } else {
  364. var line = new Orderline(product.toJSON());
  365. this.get('orderLines').add(line);
  366. line.bind('killme', function() {
  367. this.get('orderLines').remove(line);
  368. }, this);
  369. }
  370. },
  371. addPaymentLine: function(cashRegister) {
  372. var newPaymentline;
  373. newPaymentline = new Paymentline(cashRegister);
  374. /* TODO: Should be 0 for cash-like accounts */
  375. newPaymentline.set({
  376. amount: this.getDueLeft()
  377. });
  378. return (this.get('paymentLines')).add(newPaymentline);
  379. },
  380. getName: function() {
  381. return this.get('name');
  382. },
  383. getTotal: function() {
  384. return (this.get('orderLines')).reduce((function(sum, orderLine) {
  385. return sum + orderLine.getPriceWithTax();
  386. }), 0);
  387. },
  388. getTotalTaxExcluded: function() {
  389. return (this.get('orderLines')).reduce((function(sum, orderLine) {
  390. return sum + orderLine.getPriceWithoutTax();
  391. }), 0);
  392. },
  393. getTax: function() {
  394. return (this.get('orderLines')).reduce((function(sum, orderLine) {
  395. return sum + orderLine.getTax();
  396. }), 0);
  397. },
  398. getPaidTotal: function() {
  399. return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
  400. return sum + paymentLine.getAmount();
  401. }), 0);
  402. },
  403. getChange: function() {
  404. return this.getPaidTotal() - this.getTotal();
  405. },
  406. getDueLeft: function() {
  407. return this.getTotal() - this.getPaidTotal();
  408. },
  409. exportAsJSON: function() {
  410. var orderLines, paymentLines;
  411. orderLines = [];
  412. (this.get('orderLines')).each(_.bind( function(item) {
  413. return orderLines.push([0, 0, item.exportAsJSON()]);
  414. }, this));
  415. paymentLines = [];
  416. (this.get('paymentLines')).each(_.bind( function(item) {
  417. return paymentLines.push([0, 0, item.exportAsJSON()]);
  418. }, this));
  419. return {
  420. name: this.getName(),
  421. amount_paid: this.getPaidTotal(),
  422. amount_total: this.getTotal(),
  423. amount_tax: this.getTax(),
  424. amount_return: this.getChange(),
  425. lines: orderLines,
  426. statement_ids: paymentLines
  427. };
  428. },
  429. });
  430. var OrderCollection = Backbone.Collection.extend({
  431. model: Order,
  432. });
  433. var Shop = Backbone.Model.extend({
  434. initialize: function() {
  435. this.set({
  436. orders: new OrderCollection(),
  437. products: new ProductCollection()
  438. });
  439. this.set({
  440. cashRegisters: new CashRegisterCollection(pos.store.get('account.bank.statement')),
  441. });
  442. return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
  443. if ((this.get('orders')).isEmpty()) {
  444. this.addAndSelectOrder(new Order);
  445. }
  446. if ((this.get('selectedOrder')) === removedOrder) {
  447. return this.set({
  448. selectedOrder: (this.get('orders')).last()
  449. });
  450. }
  451. }, this));
  452. },
  453. addAndSelectOrder: function(newOrder) {
  454. (this.get('orders')).add(newOrder);
  455. return this.set({
  456. selectedOrder: newOrder
  457. });
  458. },
  459. });
  460. /*
  461. The numpad handles both the choice of the property currently being modified
  462. (quantity, price or discount) and the edition of the corresponding numeric value.
  463. */
  464. var NumpadState = Backbone.Model.extend({
  465. defaults: {
  466. buffer: "0",
  467. mode: "quantity"
  468. },
  469. appendNewChar: function(newChar) {
  470. var oldBuffer;
  471. oldBuffer = this.get('buffer');
  472. if (oldBuffer === '0') {
  473. this.set({
  474. buffer: newChar
  475. });
  476. } else if (oldBuffer === '-0') {
  477. this.set({
  478. buffer: "-" + newChar
  479. });
  480. } else {
  481. this.set({
  482. buffer: (this.get('buffer')) + newChar
  483. });
  484. }
  485. this.updateTarget();
  486. },
  487. deleteLastChar: function() {
  488. var tempNewBuffer;
  489. tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
  490. if (isNaN(tempNewBuffer)) {
  491. tempNewBuffer = "0";
  492. }
  493. this.set({
  494. buffer: tempNewBuffer
  495. });
  496. this.updateTarget();
  497. },
  498. switchSign: function() {
  499. var oldBuffer;
  500. oldBuffer = this.get('buffer');
  501. this.set({
  502. buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
  503. });
  504. this.updateTarget();
  505. },
  506. changeMode: function(newMode) {
  507. this.set({
  508. buffer: "0",
  509. mode: newMode
  510. });
  511. },
  512. reset: function() {
  513. this.set({
  514. buffer: "0",
  515. mode: "quantity"
  516. });
  517. },
  518. updateTarget: function() {
  519. var bufferContent, params;
  520. bufferContent = this.get('buffer');
  521. if (bufferContent && !isNaN(bufferContent)) {
  522. this.trigger('setValue', parseFloat(bufferContent));
  523. }
  524. },
  525. });
  526. /*
  527. ---
  528. Views
  529. ---
  530. */
  531. var NumpadWidget = db.web.OldWidget.extend({
  532. init: function(parent, options) {
  533. this._super(parent);
  534. this.state = new NumpadState();
  535. },
  536. start: function() {
  537. this.state.bind('change:mode', this.changedMode, this);
  538. this.changedMode();
  539. this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
  540. this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
  541. this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
  542. this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
  543. },
  544. clickDeleteLastChar: function() {
  545. return this.state.deleteLastChar();
  546. },
  547. clickSwitchSign: function() {
  548. return this.state.switchSign();
  549. },
  550. clickAppendNewChar: function(event) {
  551. var newChar;
  552. newChar = event.currentTarget.innerText || event.currentTarget.textContent;
  553. return this.state.appendNewChar(newChar);
  554. },
  555. clickChangeMode: function(event) {
  556. var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
  557. return this.state.changeMode(newMode);
  558. },
  559. changedMode: function() {
  560. var mode = this.state.get('mode');
  561. $('.selected-mode').removeClass('selected-mode');
  562. $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
  563. },
  564. });
  565. /*
  566. Gives access to the payment methods (aka. 'cash registers')
  567. */
  568. var PaypadWidget = db.web.OldWidget.extend({
  569. init: function(parent, options) {
  570. this._super(parent);
  571. this.shop = options.shop;
  572. },
  573. start: function() {
  574. this.$element.find('button').click(_.bind(this.performPayment, this));
  575. },
  576. performPayment: function(event) {
  577. if (this.shop.get('selectedOrder').get('step') === 'receipt')
  578. return;
  579. var cashRegister, cashRegisterCollection, cashRegisterId;
  580. /* set correct view */
  581. this.shop.get('selectedOrder').set({'step': 'payment'});
  582. cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
  583. cashRegisterCollection = this.shop.get('cashRegisters');
  584. cashRegister = cashRegisterCollection.find(_.bind( function(item) {
  585. return (item.get('id')) === parseInt(cashRegisterId, 10);
  586. }, this));
  587. return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
  588. },
  589. render_element: function() {
  590. this.$element.empty();
  591. return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
  592. var button = new PaymentButtonWidget();
  593. button.model = cashRegister;
  594. button.appendTo(this.$element);
  595. }, this));
  596. }
  597. });
  598. var PaymentButtonWidget = db.web.OldWidget.extend({
  599. template_fct: qweb_template('pos-payment-button-template'),
  600. render_element: function() {
  601. this.$element.html(this.template_fct({
  602. id: this.model.get('id'),
  603. name: (this.model.get('journal_id'))[1]
  604. }));
  605. return this;
  606. }
  607. });
  608. /*
  609. There are 3 steps in a POS workflow:
  610. 1. prepare the order (i.e. chose products, quantities etc.)
  611. 2. choose payment method(s) and amount(s)
  612. 3. validae order and print receipt
  613. It should be possible to go back to any step as long as step 3 hasn't been completed.
  614. Modifying an order after validation shouldn't be allowed.
  615. */
  616. var StepSwitcher = db.web.OldWidget.extend({
  617. init: function(parent, options) {
  618. this._super(parent);
  619. this.shop = options.shop;
  620. this.change_order();
  621. this.shop.bind('change:selectedOrder', this.change_order, this);
  622. },
  623. change_order: function() {
  624. if (this.selected_order) {
  625. this.selected_order.unbind('change:step', this.change_step);
  626. }
  627. this.selected_order = this.shop.get('selectedOrder');
  628. if (this.selected_order) {
  629. this.selected_order.bind('change:step', this.change_step, this);
  630. }
  631. this.change_step();
  632. },
  633. change_step: function() {
  634. var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
  635. $('.step-screen').hide();
  636. $('#' + new_step + '-screen').show();
  637. },
  638. });
  639. /*
  640. Shopping carts.
  641. */
  642. var OrderlineWidget = db.web.OldWidget.extend({
  643. tag_name: 'tr',
  644. template_fct: qweb_template('pos-orderline-template'),
  645. init: function(parent, options) {
  646. this._super(parent);
  647. this.model = options.model;
  648. this.model.bind('change', _.bind( function() {
  649. this.refresh();
  650. }, this));
  651. this.model.bind('remove', _.bind( function() {
  652. this.$element.remove();
  653. }, this));
  654. this.order = options.order;
  655. },
  656. start: function() {
  657. this.$element.click(_.bind(this.clickHandler, this));
  658. this.refresh();
  659. },
  660. clickHandler: function() {
  661. this.select();
  662. },
  663. render_element: function() {
  664. this.$element.html(this.template_fct(this.model.toJSON()));
  665. this.select();
  666. },
  667. refresh: function() {
  668. this.render_element();
  669. var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
  670. heights.push($('#current-order thead').outerHeight());
  671. var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
  672. $('#current-order').scrollTop(position);
  673. },
  674. select: function() {
  675. $('tr.selected').removeClass('selected');
  676. this.$element.addClass('selected');
  677. this.order.selected = this.model;
  678. this.on_selected();
  679. },
  680. on_selected: function() {},
  681. });
  682. var OrderWidget = db.web.OldWidget.extend({
  683. init: function(parent, options) {
  684. this._super(parent);
  685. this.shop = options.shop;
  686. this.setNumpadState(options.numpadState);
  687. this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
  688. this.bindOrderLineEvents();
  689. },
  690. setNumpadState: function(numpadState) {
  691. if (this.numpadState) {
  692. this.numpadState.unbind('setValue', this.setValue);
  693. }
  694. this.numpadState = numpadState;
  695. if (this.numpadState) {
  696. this.numpadState.bind('setValue', this.setValue, this);
  697. this.numpadState.reset();
  698. }
  699. },
  700. setValue: function(val) {
  701. var param = {};
  702. param[this.numpadState.get('mode')] = val;
  703. var order = this.shop.get('selectedOrder');
  704. if (order.get('orderLines').length !== 0) {
  705. order.selected.set(param);
  706. } else {
  707. this.shop.get('selectedOrder').destroy();
  708. }
  709. },
  710. changeSelectedOrder: function() {
  711. this.currentOrderLines.unbind();
  712. this.bindOrderLineEvents();
  713. this.render_element();
  714. },
  715. bindOrderLineEvents: function() {
  716. this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
  717. this.currentOrderLines.bind('add', this.addLine, this);
  718. this.currentOrderLines.bind('remove', this.render_element, this);
  719. },
  720. addLine: function(newLine) {
  721. var line = new OrderlineWidget(null, {
  722. model: newLine,
  723. order: this.shop.get('selectedOrder')
  724. });
  725. line.on_selected.add(_.bind(this.selectedLine, this));
  726. this.selectedLine();
  727. line.appendTo(this.$element);
  728. this.updateSummary();
  729. },
  730. selectedLine: function() {
  731. var reset = false;
  732. if (this.currentSelected !== this.shop.get('selectedOrder').selected) {
  733. reset = true;
  734. }
  735. this.currentSelected = this.shop.get('selectedOrder').selected;
  736. if (reset && this.numpadState)
  737. this.numpadState.reset();
  738. this.updateSummary();
  739. },
  740. render_element: function() {
  741. this.$element.empty();
  742. this.currentOrderLines.each(_.bind( function(orderLine) {
  743. var line = new OrderlineWidget(null, {
  744. model: orderLine,
  745. order: this.shop.get('selectedOrder')
  746. });
  747. line.on_selected.add(_.bind(this.selectedLine, this));
  748. line.appendTo(this.$element);
  749. }, this));
  750. this.updateSummary();
  751. },
  752. updateSummary: function() {
  753. var currentOrder, tax, total, totalTaxExcluded;
  754. currentOrder = this.shop.get('selectedOrder');
  755. total = currentOrder.getTotal();
  756. totalTaxExcluded = currentOrder.getTotalTaxExcluded();
  757. tax = currentOrder.getTax();
  758. $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
  759. $('#tax').html(tax.toFixed(2)).hide().fadeIn();
  760. $('#total').html(total.toFixed(2)).hide().fadeIn();
  761. },
  762. });
  763. /*
  764. "Products" step.
  765. */
  766. var CategoryWidget = db.web.OldWidget.extend({
  767. start: function() {
  768. this.$element.find(".oe-pos-categories-list a").click(_.bind(this.changeCategory, this));
  769. $("#products-screen-ol").css("top",$("#products-screen-categories").height()+"px");
  770. },
  771. template_fct: qweb_template('pos-category-template'),
  772. render_element: function() {
  773. var self = this;
  774. var c;
  775. this.$element.html(this.template_fct({
  776. breadcrumb: (function() {
  777. var _i, _len, _results;
  778. _results = [];
  779. for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
  780. c = self.ancestors[_i];
  781. _results.push(pos.categories[c]);
  782. }
  783. return _results;
  784. })(),
  785. categories: (function() {
  786. var _i, _len, _results;
  787. _results = [];
  788. for (_i = 0, _len = self.children.length; _i < _len; _i++) {
  789. c = self.children[_i];
  790. _results.push(pos.categories[c]);
  791. }
  792. return _results;
  793. })()
  794. }));
  795. },
  796. changeCategory: function(a) {
  797. var id = $(a.target).data("category-id");
  798. this.on_change_category(id);
  799. },
  800. on_change_category: function(id) {},
  801. });
  802. var ProductWidget = db.web.OldWidget.extend({
  803. tag_name:'li',
  804. template_fct: qweb_template('pos-product-template'),
  805. init: function(parent, options) {
  806. this._super(parent);
  807. this.model = options.model;
  808. this.shop = options.shop;
  809. },
  810. start: function(options) {
  811. $("a", this.$element).click(_.bind(this.addToOrder, this));
  812. },
  813. addToOrder: function(event) {
  814. /* Preserve the category URL */
  815. event.preventDefault();
  816. return (this.shop.get('selectedOrder')).addProduct(this.model);
  817. },
  818. render_element: function() {
  819. this.$element.addClass("product");
  820. this.$element.html(this.template_fct(this.model.toJSON()));
  821. return this;
  822. },
  823. });
  824. var ProductListWidget = db.web.OldWidget.extend({
  825. init: function(parent, options) {
  826. this._super(parent);
  827. this.model = options.model;
  828. this.shop = options.shop;
  829. this.shop.get('products').bind('reset', this.render_element, this);
  830. },
  831. render_element: function() {
  832. this.$element.empty();
  833. (this.shop.get('products')).each(_.bind( function(product) {
  834. var p = new ProductWidget(null, {
  835. model: product,
  836. shop: this.shop
  837. });
  838. p.appendTo(this.$element);
  839. }, this));
  840. return this;
  841. },
  842. });
  843. /*
  844. "Payment" step.
  845. */
  846. var PaymentlineWidget = db.web.OldWidget.extend({
  847. tag_name: 'tr',
  848. template_fct: qweb_template('pos-paymentline-template'),
  849. init: function(parent, options) {
  850. this._super(parent);
  851. this.model = options.model;
  852. this.model.bind('change', this.changedAmount, this);
  853. },
  854. on_delete: function() {},
  855. changeAmount: function(event) {
  856. var newAmount;
  857. newAmount = event.currentTarget.value;
  858. if (newAmount && !isNaN(newAmount)) {
  859. this.amount = parseFloat(newAmount);
  860. this.model.set({
  861. amount: this.amount,
  862. });
  863. }
  864. },
  865. changedAmount: function() {
  866. if (this.amount !== this.model.get('amount'))
  867. this.render_element();
  868. },
  869. render_element: function() {
  870. this.amount = this.model.get('amount');
  871. this.$element.html(this.template_fct({
  872. name: (this.model.get('journal_id'))[1],
  873. amount: this.amount,
  874. }));
  875. this.$element.addClass('paymentline');
  876. $('input', this.$element).keyup(_.bind(this.changeAmount, this));
  877. $('.delete-payment-line', this.$element).click(this.on_delete);
  878. },
  879. });
  880. var PaymentWidget = db.web.OldWidget.extend({
  881. init: function(parent, options) {
  882. this._super(parent);
  883. this.model = options.model;
  884. this.shop = options.shop;
  885. this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
  886. this.bindPaymentLineEvents();
  887. this.bindOrderLineEvents();
  888. },
  889. paymentLineList: function() {
  890. return this.$element.find('#paymentlines');
  891. },
  892. start: function() {
  893. $('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
  894. $('.oe-back-to-products', this.$element).click(_.bind(this.back, this));
  895. },
  896. back: function() {
  897. this.shop.get('selectedOrder').set({"step": "products"});
  898. },
  899. validateCurrentOrder: function() {
  900. var callback, currentOrder;
  901. currentOrder = this.shop.get('selectedOrder');
  902. $('button#validate-order', this.$element).attr('disabled', 'disabled');
  903. pos.pushOrder(currentOrder.exportAsJSON()).then(_.bind(function() {
  904. $('button#validate-order', this.$element).removeAttr('disabled');
  905. return currentOrder.set({
  906. validated: true
  907. });
  908. }, this));
  909. },
  910. bindPaymentLineEvents: function() {
  911. this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
  912. this.currentPaymentLines.bind('add', this.addPaymentLine, this);
  913. this.currentPaymentLines.bind('remove', this.render_element, this);
  914. this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
  915. },
  916. bindOrderLineEvents: function() {
  917. this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
  918. this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
  919. },
  920. changeSelectedOrder: function() {
  921. this.currentPaymentLines.unbind();
  922. this.bindPaymentLineEvents();
  923. this.currentOrderLines.unbind();
  924. this.bindOrderLineEvents();
  925. this.render_element();
  926. },
  927. addPaymentLine: function(newPaymentLine) {
  928. var x = new PaymentlineWidget(null, {
  929. model: newPaymentLine
  930. });
  931. x.on_delete.add(_.bind(this.deleteLine, this, x));
  932. x.appendTo(this.paymentLineList());
  933. },
  934. render_element: function() {
  935. this.paymentLineList().empty();
  936. this.currentPaymentLines.each(_.bind( function(paymentLine) {
  937. this.addPaymentLine(paymentLine);
  938. }, this));
  939. this.updatePaymentSummary();
  940. },
  941. deleteLine: function(lineWidget) {
  942. this.currentPaymentLines.remove([lineWidget.model]);
  943. },
  944. updatePaymentSummary: function() {
  945. var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
  946. currentOrder = this.shop.get('selectedOrder');
  947. paidTotal = currentOrder.getPaidTotal();
  948. dueTotal = currentOrder.getTotal();
  949. this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
  950. this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
  951. remainingAmount = dueTotal - paidTotal;
  952. remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
  953. $('#payment-remaining').html(remaining);
  954. },
  955. setNumpadState: function(numpadState) {
  956. if (this.numpadState) {
  957. this.numpadState.unbind('setValue', this.setValue);
  958. this.numpadState.unbind('change:mode', this.setNumpadMode);
  959. }
  960. this.numpadState = numpadState;
  961. if (this.numpadState) {
  962. this.numpadState.bind('setValue', this.setValue, this);
  963. this.numpadState.bind('change:mode', this.setNumpadMode, this);
  964. this.numpadState.reset();
  965. this.setNumpadMode();
  966. }
  967. },
  968. setNumpadMode: function() {
  969. this.numpadState.set({mode: 'payment'});
  970. },
  971. setValue: function(val) {
  972. this.currentPaymentLines.last().set({amount: val});
  973. },
  974. });
  975. var ReceiptWidget = db.web.OldWidget.extend({
  976. init: function(parent, options) {
  977. this._super(parent);
  978. this.model = options.model;
  979. this.shop = options.shop;
  980. this.user = pos.get('user');
  981. this.company = pos.get('company');
  982. this.shop_obj = pos.get('shop');
  983. },
  984. start: function() {
  985. this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
  986. this.changeSelectedOrder();
  987. },
  988. render_element: function() {
  989. this.$element.html(qweb_template('pos-receipt-view'));
  990. $('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
  991. $('button#print-the-ticket', this.$element).click(_.bind(this.print, this));
  992. },
  993. print: function() {
  994. window.print();
  995. },
  996. finishOrder: function() {
  997. this.shop.get('selectedOrder').destroy();
  998. },
  999. changeSelectedOrder: function() {
  1000. if (this.currentOrderLines)
  1001. this.currentOrderLines.unbind();
  1002. this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
  1003. this.currentOrderLines.bind('add', this.refresh, this);
  1004. this.currentOrderLines.bind('change', this.refresh, this);
  1005. this.currentOrderLines.bind('remove', this.refresh, this);
  1006. if (this.currentPaymentLines)
  1007. this.currentPaymentLines.unbind();
  1008. this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
  1009. this.currentPaymentLines.bind('all', this.refresh, this);
  1010. this.refresh();
  1011. },
  1012. refresh: function() {
  1013. this.currentOrder = this.shop.get('selectedOrder');
  1014. $('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
  1015. },
  1016. });
  1017. var OrderButtonWidget = db.web.OldWidget.extend({
  1018. tag_name: 'li',
  1019. template_fct: qweb_template('pos-order-selector-button-template'),
  1020. init: function(parent, options) {
  1021. this._super(parent);
  1022. this.order = options.order;
  1023. this.shop = options.shop;
  1024. this.order.bind('destroy', _.bind( function() {
  1025. return this.stop();
  1026. }, this));
  1027. this.shop.bind('change:selectedOrder', _.bind( function(shop) {
  1028. var selectedOrder;
  1029. selectedOrder = shop.get('selectedOrder');
  1030. if (this.order === selectedOrder) {
  1031. this.setButtonSelected();
  1032. }
  1033. }, this));
  1034. },
  1035. start: function() {
  1036. $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
  1037. $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
  1038. },
  1039. selectOrder: function(event) {
  1040. this.shop.set({
  1041. selectedOrder: this.order
  1042. });
  1043. },
  1044. setButtonSelected: function() {
  1045. $('.selected-order').removeClass('selected-order');
  1046. this.$element.addClass('selected-order');
  1047. },
  1048. closeOrder: function(event) {
  1049. this.order.destroy();
  1050. },
  1051. render_element: function() {
  1052. this.$element.html(this.template_fct({widget:this}));
  1053. this.$element.addClass('order-selector-button');
  1054. }
  1055. });
  1056. var ShopWidget = db.web.OldWidget.extend({
  1057. init: function(parent, options) {
  1058. this._super(parent);
  1059. this.shop = options.shop;
  1060. },
  1061. start: function() {
  1062. $('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
  1063. (this.shop.get('orders')).bind('add', this.orderAdded, this);
  1064. (this.shop.get('orders')).add(new Order);
  1065. this.productListView = new ProductListWidget(null, {
  1066. shop: this.shop
  1067. });
  1068. this.productListView.$element = $("#products-screen-ol");
  1069. this.productListView.render_element();
  1070. this.productListView.start();
  1071. this.paypadView = new PaypadWidget(null, {
  1072. shop: this.shop
  1073. });
  1074. this.paypadView.$element = $('#paypad');
  1075. this.paypadView.render_element();
  1076. this.paypadView.start();
  1077. this.numpadView = new NumpadWidget(null);
  1078. this.numpadView.$element = $('#numpad');
  1079. this.numpadView.start();
  1080. this.orderView = new OrderWidget(null, {
  1081. shop: this.shop,
  1082. });
  1083. this.orderView.$element = $('#current-order-content');
  1084. this.orderView.start();
  1085. this.paymentView = new PaymentWidget(null, {
  1086. shop: this.shop
  1087. });
  1088. this.paymentView.$element = $('#payment-screen');
  1089. this.paymentView.render_element();
  1090. this.paymentView.start();
  1091. this.receiptView = new ReceiptWidget(null, {
  1092. shop: this.shop,
  1093. });
  1094. this.receiptView.replace($('#receipt-screen'));
  1095. this.stepSwitcher = new StepSwitcher(this, {shop: this.shop});
  1096. this.shop.bind('change:selectedOrder', this.changedSelectedOrder, this);
  1097. this.changedSelectedOrder();
  1098. },
  1099. createNewOrder: function() {
  1100. var newOrder;
  1101. newOrder = new Order;
  1102. (this.shop.get('orders')).add(newOrder);
  1103. this.shop.set({
  1104. selectedOrder: newOrder
  1105. });
  1106. },
  1107. orderAdded: function(newOrder) {
  1108. var newOrderButton;
  1109. newOrderButton = new OrderButtonWidget(null, {
  1110. order: newOrder,
  1111. shop: this.shop
  1112. });
  1113. newOrderButton.appendTo($('#orders'));
  1114. newOrderButton.selectOrder();
  1115. },
  1116. changedSelectedOrder: function() {
  1117. if (this.currentOrder) {
  1118. this.currentOrder.unbind('change:step', this.changedStep);
  1119. }
  1120. this.currentOrder = this.shop.get('selectedOrder');
  1121. this.currentOrder.bind('change:step', this.changedStep, this);
  1122. this.changedStep();
  1123. },
  1124. changedStep: function() {
  1125. var step = this.currentOrder.get('step');
  1126. this.orderView.setNumpadState(null);
  1127. this.paymentView.setNumpadState(null);
  1128. if (step === 'products') {
  1129. this.orderView.setNumpadState(this.numpadView.state);
  1130. } else if (step === 'payment') {
  1131. this.paymentView.setNumpadState(this.numpadView.state);
  1132. }
  1133. },
  1134. });
  1135. var App = (function() {
  1136. function App($element) {
  1137. this.initialize($element);
  1138. }
  1139. App.prototype.initialize = function($element) {
  1140. this.shop = new Shop;
  1141. this.shopView = new ShopWidget(null, {
  1142. shop: this.shop
  1143. });
  1144. this.shopView.$element = $element;
  1145. this.shopView.start();
  1146. this.categoryView = new CategoryWidget(null, 'products-screen-categories');
  1147. this.categoryView.on_change_category.add_last(_.bind(this.category, this));
  1148. this.category();
  1149. };
  1150. App.prototype.category = function(id) {
  1151. var c, products, self = this;
  1152. id = !id ? 0 : id;
  1153. c = pos.categories[id];
  1154. this.categoryView.ancestors = c.ancestors;
  1155. this.categoryView.children = c.children;
  1156. this.categoryView.render_element();
  1157. this.categoryView.start();
  1158. allProducts = pos.store.get('product.product');
  1159. allPackages = pos.store.get('product.packaging');
  1160. products = pos.store.get('product.product').filter( function(p) {
  1161. var _ref;
  1162. return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
  1163. });
  1164. (this.shop.get('products')).reset(products);
  1165. //returns true if the code is a valid EAN codebar number by checking the control digit.
  1166. var checkEan = function(code) {
  1167. var st1 = code.slice();
  1168. var st2 = st1.slice(0,st1.length-1).reverse();
  1169. // some EAN13 barcodes have a length of 12, as they start by 0
  1170. while (st2.length < 12) {
  1171. st2.push(0);
  1172. }
  1173. var countSt3 = 1;
  1174. var st3 = 0;
  1175. $.each(st2, function() {
  1176. if (countSt3%2 === 1) {
  1177. st3 += this;
  1178. }
  1179. countSt3 ++;
  1180. });
  1181. st3 *= 3;
  1182. var st4 = 0;
  1183. var countSt4 = 1;
  1184. $.each(st2, function() {
  1185. if (countSt4%2 === 0) {
  1186. st4 += this;
  1187. }
  1188. countSt4 ++;
  1189. });
  1190. var st5 = st3 + st4;
  1191. var cd = (10 - (st5%10)) % 10;
  1192. return code[code.length-1] === cd;
  1193. }
  1194. var codeNumbers = [];
  1195. // returns a product that has a packaging with an EAN matching to provided ean string.
  1196. // returns undefined if no such product is found.
  1197. var getProductByEAN = function(ean) {
  1198. var prefix = ean.substring(0,2);
  1199. var scannedProductModel = undefined;
  1200. if (prefix in {'02':'', '22':'', '24':'', '26':'', '28':''}) {
  1201. // PRICE barcode
  1202. var itemCode = ean.substring(0,7);
  1203. var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
  1204. if (scannedPackaging !== undefined) {
  1205. scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
  1206. scannedProductModel.list_price = Number(ean.substring(7,12))/100;
  1207. }
  1208. } else if (prefix in {'21':'','23':'','27':'','29':'','25':''}) {
  1209. // WEIGHT barcode
  1210. var weight = Number(barcode.substring(7,12))/1000;
  1211. var itemCode = ean.substring(0,7);
  1212. var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
  1213. if (scannedPackaging !== undefined) {
  1214. scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
  1215. scannedProductModel.list_price *= weight;
  1216. scannedProductModel.name += ' - ' + weight + ' Kg.';
  1217. }
  1218. } else {
  1219. // UNIT barcode
  1220. scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === ean;}); //TODO DOES NOT SCALE
  1221. }
  1222. return scannedProductModel;
  1223. }
  1224. // The barcode readers acts as a keyboard, we catch all keyup events and try to find a
  1225. // barcode sequence in the typed keys, then act accordingly.
  1226. $('body').delegate('','keyup', function (e){
  1227. //We only care abo…

Large files files are truncated, but you can click here to view the full file