Merge lp:~mmakonnen/openobject-addons/point_of_sale_enhanced-70 into lp:openobject-addons

Proposed by Mauricio Matute
Status: Needs review
Proposed branch: lp:~mmakonnen/openobject-addons/point_of_sale_enhanced-70
Merge into: lp:openobject-addons
Diff against target: 1267 lines (+868/-4) (has conflicts)
9 files modified
point_of_sale/controllers/main.py (+21/-0)
point_of_sale/point_of_sale.py (+11/-2)
point_of_sale/static/src/css/pos.css (+173/-0)
point_of_sale/static/src/js/db.js (+78/-0)
point_of_sale/static/src/js/devices.js (+179/-0)
point_of_sale/static/src/js/models.js (+71/-2)
point_of_sale/static/src/js/screens.js (+113/-0)
point_of_sale/static/src/js/widgets.js (+138/-0)
point_of_sale/static/src/xml/pos.xml (+84/-0)
Text conflict in point_of_sale/controllers/main.py
Text conflict in point_of_sale/point_of_sale.py
Text conflict in point_of_sale/static/src/js/db.js
Text conflict in point_of_sale/static/src/js/models.js
Text conflict in point_of_sale/static/src/js/screens.js
Text conflict in point_of_sale/static/src/js/widgets.js
Text conflict in point_of_sale/static/src/xml/pos.xml
To merge this branch: bzr merge lp:~mmakonnen/openobject-addons/point_of_sale_enhanced-70
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+194755@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

8522. By Michael Telahun Makonnen

In point_of_sale module add ability to select customer from the POS screen

Add a button in the upper right header which, when clicked, pops up a
dialog box to select a customer. You can search for a customer by
name, tin no, email, phone or mobile.

Currently doesn't support adding a new customer from this screen. You
will have to leave the POS and add the customer in the normal way.

8521. By Michael Telahun Makonnen

Add a keypad input device to the point of sale

It uses the numeric keypad on the keyboard to mimic the keypad found on
most cash registers. The numbers are used to match a product by its code,
and the non-numeric keys are used as modifiers:
'/' - X Quantity
'*' - AMT Manual Price Override
'-' - % - Discount %
'+' - PLU (Product Code)
Keyboard "Back Space" and "Delete" keys may be used to clear the buffer.

It may be useful to tape over the modifier keys with the appropriate
symbols to make it easier for the cashiers to get used to it.

Implementation Notes:
---------------------
  - By necessity the product code may contain only numeric identifiers
  - Doesn't emulate VOID key (using Enter key would interfere with
    bar code reader)
  - Doesn't emulate surcharge key (% +)

Some examples of usage showing what the operation would look like
on a regular Cash Register and using the numeric keypad:

1. Add a product
   (code: 100, quantity: 1, Price:list price, no discount)
    Cash Register: 100 [PLU]
    Numeric Keypad: 100 +

2. Add 9 pieces of a product
   (code: 102, quantity: 9, Price: list price, no discount)
    Cash Register: 9 [ X ] 102 [PLU]
    Numeric Keypad: 9 / 102 +

3. Add 3 pieces of a product and set the price at 9.99
   (code: 450, quantity: 3, Price: 9.99, no discount)
    Cash Register: 3 [ X ] 9.99 [AMT] 450 [PLU]
    Numeric Keypad: 3 / 9.99 * 450 +

4. Add 5 pieces of a product, set price to 23.50, and discount it by 10%
   (code: 300, quantity: 5, price: 23.50, discount: 10%)
    Cash Register: 5 [ X ] 23.50 [AMT] 10 [% -] 300 [PLU]
    Numeric Keypad: 5 / 23.50 * 10 - 300 +

8520. By Michael Telahun Makonnen

POS: When we hit [Enter] in the search box add the product with the matching product code

The previous commit only added the product if it was the only one returned by the search.
Now, it will search the list of matched products for one with the same exact
product code as the search term. If it finds a match it will add it to the order.

8519. By Michael Telahun Makonnen

POS: hitting [Enter] in the search field will add the product to the order

8518. By Michael Telahun Makonnen

In POS allow searching by product code as well

8517. By Michael Telahun Makonnen

In the POS module show the product code before the price

