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

Proposed by Antony Lesuisse (OpenERP)
Status: Needs review
Proposed branch: lp:~mmakonnen/openobject-addons/point_of_sale_enhanced-70
Merge into: lp:openobject-addons/7.0
Diff against target: 1181 lines (+810/-6) (has conflicts)
9 files modified
point_of_sale/controllers/main.py (+16/-0)
point_of_sale/point_of_sale.py (+3/-1)
point_of_sale/static/src/css/pos.css (+173/-0)
point_of_sale/static/src/js/db.js (+72/-0)
point_of_sale/static/src/js/devices.js (+179/-0)
point_of_sale/static/src/js/models.js (+65/-4)
point_of_sale/static/src/js/screens.js (+109/-0)
point_of_sale/static/src/js/widgets.js (+113/-1)
point_of_sale/static/src/xml/pos.xml (+80/-0)
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
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+151375@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Alex (agg-bcn) wrote :

Hi everyone,

How can this module be installed? I have a fresh installation of OpenERP 7 and I need to select a customer in POS. SO I found this module and I've tried to install it several times, but I can't.
I can't find it in Modules list (I must be doing something wrong). Somewhere I read I should replace the original module with this one, but I didn't work.
I guess it must be a very easy issue, but I'm unable to find it ou ... Sorry.

Thanks in advance,

Àlex

Revision history for this message
Eloi-Minorisa (eloigf) wrote :

> Hi everyone,
>
> How can this module be installed? I have a fresh installation of OpenERP 7 and
> I need to select a customer in POS. SO I found this module and I've tried to
> install it several times, but I can't.
> I can't find it in Modules list (I must be doing something wrong). Somewhere I
> read I should replace the original module with this one, but I didn't work.
> I guess it must be a very easy issue, but I'm unable to find it ou ... Sorry.
>
> Thanks in advance,
>
> Àlex

This module requires point_of_sale to be overriden.
But Thierry Godin has a better solution. Check out this module before.
http://thierry-godin.developpez.com/openerp/openerp-module-pos-enhanced-en/

Revision history for this message
Alex (agg-bcn) wrote :

Hi Minorisa,

thanks for your answer. I had already seen Thierry Godin solution, but I think it is in French (I'm not talking about the post, but the solution)

Thanks,

Àlex

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

Subscribers

People subscribed via source and target branches