If you have a lot of similarly (identicaly) named products it becomes difficult to
tell them apart in the POS unless you can also see their code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'point_of_sale/controllers/main.py'
2--- point_of_sale/controllers/main.py 2013-10-22 17:06:59 +0000
3+++ point_of_sale/controllers/main.py 2013-11-11 21:43:26 +0000
4@@ -89,8 +89,29 @@
5 """
6 print 'scan_item_error_unrecognized: ' + str(ean)
7
8+<<<<<<< TREE
9 @http.route('/pos/help_needed', type='json', auth='admin')
10 def help_needed(self):
11+=======
12+ @openerp.addons.web.http.jsonrequest
13+ def keypad_item_success(self, request, data):
14+ """
15+ A product has been entered by keypad with success
16+ """
17+ print 'keypad_item_success: ' + str(data)
18+ return
19+
20+ @openerp.addons.web.http.jsonrequest
21+ def keypad_item_error_unrecognized(self, request, data):
22+ """
23+ A product has been entered by keypad without success
24+ """
25+ print 'keypad_item_error_unrecognized: ' + str(data)
26+ return
27+
28+ @openerp.addons.web.http.jsonrequest
29+ def help_needed(self, request):
30+>>>>>>> MERGE-SOURCE
31 """
32 The user wants an help (ex: light is on)
33 """
34
35=== modified file 'point_of_sale/point_of_sale.py'
36--- point_of_sale/point_of_sale.py 2013-10-18 12:58:18 +0000
37+++ point_of_sale/point_of_sale.py 2013-11-11 21:43:26 +0000
38@@ -502,15 +502,24 @@
39 for tmp_order in orders:
40 to_invoice = tmp_order['to_invoice']
41 order = tmp_order['data']
42-
43-
44+<<<<<<< TREE
45+
46+
47+=======
48+ partner_id = order['partner_id'] if 'partner_id' in order else False
49+>>>>>>> MERGE-SOURCE
50 order_id = self.create(cr, uid, {
51 'name': order['name'],
52 'user_id': order['user_id'] or False,
53 'session_id': order['pos_session_id'],
54 'lines': order['lines'],
55+<<<<<<< TREE
56 'pos_reference':order['name'],
57 'partner_id': order['partner_id'] or False
58+=======
59+ 'pos_reference':order['name'],
60+ 'partner_id':partner_id,
61+>>>>>>> MERGE-SOURCE
62 }, context)
63 for payments in order['statement_ids']:
64 payment = payments[2]
65
66=== modified file 'point_of_sale/static/src/css/pos.css'
67--- point_of_sale/static/src/css/pos.css 2013-09-17 10:19:10 +0000
68+++ point_of_sale/static/src/css/pos.css 2013-11-11 21:43:26 +0000
69@@ -205,6 +205,15 @@
70 box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
71 }
72
73+.point-of-sale #rightheader .customername{
74+ float:right;
75+ color:#DDD;
76+ font-size:16px;
77+ margin-right:32px;
78+ margin-top:10px;
79+ font-style:italic;
80+}
81+
82 /* c) The session buttons */
83
84 .point-of-sale #rightheader .header-button{
85@@ -605,6 +614,15 @@
86 max-width: 120px;
87 }
88
89+.point-of-sale .product .code-tag {
90+ position: absolute;
91+ top: 2px;
92+ left: 2px;
93+ vertical-align: top;
94+ line-height: 13px;
95+ padding: 2px 5px;
96+}
97+
98 .point-of-sale .product .price-tag {
99 position: absolute;
100 top: 2px;
101@@ -1373,6 +1391,161 @@
102 line-height:180px;
103 }
104
105+.point-of-sale .modal-dialog .popup-selection{
106+ position: absolute;
107+ left:30%;
108+ top:10%;
109+ width: 700px;
110+ height:70%;
111+ padding:10px;
112+ padding-top:20px;
113+ text-align:left;
114+ font-size:14px;
115+ font-weight:bold;
116+ background-color: #F0EEEE;
117+ border-radius: 4px;
118+ box-shadow: 0px -1px white, 0px 1px white, 0px 4px #949494, 0px 10px 20px rgba(0, 0, 0, 0.3);
119+ z-index:1200;
120+}
121+
122+.point-of-sale .popup-selection .button{
123+ float:right;
124+ width: 110px;
125+ height: 40px;
126+ line-height:40px;
127+ text-align:center;
128+ margin:3px;
129+ margin-top:10px;
130+ margin-right:50px;
131+
132+ font-size: 14px;
133+ font-weight: bold;
134+
135+ cursor: pointer;
136+
137+ border: 1px solid #cacaca;
138+ border-radius: 4px;
139+
140+ background: #e2e2e2;
141+ background: -webkit-linear-gradient(#f0f0f0, #e2e2e2);
142+ background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
143+ background: -ms-linear-gradient(#f0f0f0, #e2e2e2);
144+ background: linear-gradient(#f0f0f0, #e2e2e2);
145+ -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
146+ -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
147+ box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
148+}
149+.point-of-sale .popup-selection .button:hover {
150+ color: white;
151+ background: #7f82ac;
152+ border: 1px solid #7f82ac;
153+ background: -webkit-linear-gradient(#9d9fc5, #7f82ac);
154+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
155+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
156+ background: linear-gradient(#9d9fc5, #7f82ac);
157+
158+ -webkit-transition-property: background, border;
159+ -webkit-transition-duration: 0.2s;
160+ -webkit-transition-timing-function: ease-out;
161+}
162+
163+/* customer selection */
164+
165+#customer-cancel {
166+ position: absolute;
167+ left: 10px;
168+ bottom: 10px;
169+}
170+
171+.point-of-sale .customer-list-container {
172+ position: absolute;
173+ top:70px;
174+ left:5px;
175+ right:5px;
176+ bottom: 70px;
177+ text-align: left;
178+ overflow: auto;
179+}
180+
181+.point-of-sale .customer-list-scroller {
182+ -webkit-box-sizing: border-box;
183+ -moz-box-sizing: border-box;
184+ -ms-box-sizing: border-box;
185+ box-sizing: border-box;
186+ width:98%;
187+}
188+
189+.customer-list-scroller ol {
190+ list-style: none;
191+}
192+
193+.customer-list-scroller ol li { }
194+
195+.customer-list-scroller ol li a {
196+ display:block;
197+ text-decoration:none;
198+ color:#000000;
199+ background-color:#FFFFFF;
200+ line-height:20px;
201+ border-bottom-style:solid;
202+ border-bottom-width:1px;
203+ border-bottom-color:#CCCCCC;
204+ padding-left:10px;
205+ cursor:pointer;
206+}
207+
208+.customer-list-scroller ol li a:hover {
209+ color:#FFFFFF;
210+ background-image:url(/point_of_sale/static/src/img/hover.png);
211+ background-repeat:repeat-x;
212+}
213+
214+.point-of-sale .customer {
215+ width: 100%;
216+}
217+
218+.point-of-sale .customer a {
219+ width: 100%;
220+}
221+
222+.point-of-sale .customer .customer-field {
223+ width: 130px;
224+ padding: 5px;
225+ margin: 5px;
226+}
227+
228+.point-of-sale .customer .customer-name{
229+ width: 200px;
230+ padding: 5px;
231+ margin: 5px;
232+}
233+
234+.point-of-sale .customer .customer-phone {
235+ width: 50px;
236+ padding: 5px;
237+ margin: 5px;
238+}
239+
240+.point-of-sale .customer-searchbox {
241+ right: 2px;
242+}
243+.point-of-sale .customer-searchbox input {
244+ width: 130px;
245+ border-radius: 11px;
246+ border: 1px solid #cecbcb;
247+ padding: 3px 19px;
248+ margin: 6px;
249+ background: url("../img/search.png") no-repeat 5px;
250+ background-color: white;
251+}
252+.point-of-sale .customer-search-clear {
253+ postion: absolute;
254+ top: 11px;
255+ right: 600px;
256+ cursor: pointer;
257+ display: none;
258+}
259+
260 /* ********* The ScrollBarWidget ********* */
261
262 .point-of-sale .scrollbar{
263
264=== added file 'point_of_sale/static/src/img/hover.png'
265Binary files point_of_sale/static/src/img/hover.png 1970-01-01 00:00:00 +0000 and point_of_sale/static/src/img/hover.png 2013-11-11 21:43:26 +0000 differ
266=== modified file 'point_of_sale/static/src/js/db.js'
267--- point_of_sale/static/src/js/db.js 2013-09-23 14:51:39 +0000
268+++ point_of_sale/static/src/js/db.js 2013-11-11 21:43:26 +0000
269@@ -54,7 +54,11 @@
270 this.category_search_string = {};
271 this.packagings_by_id = {};
272 this.packagings_by_product_id = {};
273+<<<<<<< TREE
274 this.packagings_by_ean13 = {};
275+=======
276+ this.customer_list_search_strings = '';
277+>>>>>>> MERGE-SOURCE
278 },
279 /* returns the category object from its id. If you pass a list of id as parameters, you get
280 * a list of category objects.
281@@ -151,9 +155,15 @@
282 if(product.ean13){
283 str += '|' + product.ean13;
284 }
285+<<<<<<< TREE
286 if(product.default_code){
287 str += '|' + product.default_code;
288 }
289+=======
290+ if(product.code){
291+ str += '|' + product.code;
292+ }
293+>>>>>>> MERGE-SOURCE
294 var packagings = this.packagings_by_product_id[product.id] || [];
295 for(var i = 0; i < packagings.length; i++){
296 str += '|' + packagings[i].ean;
297@@ -216,6 +226,35 @@
298 }
299 }
300 },
301+ _customer_search_string: function(customer){
302+ var str = '' + customer.id + ':' + customer.name;
303+ if(customer.vat){
304+ str += '|' + customer.vat;
305+ }
306+ if(customer.email){
307+ str += '|' + customer.email;
308+ }
309+ if(customer.phone){
310+ str += '|' + customer.phone;
311+ }
312+ if(customer.mobile){
313+ str += '|' + customer.mobile;
314+ }
315+ return str + '\n';
316+ },
317+ add_customers: function(customers){
318+ var stored_customers = this.load('customers',{});
319+
320+ if(!customers instanceof Array){
321+ customers = [customers];
322+ }
323+ for(var i = 0, len = customers.length; i < len; i++){
324+ var c = customers[i];
325+ this.customer_list_search_strings += this._customer_search_string(c);
326+ stored_customers[c.id] = c;
327+ }
328+ this.save('customers',stored_customers);
329+ },
330 /* removes all the data from the database. TODO : being able to selectively remove data */
331 clear: function(stores){
332 for(var i = 0, len = arguments.length; i < len; i++){
333@@ -245,9 +284,20 @@
334 }
335 return undefined;
336 },
337+<<<<<<< TREE
338 get_product_by_reference: function(ref){
339 return this.product_by_reference[ref];
340 },
341+=======
342+ get_product_by_code: function(code){
343+ var products = this.load('products', {});
344+ for(var i in products){
345+ if(products[i] && products[i].code === code){
346+ return products[i];
347+ }
348+ }
349+ },
350+>>>>>>> MERGE-SOURCE
351 get_product_by_category: function(category_id){
352 var product_ids = this.product_by_category_id[category_id];
353 var list = [];
354@@ -258,6 +308,17 @@
355 }
356 return list;
357 },
358+ get_customer_by_id: function(id){
359+ return this.load('customers',{})[id];
360+ },
361+ get_all_customers: function(){
362+ list = [];
363+ stored_customers = this.load('customers',{});
364+ for (var i in stored_customers) {
365+ list.push(stored_customers[i]);
366+ }
367+ return list;
368+ },
369 /* returns a list of products with :
370 * - a category that is or is a child of category_id,
371 * - a name, package or ean13 containing the query (case insensitive)
372@@ -276,6 +337,23 @@
373 }
374 return results;
375 },
376+ /* returns a list of customers with :
377+ * - a name, TIN, email, or phone/mobile containing the query (case insensitive)
378+ */
379+ search_customers: function(query){
380+ var re = RegExp("([0-9]+):.*?"+query,"gi");
381+ var results = [];
382+ for(var i = 0; i < this.limit; i++){
383+ r = re.exec(this.customer_list_search_strings);
384+ if(r){
385+ var id = Number(r[1]);
386+ results.push(this.get_customer_by_id(id));
387+ }else{
388+ break;
389+ }
390+ }
391+ return results;
392+ },
393 add_order: function(order){
394 var order_id = order.uid;
395 var orders = this.load('orders',[]);
396
397=== modified file 'point_of_sale/static/src/js/devices.js'
398--- point_of_sale/static/src/js/devices.js 2013-09-24 11:07:01 +0000
399+++ point_of_sale/static/src/js/devices.js 2013-11-11 21:43:26 +0000
400@@ -156,6 +156,18 @@
401 return this.message('scan_item_error_unrecognized',{ean: ean});
402 },
403
404+ //a product has been entered by keypad and recognized with success
405+ // data is a parsed {product.code,qty,price} object
406+ keypad_item_success: function(data){
407+ return this.message('keypad_item_success',{data: data});
408+ },
409+
410+ // a product has been entered by keypad but not recognized
411+ // data is a parsed {product.code,qty,price} object
412+ keypad_item_error_unrecognized: function(data){
413+ return this.message('keypad_item_error_unrecognized',{data: data});
414+ },
415+
416 //the client is asking for help
417 help_needed: function(){
418 return this.message('help_needed');
419@@ -600,5 +612,172 @@
420 $('body').off('keypress', this.handler)
421 },
422 });
423+
424+ // this module mimics a keypad-only cash register. Use connect() and
425+ // disconnect() to activate and deactivate it. Use set_action_callback to
426+ // tell it what to do when the cashier enters product data(qty, price, etc).
427+ module.Keypad = instance.web.Class.extend({
428+ init: function(attributes){
429+ this.pos = attributes.pos;
430+ this.action_callback = undefined;
431+ this.saved_callback_stack = [];
432+ },
433+
434+ save_callback: function(){
435+ this.saved_callback_stack.push(this.action_callback);
436+ },
437+
438+ restore_callback: function(){
439+ if (this.saved_callback_stack.length > 0) {
440+ this.action_callback = this.saved_callback_stack.pop();
441+ }
442+ },
443+
444+ set_action_callback: function(callback){
445+ this.action_callback = callback
446+ },
447+
448+ //remove action callback
449+ reset_action_callback: function(){
450+ this.action_callback = undefined;
451+ },
452+
453+ reset_parse_result: function(parse_result) {
454+ parse_result.code = 0;
455+ parse_result.qty = 0;
456+ parse_result.priceOverride = false;
457+ parse_result.price = 0.00;
458+ parse_result.discount = 0.00;
459+ parse_result.void_last_line = false;
460+ },
461+
462+ copy_parse_result: function(src) {
463+ var dst = {
464+ code: null,
465+ qty: 1,
466+ priceOverride: false,
467+ price: 0.00,
468+ discount: 0.00,
469+ void_last_line: false,
470+ };
471+ dst.code = src.code;
472+ dst.qty = src.qty;
473+ dst.priceOverride = src.priceOverride;
474+ dst.price = src.price;
475+ dst.discount = src.discount;
476+ dst.void_last_line = src.void_last_line;
477+ return dst;
478+ },
479+
480+ // starts catching keyboard events and tries to interpret keystrokes,
481+ // calling the callback when needed.
482+ connect: function(){
483+ var self = this;
484+ var KC_PLU = 107; // KeyCode: Product Code (Keypad '+')
485+ var KC_QTY = 111; // KeyCode: Quantity (Keypad '/')
486+ var KC_AMT = 106; // KeyCode: Price (Keypad '*')
487+ var KC_DISC = 109; // KeyCode: Discount Percentage [0..100] (Keypad '-')
488+ var KC_VOID = 13; // KeyCode: Void current line (Keyboard/Keypad Enter key)
489+ var KC_CLR1 = 46; // KeyCode: Clear last line of order (Keyboard Delete key)
490+ var KC_CLR2 = 8; // KeyCode: Clear current line (Keyboard Backspace key)
491+ var codeNumbers = [];
492+ var codeChars = [];
493+ var parse_result = {
494+ code: null,
495+ qty: 1,
496+ priceOverride: false,
497+ price: 0.00,
498+ discount: 0.00,
499+ void_last_line: false,
500+ };
501+ var kc_lookup = {
502+ 96: '0',
503+ 97: '1',
504+ 98: '2',
505+ 99: '3',
506+ 100: '4',
507+ 101: '5',
508+ 102: '6',
509+ 103: '7',
510+ 104: '8',
511+ 105: '9',
512+ 106: '*',
513+ 107: '+',
514+ 109: '-',
515+ 110: '.',
516+ 111: '/',
517+ };
518+
519+ // Catch keyup events anywhere in the POS interface. Barcode reader also does this, but won't interfere
520+ // because it looks for a specific timing between keyup events. On the plus side this should mean that you
521+ // can use both the keypad and the barcode reader during the same session (but for separate order lines).
522+ // This could be useful in cases where the scanner can't read the barcode.
523+ $('body').delegate('','keyup', function (e){
524+ console.log('keyup:'+String.fromCharCode(e.keyCode)+' '+e.keyCode,e);
525+ //We only care about numbers and modifiers
526+ token = e.keyCode;
527+ if ((token >= 96 && token <= 111) || token === KC_PLU || token === KC_QTY || token === KC_AMT) {
528+
529+ if (token === KC_PLU) {
530+ parse_result.code = codeChars.join('');
531+ var res = self.copy_parse_result(parse_result);
532+ codeNumbers = [];
533+ codeChars = [];
534+ self.reset_parse_result(parse_result);
535+ console.log('PLU token: code:'+res.code+', qty:'+res.qty+', price:'+res.price+', discount:'+res.discount);
536+ self.action_callback(res);
537+ } else if (token === KC_QTY) {
538+ parse_result.qty = parseInt(codeChars.join(''));
539+ codeNumbers = [];
540+ codeChars = [];
541+ console.log('QTY token: qty:'+parse_result.qty);
542+ } else if (token === KC_AMT) {
543+ parse_result.price = parseFloat(codeChars.join('')).toFixed(2);
544+ parse_result.priceOverride = true;
545+ codeNumbers = [];
546+ codeChars = [];
547+ console.log('AMT token: price:'+parse_result.price);
548+ } else if (token === KC_DISC) {
549+ parse_result.discount = parseFloat(codeChars.join(''));
550+ codeNumbers = [];
551+ codeChars = [];
552+ console.log('DISC token: discount:'+parse_result.discount);
553+ } else {
554+ codeNumbers.push(token - 48);
555+ codeChars.push(kc_lookup[token]);
556+ }
557+ } else if (token === KC_VOID) {
558+ /*
559+ * This is commented out for now. We don't want to interfere with
560+ * the 'Enter' keycode used by the barcode reader to signify a scan.
561+ */
562+ // Void the last line of the order only if there isn't another line in pregress.
563+// if (codeNumbers.length === 0) {
564+// parse_result.void_last_line = true;
565+// var res = self.copy_parse_result(parse_result);
566+// codeNumbers = [];
567+// codeChars = [];
568+// self.reset_parse_result(parse_result);
569+// console.log('VOID token:'+res.void_last_line);
570+// self.action_callback(res);
571+// }
572+ } else {
573+ // For now pressing Backspace or Delete just defaults to doing nothing.
574+ // In the future we might want it to display a popup or something.
575+ if (token === KC_CLR1 || token === KC_CLR2) {
576+ ;
577+ }
578+ codeNumbers = [];
579+ codeChars = [];
580+ self.reset_parse_result(parse_result);
581+ }
582+ });
583+ },
584+
585+ // stops catching keyboard events
586+ disconnect: function(){
587+ $('body').undelegate('', 'keyup')
588+ },
589+ });
590
591 }
592
593=== modified file 'point_of_sale/static/src/js/models.js'
594--- point_of_sale/static/src/js/models.js 2013-09-23 17:13:10 +0000
595+++ point_of_sale/static/src/js/models.js 2013-11-11 21:43:26 +0000
596@@ -23,11 +23,18 @@
597 this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time
598
599 this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
600+ this.keypad = new module.Keypad({'pos': this}); // used to simulate a cash register keypad
601 this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
602 this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy
603 this.db = new module.PosLS(); // a database used to store the products and categories
604+<<<<<<< TREE
605 this.db.clear('products','categories');
606 this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
607+=======
608+ this.db.clear('products','categories','customers');
609+ this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
610+
611+>>>>>>> MERGE-SOURCE
612
613 // default attributes values. If null, it will be loaded below.
614 this.set({
615@@ -44,6 +51,7 @@
616 'orders': new module.OrderCollection(),
617 //this is the product list as seen by the product list widgets, it will change based on the category filters
618 'products': new module.ProductCollection(),
619+ 'customers': new module.CustomerCollection(),
620 'cashRegisters': null,
621
622 'bank_statements': null,
623@@ -135,6 +143,10 @@
624 }).then(function(partners){
625 self.set('partner_list',partners);
626
627+ return self.fetch('res.partner', ['name','vat','email','phone','mobile'], [['customer', '=', true]]);
628+ }).then(function(customers){
629+ self.db.add_customers(customers);
630+
631 return self.fetch('account.tax', ['amount', 'price_include', 'type']);
632 }).then(function(taxes){
633 self.set('taxes', taxes);
634@@ -187,7 +199,11 @@
635
636 return self.fetch(
637 'product.product',
638+<<<<<<< TREE
639 ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code',
640+=======
641+ ['name', 'code', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13',
642+>>>>>>> MERGE-SOURCE
643 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
644 [['sale_ok','=',true],['available_in_pos','=',true]],
645 {pricelist: self.get('pricelist').id} // context for price
646@@ -450,6 +466,46 @@
647 }
648 return true;
649 },
650+
651+ keypad_enter_product: function(parsed_data){
652+ var self = this;
653+ var doMerge = true;
654+ var product = this.db.get_product_by_code(parsed_data.code);
655+ var selectedOrder = this.get('selectedOrder');
656+
657+ if (parsed_data.void_last_line) {
658+ line = selectedOrder.getLastOrderline();
659+ if (line) {
660+ line.set_quantity(Number.NaN);
661+ }
662+ return true;
663+ }
664+
665+ if (!product){
666+ return false;
667+ }
668+
669+ if (parsed_data.discount > 0) {
670+ doMerge = false;
671+ }
672+
673+ if (!parsed_data.priceOverride) {
674+ parsed_data.price = product.price;
675+ }
676+
677+// if (product.get('to_weight') && self.pos.iface_electronic_scale) {
678+// self.pos_widget.screen_selector.set_current_screen(self.scale_screen, {product: product});
679+// } else {
680+ selectedOrder.addProduct(new module.Product(product), { quantity: parsed_data.qty,
681+ price: parsed_data.price,
682+ merge: doMerge});
683+// }
684+ if (!doMerge){
685+ selectedOrder.selected_orderline.set_discount(parsed_data.discount);
686+ }
687+ self.get('products').reset(product);
688+ return true;
689+ },
690 });
691
692 module.CashRegister = Backbone.Model.extend({
693@@ -866,7 +922,7 @@
694 this.get('paymentLines').each(function(paymentline){
695 paymentlines.push(paymentline.export_for_printing());
696 });
697- var client = this.get('client');
698+ var client = this.pos.get('selectedOrder').get('client');
699 var cashier = this.pos.get('cashier') || this.pos.get('user');
700 var company = this.pos.get('company');
701 var shop = this.pos.get('shop');
702@@ -883,7 +939,7 @@
703 total_discount: this.getDiscountTotal(),
704 change: this.getChange(),
705 name : this.getName(),
706- client: client ? client.name : null ,
707+ client: client ? client : null ,
708 invoice_id: null, //TODO
709 cashier: cashier ? cashier.name : null,
710 date: {
711@@ -919,6 +975,7 @@
712 (this.get('paymentLines')).each(_.bind( function(item) {
713 return paymentLines.push([0, 0, item.export_as_JSON()]);
714 }, this));
715+ var client = this.pos.get('selectedOrder').get('client');
716 return {
717 name: this.getName(),
718 amount_paid: this.getPaidTotal(),
719@@ -928,7 +985,11 @@
720 lines: orderLines,
721 statement_ids: paymentLines,
722 pos_session_id: this.pos.get('pos_session').id,
723+<<<<<<< TREE
724 partner_id: this.get_client() ? this.get_client().id : false,
725+=======
726+ partner_id: client ? client.id : undefined,
727+>>>>>>> MERGE-SOURCE
728 user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
729 uid: this.uid,
730 };
731@@ -1019,4 +1080,12 @@
732 this.set({buffer:'0'});
733 },
734 });
735+
736+ module.Customer = Backbone.Model.extend({
737+ });
738+
739+ module.CustomerCollection = Backbone.Collection.extend({
740+ model: module.Customer,
741+ });
742+
743 }
744
745=== modified file 'point_of_sale/static/src/js/screens.js'
746--- point_of_sale/static/src/js/screens.js 2013-09-23 14:51:39 +0000
747+++ point_of_sale/static/src/js/screens.js 2013-11-11 21:43:26 +0000
748@@ -223,6 +223,20 @@
749 }
750 },
751
752+ // what happens when a product is entered by keypad emulator :
753+ // it will add the product to the order.
754+ keypad_product_action: function(data){
755+ var self = this;
756+ if(self.pos.keypad_enter_product(data)){
757+ self.pos.proxy.keypad_item_success(data);
758+ }else{
759+ self.pos.proxy.keypad_item_error_unrecognized(data);
760+ if(self.product_error_popup && self.pos_widget.screen_selector.get_user_mode() === 'cashier'){
761+ self.pos_widget.screen_selector.show_popup(self.product_error_popup);
762+ }
763+ }
764+ },
765+
766 // shows an action bar on the screen. The actionbar is automatically shown when you add a button
767 // with add_action_button()
768 show_action_bar: function(){
769@@ -283,9 +297,15 @@
770 }else{
771 this.pos_widget.client_button.hide();
772 }
773+<<<<<<< TREE
774 if(this.cashier_mode){
775+=======
776+ if(cashier_mode){
777+ this.pos_widget.select_customer_button.show();
778+>>>>>>> MERGE-SOURCE
779 this.pos_widget.close_button.show();
780 }else{
781+ this.pos_widget.select_customer_button.hide();
782 this.pos_widget.close_button.hide();
783 }
784
785@@ -297,11 +317,16 @@
786 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
787 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
788 });
789+
790+ this.pos.keypad.set_action_callback(function(data){ self.keypad_product_action(data); });
791 },
792
793 // this method is called when the screen is closed to make place for a new screen. this is a good place
794 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
795 close: function(){
796+ if(this.pos.keypad){
797+ this.pos.keypad.reset_action_callback();
798+ }
799 if(this.pos.barcode_reader){
800 this.pos.barcode_reader.reset_action_callbacks();
801 }
802@@ -401,6 +426,7 @@
803 this._super();
804 this.pos.proxy.help_needed();
805 this.pos.proxy.scan_item_error_unrecognized();
806+ this.pos.proxy.keypad_item_error_unrecognized();
807
808 this.pos.barcode_reader.save_callbacks();
809 this.pos.barcode_reader.reset_action_callbacks();
810@@ -411,6 +437,8 @@
811 self.pos_widget.screen_selector.set_user_mode('cashier');
812 },
813 });
814+ this.pos.keypad.save_callback();
815+ this.pos.keypad.reset_action_callback();
816 this.$('.footer .button').off('click').click(function(){
817 self.pos_widget.screen_selector.close_popup();
818 });
819@@ -418,6 +446,7 @@
820 close:function(){
821 this._super();
822 this.pos.proxy.help_canceled();
823+ this.pos.keypad.restore_callback();
824 this.pos.barcode_reader.restore_callbacks();
825 },
826 });
827@@ -784,10 +813,12 @@
828 this.user = this.pos.get('user');
829 this.company = this.pos.get('company');
830 this.shop_obj = this.pos.get('shop');
831+ this.client = null;
832 },
833 renderElement: function() {
834 this._super();
835 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
836+ this.pos.get('selectedOrder').bind('change:client', this.change_client, this);
837 this.change_selected_order();
838 },
839 show: function(){
840@@ -850,6 +881,10 @@
841 this.currentPaymentLines.bind('all', this.refresh, this);
842 this.refresh();
843 },
844+ change_client: function() {
845+ this.client = this.pos.get('selectedOrder').get('client');
846+ this.refresh();
847+ },
848 refresh: function() {
849 this.currentOrder = this.pos.get('selectedOrder');
850 $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));
851@@ -1062,4 +1097,82 @@
852 this.currentPaymentLines.last().set_amount(val);
853 },
854 });
855+<<<<<<< TREE
856+=======
857+
858+ module.SelectCustomerPopupWidget = module.PopUpWidget.extend({
859+ template:'SelectCustomerPopupWidget',
860+
861+ start: function(){
862+ this._super();
863+ var self = this;
864+ this.customer_list_widget = new module.CustomerListWidget(this,{
865+ click_customer_action: function(customer){
866+ this.pos.get('selectedOrder').set_client(customer);
867+ this.pos_widget.customername.refresh();
868+ this.pos_widget.screen_selector.set_current_screen('products');
869+ },
870+ });
871+ },
872+
873+ show: function(){
874+ this._super();
875+ var self = this;
876+ this.renderElement();
877+
878+ this.customer_list_widget.replace($('.placeholder-CustomerListWidget'));
879+
880+ this.$('.button.cancel').off('click').click(function(){
881+ self.pos_widget.screen_selector.set_current_screen('products');
882+ });
883+
884+ this.customer_search();
885+ },
886+
887+ // Customer search filter
888+ customer_search: function(){
889+ var self = this;
890+
891+ // find all products belonging to the current category
892+ var customers = this.pos.db.get_all_customers();
893+ self.pos.get('customers').reset(customers);
894+
895+ // filter customers according to the search string
896+ this.$('.customer-searchbox input').keyup(function(event){
897+ query = $(this).val().toLowerCase();
898+ if(query){
899+ var customers = self.pos.db.search_customers(query);
900+ self.pos.get('customers').reset(customers);
901+ self.$('.customer-search-clear').fadeIn();
902+ if(event.keyCode == 13){
903+ var c = null;
904+ if(customers.length == 1){
905+ c = self.pos.get('customers').get(customers[0]);
906+ }
907+ if(c !== null){
908+ self.pos_widget.select_customer_popup.customer_list_widget.click_customer_action(c);
909+ self.$('.customer-search-clear').trigger('click');
910+ }
911+ }
912+ }else{
913+ var customers = self.pos.db.get_all_customers();
914+ self.pos.get('customers').reset(customers);
915+ self.$('.customer-search-clear').fadeOut();
916+ }
917+ });
918+
919+ this.$('.customer-searchbox input').click(function(){}); //Why ???
920+
921+ //reset the search when clicking on reset
922+ this.$('.customer-search-clear').click(function(){
923+ var customers = self.pos.db.get_all_customers();
924+ self.pos.get('customers').reset(customers);
925+ self.$('.customer-searchbox input').val('').focus();
926+ self.$('.customer-search-clear').fadeOut();
927+ });
928+ },
929+
930+ });
931+
932+>>>>>>> MERGE-SOURCE
933 }
934
935=== modified file 'point_of_sale/static/src/js/widgets.js'
936--- point_of_sale/static/src/js/widgets.js 2013-10-09 13:29:44 +0000
937+++ point_of_sale/static/src/js/widgets.js 2013-11-11 21:43:26 +0000
938@@ -561,10 +561,15 @@
939 self.pos.get('products').reset(products);
940
941 // filter the products according to the search string
942+<<<<<<< TREE
943 this.$('.searchbox input').keyup(function(event){
944 console.log('event',event);
945+=======
946+ this.$('.searchbox input').keyup(function(event){
947+>>>>>>> MERGE-SOURCE
948 query = $(this).val().toLowerCase();
949 if(query){
950+<<<<<<< TREE
951 if(event.which === 13){
952 if( self.pos.get('products').size() === 1 ){
953 self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0));
954@@ -575,6 +580,30 @@
955 self.pos.get('products').reset(products);
956 self.$('.search-clear').fadeIn();
957 }
958+=======
959+ var products = self.pos.db.search_product_in_category(self.category.id, query);
960+ self.pos.get('products').reset(products);
961+ self.$('.search-clear').fadeIn();
962+ if(event.keyCode == 13){
963+ var p = null;
964+ if(products.length > 1){
965+ i = 0;
966+ while(i < products.length){
967+ if (products[i].code == query){
968+ p = self.pos.get('products').get(products[i]);
969+ break;
970+ }
971+ i++;
972+ }
973+ }else if(products.length == 1){
974+ p = self.pos.get('products').get(products[0]);
975+ }
976+ if(p !== null){
977+ self.pos_widget.product_screen.product_list_widget.click_product_action(p);
978+ self.$('.search-clear').trigger('click');
979+ }
980+ }
981+>>>>>>> MERGE-SOURCE
982 }else{
983 var products = self.pos.db.get_product_by_category(self.category.id);
984 self.pos.get('products').reset(products);
985@@ -666,6 +695,27 @@
986 },
987 });
988
989+ module.CustomernameWidget = module.PosBaseWidget.extend({
990+ template: 'CustomernameWidget',
991+ init: function(parent, options){
992+ var options = options || {};
993+ this._super(parent,options);
994+ this.pos.bind('change:selectedOrder', this.renderElement, this);
995+ },
996+ refresh: function(){
997+ this.renderElement();
998+ },
999+ get_name: function(){
1000+ var user;
1001+ customer = this.pos.get('selectedOrder').get_client();
1002+ if(customer){
1003+ return customer.name;
1004+ }else{
1005+ return "";
1006+ }
1007+ },
1008+ });
1009+
1010 module.HeaderButtonWidget = module.PosBaseWidget.extend({
1011 template: 'HeaderButtonWidget',
1012 init: function(parent, options){
1013@@ -874,6 +924,8 @@
1014
1015
1016 self.pos.barcode_reader.connect();
1017+
1018+ self.pos.keypad.connect();
1019
1020 instance.webclient.set_content_full_screen(true);
1021
1022@@ -948,6 +1000,9 @@
1023
1024 this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
1025 this.error_negative_price_popup.appendTo($('.point-of-sale'));
1026+
1027+ this.select_customer_popup = new module.SelectCustomerPopupWidget(this, {});
1028+ this.select_customer_popup.appendTo($('.point-of-sale'));
1029
1030 this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
1031 this.error_no_client_popup.appendTo($('.point-of-sale'));
1032@@ -995,6 +1050,15 @@
1033 });
1034 this.client_button.appendTo(this.$('#rightheader'));
1035
1036+ this.select_customer_button = new module.HeaderButtonWidget(this,{
1037+ label:'Select Customer',
1038+ action: function(){ self.screen_selector.show_popup('select-customer'); },
1039+ });
1040+ this.select_customer_button.appendTo(this.$('#rightheader'));
1041+
1042+ this.customername = new module.CustomernameWidget(this,{});
1043+ this.customername.appendTo(this.$('#rightheader'));
1044+
1045
1046 // -------- Screen Selector ---------
1047
1048@@ -1016,8 +1080,12 @@
1049 'error-session': this.error_session_popup,
1050 'error-negative-price': this.error_negative_price_popup,
1051 'choose-receipt': this.choose_receipt_popup,
1052+<<<<<<< TREE
1053 'error-no-client': this.error_no_client_popup,
1054 'error-invoice-transfer': this.error_invoice_transfer_popup,
1055+=======
1056+ 'select-customer': this.select_customer_popup,
1057+>>>>>>> MERGE-SOURCE
1058 },
1059 default_client_screen: 'welcome',
1060 default_cashier_screen: 'products',
1061@@ -1097,6 +1165,7 @@
1062 },
1063 close: function() {
1064 var self = this;
1065+<<<<<<< TREE
1066
1067 function close(){
1068 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1069@@ -1120,6 +1189,19 @@
1070 }else{
1071 return close();
1072 }
1073+=======
1074+ this.pos.barcode_reader.disconnect();
1075+ this.pos.keypad.disconnect();
1076+ return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1077+ _.bind(function(res) {
1078+ return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1079+ var action = result;
1080+ action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1081+ //self.destroy();
1082+ this.do_action(action);
1083+ }, this));
1084+ }, this));
1085+>>>>>>> MERGE-SOURCE
1086 },
1087 destroy: function() {
1088 this.pos.destroy();
1089@@ -1127,4 +1209,60 @@
1090 this._super();
1091 }
1092 });
1093+
1094+ module.CustomerWidget = module.PosBaseWidget.extend({
1095+ template: 'CustomerWidget',
1096+ init: function(parent, options) {
1097+ this._super(parent,options);
1098+ this.model = options.model;
1099+ this.click_customer_action = options.click_customer_action;
1100+ },
1101+ renderElement: function() {
1102+ this._super();
1103+ var self = this;
1104+ $("a", this.$el).click(function(e){
1105+ if(self.click_customer_action){
1106+ self.click_customer_action(self.model.toJSON());
1107+ }
1108+ });
1109+ },
1110+ });
1111+
1112+ module.CustomerListWidget = module.ScreenWidget.extend({
1113+ template:'CustomerListWidget',
1114+ init: function(parent, options) {
1115+ var self = this;
1116+ this._super(parent,options);
1117+ this.model = options.model;
1118+ this.customer_list = [];
1119+ this.next_screen = options.next_screen || false;
1120+ this.click_customer_action = options.click_customer_action;
1121+
1122+ var customers = self.pos.db.get_all_customers();
1123+ self.pos.get('customers').reset(customers);
1124+ this.pos.get('customers').bind('reset', function(){
1125+ self.renderElement();
1126+ });
1127+ },
1128+ renderElement: function() {
1129+ var self = this;
1130+ this._super();
1131+ this.customer_list = [];
1132+
1133+ this.pos.get('customers')
1134+ .chain()
1135+ .map(function(customer) {
1136+ var customer = new module.CustomerWidget(self, {
1137+ model: customer,
1138+ next_screen: 'products',
1139+ click_customer_action: self.click_customer_action,
1140+ })
1141+ self.customer_list.push(customer);
1142+ return customer;
1143+ })
1144+ .invoke('appendTo', this.$('.customer-list'));
1145+
1146+ },
1147+ });
1148+
1149 }
1150
1151=== modified file 'point_of_sale/static/src/xml/pos.xml'
1152--- point_of_sale/static/src/xml/pos.xml 2013-09-23 14:51:39 +0000
1153+++ point_of_sale/static/src/xml/pos.xml 2013-11-11 21:43:26 +0000
1154@@ -404,7 +404,14 @@
1155 <a href="#">
1156 <div class="product-img">
1157 <img src='' /> <!-- the product thumbnail -->
1158+<<<<<<< TREE
1159 <t t-if="!product.get('to_weight')">
1160+=======
1161+ <span class="code-tag">
1162+ <t t-esc="widget.model.get('code')"/>
1163+ </span>
1164+ <t t-if="!widget.model.get('to_weight')">
1165+>>>>>>> MERGE-SOURCE
1166 <span class="price-tag">
1167 <t t-esc="widget.format_currency(product.get('price'))"/>
1168 </span>
1169@@ -582,6 +589,12 @@
1170 </span>
1171 </t>
1172
1173+ <t t-name="CustomernameWidget">
1174+ <span class="customername">
1175+ <t t-esc="widget.get_name()" />
1176+ </span>
1177+ </t>
1178+
1179 <t t-name="PosTicket">
1180 <div class="pos-sale-ticket">
1181
1182@@ -589,9 +602,18 @@
1183 Date.CultureInfo.formatPatterns.longTime)"/> <t t-esc="widget.currentOrder.attributes.name"/></div>
1184 <br />
1185 <t t-esc="widget.company.name"/><br />
1186+ <t t-if="widget.company.vat">
1187+ TIN: <t t-esc="widget.company.vat"/><br />
1188+ </t>
1189 Phone: <t t-esc="widget.company.phone || ''"/><br />
1190 User: <t t-esc="widget.user.name"/><br />
1191 Shop: <t t-esc="widget.shop_obj.name"/><br />
1192+ <t t-if="widget.client">
1193+ Customer: <t t-esc="widget.client.name"/><br />
1194+ <t t-if="widget.client.vat">
1195+ Customer TIN: <t t-esc="widget.client.vat"/><br />
1196+ </t>
1197+ </t>
1198 <br />
1199 <table>
1200 <colgroup>
1201@@ -781,4 +803,66 @@
1202 </div>
1203 </t>
1204
1205+ <t t-name="CustomerListWidget">
1206+ <div class='customer-list-container'>
1207+ <div class="customer-list-scroller">
1208+ <ol id="customers-screen-ol" class="customer-list">
1209+ </ol>
1210+ </div>
1211+ </div>
1212+ </t>
1213+
1214+ <t t-name="SelectCustomerPopupWidget">
1215+ <div class="modal-dialog">
1216+ <div class="popup-selection">
1217+ <div class="customer-title">
1218+ Customer Selection
1219+ </div>
1220+
1221+ <div class="customer-searchbox">
1222+ <input placeholder="Search Customers" />
1223+ <img class="customer-search-clear" src="/point_of_sale/static/src/img/search_reset.gif" />
1224+ </div>
1225+
1226+ <div class="content-container">
1227+ <span class="placeholder-CustomerListWidget" />
1228+ </div>
1229+
1230+ <div id="customer-cancel" class = "button cancel">
1231+ Cancel
1232+ </div>
1233+ </div>
1234+ </div>
1235+ </t>
1236+
1237+ <t t-name="CustomerWidget">
1238+ <li class='customer'>
1239+ <a href="#">
1240+ <span class="customer-field customer-name">
1241+ <t t-esc="widget.model.get('name')"/>
1242+ </span>
1243+ <span class="customer-field">
1244+ <t t-if="widget.model.get('vat')">
1245+ <t t-esc="widget.model.get('vat')"/>
1246+ </t>
1247+ </span>
1248+ <span class="customer-field">
1249+ <t t-if="widget.model.get('email')">
1250+ <t t-esc="widget.model.get('email')"/>
1251+ </t>
1252+ </span>
1253+ <span class="customer-field customer-phone">
1254+ <t t-if="widget.model.get('phone')">
1255+ <t t-esc="widget.model.get('phone')"/>
1256+ </t>
1257+ </span>
1258+ <span class="customer-field customer-phone">
1259+ <t t-if="widget.model.get('mobile')">
1260+ <t t-esc="widget.model.get('mobile')"/>
1261+ </t>
1262+ </span>
1263+ </a>
1264+ </li>
1265+ </t>
1266+
1267 </templates>

Subscribers

People subscribed via source and target branches

to all changes: