Merge lp:~invitu/openobject-addons/trunk-point_of_sale_select_customer into lp:openobject-addons

Proposed by invitu
Status: Needs review
Proposed branch: lp:~invitu/openobject-addons/trunk-point_of_sale_select_customer
Merge into: lp:openobject-addons
Diff against target: 1055 lines (+575/-89)
9 files modified
plugin_outlook/i18n/et.po (+1/-1)
point_of_sale/point_of_sale.py (+16/-17)
point_of_sale/static/src/css/pos.css (+165/-0)
point_of_sale/static/src/js/db.js (+58/-0)
point_of_sale/static/src/js/models.js (+18/-4)
point_of_sale/static/src/js/screens.js (+149/-66)
point_of_sale/static/src/js/widgets.js (+89/-1)
point_of_sale/static/src/xml/pos.xml (+77/-0)
project_issue/project_issue_view.xml (+2/-0)
To merge this branch: bzr merge lp:~invitu/openobject-addons/trunk-point_of_sale_select_customer
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+195496@code.launchpad.net

Description of the change

[point_of_sale] allows customer selection in pos (inspired by Michael Telahun Makonnen's work)

To post a comment you must log in.
8977. By invitu

[IMP] allows customer selection in pos (inspired by Michael Telahun Makonnen's work)

Unmerged revisions

8977. By invitu

[IMP] allows customer selection in pos (inspired by Michael Telahun Makonnen's work)

8976. By invitu

[IMP] Convert to task button is back in project issue form view

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'plugin_outlook/i18n/et.po'
2--- plugin_outlook/i18n/et.po 2013-09-12 06:40:18 +0000
3+++ plugin_outlook/i18n/et.po 2013-11-17 01:16:28 +0000
4@@ -27,7 +27,7 @@
5 msgid "Download and install the plug-in"
6 msgstr ""
7
8-#. module: plugin_outlook
9+#. mdule: plugin_outlook
10 #: model:ir.model,name:plugin_outlook.model_outlook_installer
11 msgid "outlook.installer"
12 msgstr "outlook.installer"
13
14=== modified file 'point_of_sale/point_of_sale.py'
15--- point_of_sale/point_of_sale.py 2013-10-18 12:58:18 +0000
16+++ point_of_sale/point_of_sale.py 2013-11-17 01:16:28 +0000
17@@ -48,7 +48,7 @@
18 _columns = {
19 'name' : fields.char('Point of Sale Name', size=32, select=1,
20 required=True, help="An internal identification of the point of sale"),
21- 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel',
22+ 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel',
23 'pos_config_id', 'journal_id', 'Available Payment Methods',
24 domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",),
25 'warehouse_id' : fields.many2one('stock.warehouse', 'Warehouse',
26@@ -202,7 +202,7 @@
27 readonly=True,
28 states={'opening_control' : [('readonly', False)]}
29 ),
30- 'start_at' : fields.datetime('Opening Date', readonly=True),
31+ 'start_at' : fields.datetime('Opening Date', readonly=True),
32 'stop_at' : fields.datetime('Closing Date', readonly=True),
33
34 'state' : fields.selection(POS_SESSION_STATE, 'Status',
35@@ -221,10 +221,10 @@
36 type='many2one', relation='account.bank.statement',
37 string='Cash Register', store=True),
38
39- 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids',
40+ 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids',
41 type='one2many', relation='account.cashbox.line',
42 string='Opening Cash Control'),
43- 'details_ids' : fields.related('cash_register_id', 'details_ids',
44+ 'details_ids' : fields.related('cash_register_id', 'details_ids',
45 type='one2many', relation='account.cashbox.line',
46 string='Cash Control'),
47
48@@ -419,7 +419,7 @@
49 raise osv.except_osv( _('Error!'),
50 _("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff))
51 if (st.journal_id.type not in ['bank', 'cash']):
52- raise osv.except_osv(_('Error!'),
53+ raise osv.except_osv(_('Error!'),
54 _("The type of the journal for your payment method should be bank or cash "))
55 if st.difference and st.journal_id.cash_control == True:
56 if st.difference > 0.0:
57@@ -503,14 +503,13 @@
58 to_invoice = tmp_order['to_invoice']
59 order = tmp_order['data']
60
61-
62 order_id = self.create(cr, uid, {
63 'name': order['name'],
64 'user_id': order['user_id'] or False,
65 'session_id': order['pos_session_id'],
66 'lines': order['lines'],
67 'pos_reference':order['name'],
68- 'partner_id': order['partner_id'] or False
69+ 'partner_id': order['partner_id'] if 'partner_id' in order else False
70 }, context)
71 for payments in order['statement_ids']:
72 payment = payments[2]
73@@ -630,7 +629,7 @@
74 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
75 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
76
77- 'session_id' : fields.many2one('pos.session', 'Session',
78+ 'session_id' : fields.many2one('pos.session', 'Session',
79 #required=True,
80 select=1,
81 domain="[('state', '=', 'opened')]",
82@@ -659,7 +658,7 @@
83 return session_ids and session_ids[0] or False
84
85 def _default_pricelist(self, cr, uid, context=None):
86- session_ids = self._default_session(cr, uid, context)
87+ session_ids = self._default_session(cr, uid, context)
88 if session_ids:
89 session_record = self.pool.get('pos.session').browse(cr, uid, session_ids, context=context)
90 return session_record.config_id.pricelist_id and session_record.config_id.pricelist_id.id or False
91@@ -668,7 +667,7 @@
92 _defaults = {
93 'user_id': lambda self, cr, uid, context: uid,
94 'state': 'draft',
95- 'name': '/',
96+ 'name': '/',
97 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
98 'nb_print': 0,
99 'session_id': _default_session,
100@@ -737,7 +736,7 @@
101 }, context=context)
102 if line.qty < 0:
103 location_id, output_id = output_id, location_id
104-
105+
106 picking_obj.signal_button_confirm(cr, uid, [picking_id])
107 picking_obj.force_assign(cr, uid, [picking_id], context)
108 return True
109@@ -813,7 +812,7 @@
110 """Create a copy of order for refund order"""
111 clone_list = []
112 line_obj = self.pool.get('pos.order.line')
113-
114+
115 for order in self.browse(cr, uid, ids, context=context):
116 current_session_ids = self.pool.get('pos.session').search(cr, uid, [
117 ('state', '!=', 'closed'),
118@@ -1030,8 +1029,8 @@
119 else:
120 grouped_data[key].append(values)
121
122- #because of the weird way the pos order is written, we need to make sure there is at least one line,
123- #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
124+ #because of the weird way the pos order is written, we need to make sure there is at least one line,
125+ #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
126 #are set inside the for loop)
127 #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack
128 assert order.lines, _('The POS order must have lines when calling this method')
129@@ -1292,7 +1291,7 @@
130 for obj in self.browse(cr, uid, ids, context=context):
131 result[obj.id] = tools.image_get_resized_images(obj.image)
132 return result
133-
134+
135 def _set_image(self, cr, uid, id, name, value, args, context=None):
136 return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
137
138@@ -1302,7 +1301,7 @@
139 'parent_id': fields.many2one('pos.category','Parent Category', select=True),
140 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
141 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
142-
143+
144 # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
145 # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
146 # In this case, the default image is set by the js code.
147@@ -1364,7 +1363,7 @@
148 _columns = {
149 'income_pdt': fields.boolean('Point of Sale Cash In', help="Check if, this is a product you can use to put cash into a statement for the point of sale backend."),
150 'expense_pdt': fields.boolean('Point of Sale Cash Out', help="Check if, this is a product you can use to take cash from a statement for the point of sale backend, example: money lost, transfer to bank, etc."),
151- 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'),
152+ 'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'),
153 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
154 help="These products belong to those categories that are used to group similar products and are specific to the Point of Sale."),
155 'to_weight' : fields.boolean('To Weight', help="Check if the product should be weighted (mainly used with self check-out interface)."),
156
157=== modified file 'point_of_sale/static/src/css/pos.css'
158--- point_of_sale/static/src/css/pos.css 2013-11-12 14:04:57 +0000
159+++ point_of_sale/static/src/css/pos.css 2013-11-17 01:16:28 +0000
160@@ -208,6 +208,15 @@
161 box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
162 }
163
164+.point-of-sale #rightheader .customername{
165+ float:right;
166+ color:#DDD;
167+ font-size:16px;
168+ margin-right:32px;
169+ margin-top:10px;
170+ font-style:italic;
171+}
172+
173 /* c) The session buttons */
174
175 .point-of-sale #rightheader .header-button{
176@@ -1379,6 +1388,162 @@
177 line-height:180px;
178 }
179
180+.point-of-sale .modal-dialog .popup-selection{
181+ position: absolute;
182+ left:30%;
183+ top:10%;
184+ width: 700px;
185+ height:70%;
186+ padding:10px;
187+ padding-top:20px;
188+ text-align:left;
189+ font-size:14px;
190+ font-weight:bold;
191+ background-color: #F0EEEE;
192+ border-radius: 4px;
193+ box-shadow: 0px -1px white, 0px 1px white, 0px 4px #949494, 0px 10px 20px rgba(0, 0, 0, 0.3);
194+ z-index:1200;
195+}
196+
197+.point-of-sale .popup-selection .button{
198+ float:right;
199+ width: 110px;
200+ height: 40px;
201+ line-height:40px;
202+ text-align:center;
203+ margin:3px;
204+ margin-top:10px;
205+ margin-right:50px;
206+
207+ font-size: 14px;
208+ font-weight: bold;
209+
210+ cursor: pointer;
211+
212+ border: 1px solid #cacaca;
213+ border-radius: 4px;
214+
215+ background: #e2e2e2;
216+ background: -webkit-linear-gradient(#f0f0f0, #e2e2e2);
217+ background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
218+ background: -ms-linear-gradient(#f0f0f0, #e2e2e2);
219+ background: linear-gradient(#f0f0f0, #e2e2e2);
220+ -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
221+ -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
222+ box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
223+}
224+.point-of-sale .popup-selection .button:hover {
225+ color: white;
226+ background: #7f82ac;
227+ border: 1px solid #7f82ac;
228+ background: -webkit-linear-gradient(#9d9fc5, #7f82ac);
229+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
230+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
231+ background: linear-gradient(#9d9fc5, #7f82ac);
232+
233+ -webkit-transition-property: background, border;
234+ -webkit-transition-duration: 0.2s;
235+ -webkit-transition-timing-function: ease-out;
236+}
237+
238+/* customer selection */
239+
240+#customer-cancel {
241+ position: absolute;
242+ left: 10px;
243+ bottom: 10px;
244+}
245+
246+.point-of-sale .customer-list-container {
247+ position: absolute;
248+ top:70px;
249+ left:5px;
250+ right:5px;
251+ bottom: 70px;
252+ text-align: left;
253+ overflow: auto;
254+}
255+
256+.point-of-sale .customer-list-scroller {
257+ -webkit-box-sizing: border-box;
258+ -moz-box-sizing: border-box;
259+ -ms-box-sizing: border-box;
260+ box-sizing: border-box;
261+ width:98%;
262+}
263+
264+.customer-list-scroller ol {
265+ list-style: none;
266+}
267+
268+.customer-list-scroller ol li { }
269+
270+.customer-list-scroller ol li a {
271+ display:block;
272+ text-decoration:none;
273+ color:#000000;
274+ background-color:#FFFFFF;
275+ line-height:20px;
276+ border-bottom-style:solid;
277+ border-bottom-width:1px;
278+ border-bottom-color:#CCCCCC;
279+ padding-left:10px;
280+ cursor:pointer;
281+}
282+
283+.customer-list-scroller ol li a:hover {
284+ color:#FFFFFF;
285+ background-image:url(/point_of_sale/static/src/img/hover.png);
286+ background-repeat:repeat-x;
287+}
288+
289+.point-of-sale .customer {
290+ width: 100%;
291+}
292+
293+.point-of-sale .customer a {
294+ width: 100%;
295+}
296+
297+.point-of-sale .customer .customer-field {
298+ width: 130px;
299+ padding: 5px;
300+ margin: 5px;
301+}
302+
303+.point-of-sale .customer .customer-name{
304+ width: 200px;
305+ padding: 5px;
306+ margin: 5px;
307+}
308+
309+.point-of-sale .customer .customer-phone {
310+ width: 50px;
311+ padding: 5px;
312+ margin: 5px;
313+}
314+
315+.point-of-sale .customer-searchbox {
316+ right: 2px;
317+}
318+.point-of-sale .customer-searchbox input {
319+ width: 130px;
320+ border-radius: 11px;
321+ border: 1px solid #cecbcb;
322+ padding: 3px 19px;
323+ margin: 6px;
324+ background: url("../img/search.png") no-repeat 5px;
325+ background-color: white;
326+}
327+.point-of-sale .customer-search-clear {
328+ postion: absolute;
329+ top: 11px;
330+ right: 600px;
331+ cursor: pointer;
332+ display: none;
333+}
334+
335+
336 /* ********* The ScrollBarWidget ********* */
337
338 .point-of-sale .scrollbar{
339
340=== added file 'point_of_sale/static/src/img/hover.png'
341Binary 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-17 01:16:28 +0000 differ
342=== modified file 'point_of_sale/static/src/js/db.js'
343--- point_of_sale/static/src/js/db.js 2013-09-23 14:51:39 +0000
344+++ point_of_sale/static/src/js/db.js 2013-11-17 01:16:28 +0000
345@@ -55,6 +55,7 @@
346 this.packagings_by_id = {};
347 this.packagings_by_product_id = {};
348 this.packagings_by_ean13 = {};
349+ this.customer_list_search_strings = '';
350 },
351 /* returns the category object from its id. If you pass a list of id as parameters, you get
352 * a list of category objects.
353@@ -216,6 +217,35 @@
354 }
355 }
356 },
357+ _customer_search_string: function(customer){
358+ var str = '' + customer.id + ':' + customer.name;
359+ if(customer.vat){
360+ str += '|' + customer.vat;
361+ }
362+ if(customer.email){
363+ str += '|' + customer.email;
364+ }
365+ if(customer.phone){
366+ str += '|' + customer.phone;
367+ }
368+ if(customer.mobile){
369+ str += '|' + customer.mobile;
370+ }
371+ return str + '\n';
372+ },
373+ add_customers: function(customers){
374+ var stored_customers = this.load('customers',{});
375+
376+ if(!customers instanceof Array){
377+ customers = [customers];
378+ }
379+ for(var i = 0, len = customers.length; i < len; i++){
380+ var c = customers[i];
381+ this.customer_list_search_strings += this._customer_search_string(c);
382+ stored_customers[c.id] = c;
383+ }
384+ this.save('customers',stored_customers);
385+ },
386 /* removes all the data from the database. TODO : being able to selectively remove data */
387 clear: function(stores){
388 for(var i = 0, len = arguments.length; i < len; i++){
389@@ -258,6 +288,17 @@
390 }
391 return list;
392 },
393+ get_customer_by_id: function(id){
394+ return this.load('customers',{})[id];
395+ },
396+ get_all_customers: function(){
397+ list = [];
398+ stored_customers = this.load('customers',{});
399+ for (var i in stored_customers) {
400+ list.push(stored_customers[i]);
401+ }
402+ return list;
403+ },
404 /* returns a list of products with :
405 * - a category that is or is a child of category_id,
406 * - a name, package or ean13 containing the query (case insensitive)
407@@ -276,6 +317,23 @@
408 }
409 return results;
410 },
411+ /* returns a list of customers with :
412+ * * - a name, TIN, email, or phone/mobile containing the query (case insensitive)
413+ * */
414+ search_customers: function(query){
415+ var re = RegExp("([0-9]+):.*?"+query,"gi");
416+ var results = [];
417+ for(var i = 0; i < this.limit; i++){
418+ r = re.exec(this.customer_list_search_strings);
419+ if(r){
420+ var id = Number(r[1]);
421+ results.push(this.get_customer_by_id(id));
422+ }else{
423+ break;
424+ }
425+ }
426+ return results;
427+ },
428 add_order: function(order){
429 var order_id = order.uid;
430 var orders = this.load('orders',[]);
431
432=== modified file 'point_of_sale/static/src/js/models.js'
433--- point_of_sale/static/src/js/models.js 2013-09-23 17:13:10 +0000
434+++ point_of_sale/static/src/js/models.js 2013-11-17 01:16:28 +0000
435@@ -26,7 +26,7 @@
436 this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
437 this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy
438 this.db = new module.PosLS(); // a database used to store the products and categories
439- this.db.clear('products','categories');
440+ this.db.clear('products','categories','customers');
441 this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
442
443 // default attributes values. If null, it will be loaded below.
444@@ -44,6 +44,7 @@
445 'orders': new module.OrderCollection(),
446 //this is the product list as seen by the product list widgets, it will change based on the category filters
447 'products': new module.ProductCollection(),
448+ 'customers': new module.CustomerCollection(),
449 'cashRegisters': null,
450
451 'bank_statements': null,
452@@ -135,6 +136,10 @@
453 }).then(function(partners){
454 self.set('partner_list',partners);
455
456+ return self.fetch('res.partner', ['name','vat','email','phone','mobile'], [['customer', '=', true]]);
457+ }).then(function(customers){
458+ self.db.add_customers(customers);
459+
460 return self.fetch('account.tax', ['amount', 'price_include', 'type']);
461 }).then(function(taxes){
462 self.set('taxes', taxes);
463@@ -866,7 +871,7 @@
464 this.get('paymentLines').each(function(paymentline){
465 paymentlines.push(paymentline.export_for_printing());
466 });
467- var client = this.get('client');
468+ var client = this.pos.get('selectedOrder').get('client');
469 var cashier = this.pos.get('cashier') || this.pos.get('user');
470 var company = this.pos.get('company');
471 var shop = this.pos.get('shop');
472@@ -883,7 +888,7 @@
473 total_discount: this.getDiscountTotal(),
474 change: this.getChange(),
475 name : this.getName(),
476- client: client ? client.name : null ,
477+ client: client ? client : null ,
478 invoice_id: null, //TODO
479 cashier: cashier ? cashier.name : null,
480 date: {
481@@ -919,6 +924,7 @@
482 (this.get('paymentLines')).each(_.bind( function(item) {
483 return paymentLines.push([0, 0, item.export_as_JSON()]);
484 }, this));
485+ var client = this.pos.get('selectedOrder').get('client');
486 return {
487 name: this.getName(),
488 amount_paid: this.getPaidTotal(),
489@@ -928,7 +934,7 @@
490 lines: orderLines,
491 statement_ids: paymentLines,
492 pos_session_id: this.pos.get('pos_session').id,
493- partner_id: this.get_client() ? this.get_client().id : false,
494+ partner_id: client ? client.id : undefined,
495 user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
496 uid: this.uid,
497 };
498@@ -1019,4 +1025,12 @@
499 this.set({buffer:'0'});
500 },
501 });
502+
503+ module.Customer = Backbone.Model.extend({
504+ });
505+
506+ module.CustomerCollection = Backbone.Collection.extend({
507+ model: module.Customer,
508+ });
509+
510 }
511
512=== modified file 'point_of_sale/static/src/js/screens.js'
513--- point_of_sale/static/src/js/screens.js 2013-11-12 15:10:41 +0000
514+++ point_of_sale/static/src/js/screens.js 2013-11-17 01:16:28 +0000
515@@ -284,8 +284,10 @@
516 this.pos_widget.client_button.hide();
517 }
518 if(this.cashier_mode){
519+ this.pos_widget.select_customer_button.show();
520 this.pos_widget.close_button.show();
521 }else{
522+ this.pos_widget.select_customer_button.hide();
523 this.pos_widget.close_button.hide();
524 }
525
526@@ -784,10 +786,13 @@
527 this.user = this.pos.get('user');
528 this.company = this.pos.get('company');
529 this.shop_obj = this.pos.get('shop');
530+ this.client = null;
531+
532 },
533 renderElement: function() {
534 this._super();
535 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
536+ this.pos.get('selectedOrder').bind('change:client', this.change_client, this);
537 this.change_selected_order();
538 },
539 show: function(){
540@@ -850,6 +855,10 @@
541 this.currentPaymentLines.bind('all', this.refresh, this);
542 this.refresh();
543 },
544+ change_client: function() {
545+ this.client = this.pos.get('selectedOrder').get('client');
546+ this.refresh();
547+ },
548 refresh: function() {
549 this.currentOrder = this.pos.get('selectedOrder');
550 $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));
551@@ -874,53 +883,53 @@
552 var self = this;
553
554 if( this.pos.iface_cashdrawer && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ return pl.cashregister.get('journal').type === 'cash'; })){
555- this.pos.proxy.open_cashbox();
556+ this.pos.proxy.open_cashbox();
557 }
558
559 this.set_numpad_state(this.pos_widget.numpad.state);
560-
561- this.add_action_button({
562- label: _t('Back'),
563- icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
564- click: function(){
565- _.each(self.paymentlinewidgets, function(widget){
566- if( widget.payment_line.get_amount() === 0 ){
567- widget.payment_line.destroy();
568- }
569- });
570- self.pos_widget.screen_selector.set_current_screen(self.back_screen);
571- },
572- });
573-
574- this.add_action_button({
575- label: _t('Validate'),
576- name: 'validation',
577- icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
578- click: function(){
579- self.validateCurrentOrder();
580- },
581- });
582-
583+
584+ this.add_action_button({
585+ label: _t('Back'),
586+ icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
587+ click: function(){
588+ _.each(self.paymentlinewidgets, function(widget){
589+ if( widget.payment_line.get_amount() === 0 ){
590+ widget.payment_line.destroy();
591+ }
592+ });
593+ self.pos_widget.screen_selector.set_current_screen(self.back_screen);
594+ },
595+ });
596+
597+ this.add_action_button({
598+ label: _t('Validate'),
599+ name: 'validation',
600+ icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
601+ click: function(){
602+ self.validateCurrentOrder();
603+ },
604+ });
605+
606 if(this.pos.iface_invoicing){
607 this.add_action_button({
608- label: 'Invoice',
609- name: 'invoice',
610- icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
611- click: function(){
612- self.validateCurrentOrder({invoice: true});
613- },
614- });
615+ label: 'Invoice',
616+ name: 'invoice',
617+ icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
618+ click: function(){
619+ self.validateCurrentOrder({invoice: true});
620+ },
621+ });
622 }
623
624 if( this.pos.iface_cashdrawer ){
625 this.add_action_button({
626- label: _t('Cash'),
627- name: 'cashbox',
628- icon: '/point_of_sale/static/src/img/open-cashbox.png',
629- click: function(){
630- self.pos.proxy.open_cashbox();
631- },
632- });
633+ label: _t('Cash'),
634+ name: 'cashbox',
635+ icon: '/point_of_sale/static/src/img/open-cashbox.png',
636+ click: function(){
637+ self.pos.proxy.open_cashbox();
638+ },
639+ });
640 }
641
642 this.updatePaymentSummary();
643@@ -966,12 +975,12 @@
644
645 }else{
646 this.pos.push_order(currentOrder)
647- if(this.pos.iface_print_via_proxy){
648- this.pos.proxy.print_receipt(currentOrder.export_for_printing());
649- this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
650- }else{
651- this.pos_widget.screen_selector.set_current_screen(this.next_screen);
652- }
653+ if(this.pos.iface_print_via_proxy){
654+ this.pos.proxy.print_receipt(currentOrder.export_for_printing());
655+ this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
656+ }else{
657+ this.pos_widget.screen_selector.set_current_screen(this.next_screen);
658+ }
659 }
660 },
661 bindPaymentLineEvents: function() {
662@@ -1004,7 +1013,7 @@
663 addPaymentLine: function(newPaymentLine) {
664 var self = this;
665 var l = new module.PaymentlineWidget(this, {
666- payment_line: newPaymentLine,
667+ payment_line: newPaymentLine,
668 });
669 l.on('delete_payment_line', self, function(r) {
670 self.deleteLine(r);
671@@ -1023,14 +1032,14 @@
672 this.paymentlinewidgets[i].destroy();
673 }
674 this.paymentlinewidgets = [];
675-
676+
677 this.currentPaymentLines.each(_.bind( function(paymentLine) {
678 this.addPaymentLine(paymentLine);
679 }, this));
680 this.updatePaymentSummary();
681 },
682 deleteLine: function(lineWidget) {
683- this.currentPaymentLines.remove([lineWidget.payment_line]);
684+ this.currentPaymentLines.remove([lineWidget.payment_line]);
685 lineWidget.destroy();
686 },
687 updatePaymentSummary: function() {
688@@ -1047,30 +1056,104 @@
689 if(currentOrder.selected_orderline === undefined){
690 remaining = 1; // What is this ?
691 }
692-
693+
694 if(this.pos_widget.action_bar){
695 this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0.000001);
696 this.pos_widget.action_bar.set_button_disabled('invoice', remaining > 0.000001);
697 }
698 },
699 set_numpad_state: function(numpadState) {
700- if (this.numpadState) {
701- this.numpadState.unbind('set_value', this.set_value);
702- this.numpadState.unbind('change:mode', this.setNumpadMode);
703- }
704- this.numpadState = numpadState;
705- if (this.numpadState) {
706- this.numpadState.bind('set_value', this.set_value, this);
707- this.numpadState.bind('change:mode', this.setNumpadMode, this);
708- this.numpadState.reset();
709- this.setNumpadMode();
710- }
711- },
712- setNumpadMode: function() {
713- this.numpadState.set({mode: 'payment'});
714- },
715+ if (this.numpadState) {
716+ this.numpadState.unbind('set_value', this.set_value);
717+ this.numpadState.unbind('change:mode', this.setNumpadMode);
718+ }
719+ this.numpadState = numpadState;
720+ if (this.numpadState) {
721+ this.numpadState.bind('set_value', this.set_value, this);
722+ this.numpadState.bind('change:mode', this.setNumpadMode, this);
723+ this.numpadState.reset();
724+ this.setNumpadMode();
725+ }
726+ },
727+ setNumpadMode: function() {
728+ this.numpadState.set({mode: 'payment'});
729+ },
730 set_value: function(val) {
731- this.currentPaymentLines.last().set_amount(val);
732- },
733- });
734+ this.currentPaymentLines.last().set_amount(val);
735+ },
736+ });
737+ module.SelectCustomerPopupWidget = module.PopUpWidget.extend({
738+ template:'SelectCustomerPopupWidget',
739+
740+ start: function(){
741+ this._super();
742+ var self = this;
743+ this.customer_list_widget = new module.CustomerListWidget(this,{
744+ click_customer_action: function(customer){
745+ this.pos.get('selectedOrder').set_client(customer);
746+ this.pos_widget.customername.refresh();
747+ this.pos_widget.screen_selector.set_current_screen('products');
748+ },
749+ });
750+ },
751+
752+ show: function(){
753+ this._super();
754+ var self = this;
755+ this.renderElement();
756+
757+ this.customer_list_widget.replace($('.placeholder-CustomerListWidget'));
758+
759+ this.$('.button.cancel').off('click').click(function(){
760+ self.pos_widget.screen_selector.set_current_screen('products');
761+ });
762+
763+ this.customer_search();
764+ },
765+
766+ // Customer search filter
767+ customer_search: function(){
768+ var self = this;
769+
770+ // find all products belonging to the current category
771+ var customers = this.pos.db.get_all_customers();
772+ self.pos.get('customers').reset(customers);
773+
774+ // filter customers according to the search string
775+ this.$('.customer-searchbox input').keyup(function(event){
776+ query = $(this).val().toLowerCase();
777+ if(query){
778+ var customers = self.pos.db.search_customers(query);
779+ self.pos.get('customers').reset(customers);
780+ self.$('.customer-search-clear').fadeIn();
781+ if(event.keyCode == 13){
782+ var c = null;
783+ if(customers.length == 1){
784+ c = self.pos.get('customers').get(customers[0]);
785+ }
786+ if(c !== null){
787+ self.pos_widget.select_customer_popup.customer_list_widget.click_customer_action(c);
788+ self.$('.customer-search-clear').trigger('click');
789+ }
790+ }
791+ }else{
792+ var customers = self.pos.db.get_all_customers();
793+ self.pos.get('customers').reset(customers);
794+ self.$('.customer-search-clear').fadeOut();
795+ }
796+ });
797+
798+ this.$('.customer-searchbox input').click(function(){}); //Why ???
799+
800+ //reset the search when clicking on reset
801+ this.$('.customer-search-clear').click(function(){
802+ var customers = self.pos.db.get_all_customers();
803+ self.pos.get('customers').reset(customers);
804+ self.$('.customer-searchbox input').val('').focus();
805+ self.$('.customer-search-clear').fadeOut();
806+ });
807+ },
808+
809+ });
810+
811 }
812
813=== modified file 'point_of_sale/static/src/js/widgets.js'
814--- point_of_sale/static/src/js/widgets.js 2013-11-12 16:05:34 +0000
815+++ point_of_sale/static/src/js/widgets.js 2013-11-17 01:16:28 +0000
816@@ -672,6 +672,27 @@
817 },
818 });
819
820+ module.CustomernameWidget = module.PosBaseWidget.extend({
821+ template: 'CustomernameWidget',
822+ init: function(parent, options){
823+ var options = options || {};
824+ this._super(parent,options);
825+ this.pos.bind('change:selectedOrder', this.renderElement, this);
826+ },
827+ refresh: function(){
828+ this.renderElement();
829+ },
830+ get_name: function(){
831+ var user;
832+ customer = this.pos.get('selectedOrder').get_client();
833+ if(customer){
834+ return customer.name;
835+ }else{
836+ return "";
837+ }
838+ },
839+ });
840+
841 module.HeaderButtonWidget = module.PosBaseWidget.extend({
842 template: 'HeaderButtonWidget',
843 init: function(parent, options){
844@@ -961,6 +982,9 @@
845 this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
846 this.error_invoice_transfer_popup.appendTo($('.point-of-sale'));
847
848+ this.select_customer_popup = new module.SelectCustomerPopupWidget(this, {});
849+ this.select_customer_popup.appendTo($('.point-of-sale'));
850+
851 // -------- Misc ---------
852
853 this.notification = new module.SynchNotificationWidget(this,{});
854@@ -1001,7 +1025,15 @@
855 });
856 this.client_button.appendTo(this.$('#rightheader'));
857
858-
859+ this.select_customer_button = new module.HeaderButtonWidget(this,{
860+ label:'Select Customer',
861+ action: function(){ self.screen_selector.show_popup('select-customer'); },
862+ });
863+ this.select_customer_button.appendTo(this.$('#rightheader'));
864+
865+ this.customername = new module.CustomernameWidget(this,{});
866+ this.customername.appendTo(this.$('#rightheader'));
867+
868 // -------- Screen Selector ---------
869
870 this.screen_selector = new module.ScreenSelector({
871@@ -1024,6 +1056,7 @@
872 'choose-receipt': this.choose_receipt_popup,
873 'error-no-client': this.error_no_client_popup,
874 'error-invoice-transfer': this.error_invoice_transfer_popup,
875+ 'select-customer': this.select_customer_popup,
876 },
877 default_client_screen: 'welcome',
878 default_cashier_screen: 'products',
879@@ -1133,4 +1166,59 @@
880 this._super();
881 }
882 });
883+ module.CustomerWidget = module.PosBaseWidget.extend({
884+ template: 'CustomerWidget',
885+ init: function(parent, options) {
886+ this._super(parent,options);
887+ this.model = options.model;
888+ this.click_customer_action = options.click_customer_action;
889+ },
890+ renderElement: function() {
891+ this._super();
892+ var self = this;
893+ $("a", this.$el).click(function(e){
894+ if(self.click_customer_action){
895+ self.click_customer_action(self.model.toJSON());
896+ }
897+ });
898+ },
899+ });
900+
901+ module.CustomerListWidget = module.ScreenWidget.extend({
902+ template:'CustomerListWidget',
903+ init: function(parent, options) {
904+ var self = this;
905+ this._super(parent,options);
906+ this.model = options.model;
907+ this.customer_list = [];
908+ this.next_screen = options.next_screen || false;
909+ this.click_customer_action = options.click_customer_action;
910+
911+ var customers = self.pos.db.get_all_customers();
912+ self.pos.get('customers').reset(customers);
913+ this.pos.get('customers').bind('reset', function(){
914+ self.renderElement();
915+ });
916+ },
917+ renderElement: function() {
918+ var self = this;
919+ this._super();
920+ this.customer_list = [];
921+
922+ this.pos.get('customers')
923+ .chain()
924+ .map(function(customer) {
925+ var customer = new module.CustomerWidget(self, {
926+ model: customer,
927+ next_screen: 'products',
928+ click_customer_action: self.click_customer_action,
929+ })
930+ self.customer_list.push(customer);
931+ return customer;
932+ })
933+ .invoke('appendTo', this.$('.customer-list'));
934+
935+ },
936+ });
937+
938 }
939
940=== modified file 'point_of_sale/static/src/xml/pos.xml'
941--- point_of_sale/static/src/xml/pos.xml 2013-11-12 14:04:57 +0000
942+++ point_of_sale/static/src/xml/pos.xml 2013-11-17 01:16:28 +0000
943@@ -582,6 +582,12 @@
944 </span>
945 </t>
946
947+ <t t-name="CustomernameWidget">
948+ <span class="customername">
949+ <t t-esc="widget.get_name()" />
950+ </span>
951+ </t>
952+
953 <t t-name="PosTicket">
954 <div class="pos-sale-ticket">
955
956@@ -589,9 +595,18 @@
957 Date.CultureInfo.formatPatterns.longTime)"/> <t t-esc="widget.currentOrder.attributes.name"/></div>
958 <br />
959 <t t-esc="widget.company.name"/><br />
960+ <t t-if="widget.company.vat">
961+ TIN: <t t-esc="widget.company.vat"/><br />
962+ </t>
963 Phone: <t t-esc="widget.company.phone || ''"/><br />
964 User: <t t-esc="widget.user.name"/><br />
965 Shop: <t t-esc="widget.shop_obj.name"/><br />
966+ <t t-if="widget.client">
967+ Customer: <t t-esc="widget.client.name"/><br />
968+ <t t-if="widget.client.vat">
969+ Customer TIN: <t t-esc="widget.client.vat"/><br />
970+ </t>
971+ </t>
972 <br />
973 <table>
974 <colgroup>
975@@ -780,5 +795,67 @@
976 <p class="close_button">close</p>
977 </div>
978 </t>
979+ <t t-name="CustomerListWidget">
980+ <div class='customer-list-container'>
981+ <div class="customer-list-scroller">
982+ <ol id="customers-screen-ol" class="customer-list">
983+ </ol>
984+ </div>
985+ </div>
986+ </t>
987+
988+ <t t-name="SelectCustomerPopupWidget">
989+ <div class="modal-dialog">
990+ <div class="popup-selection">
991+ <div class="customer-title">
992+ Customer Selection
993+ </div>
994+
995+ <div class="customer-searchbox">
996+ <input placeholder="Search Customers" />
997+ <img class="customer-search-clear" src="/point_of_sale/static/src/img/search_reset.gif" />
998+ </div>
999+
1000+ <div class="content-container">
1001+ <span class="placeholder-CustomerListWidget" />
1002+ </div>
1003+
1004+ <div id="customer-cancel" class = "button cancel">
1005+ Cancel
1006+ </div>
1007+ </div>
1008+ </div>
1009+ </t>
1010+
1011+ <t t-name="CustomerWidget">
1012+ <li class='customer'>
1013+ <a href="#">
1014+ <span class="customer-field customer-name">
1015+ <t t-esc="widget.model.get('name')"/>
1016+ </span>
1017+ <span class="customer-field">
1018+ <t t-if="widget.model.get('vat')">
1019+ <t t-esc="widget.model.get('vat')"/>
1020+ </t>
1021+ </span>
1022+ <span class="customer-field">
1023+ <t t-if="widget.model.get('email')">
1024+ <t t-esc="widget.model.get('email')"/>
1025+ </t>
1026+ </span>
1027+ <span class="customer-field customer-phone">
1028+ <t t-if="widget.model.get('phone')">
1029+ <t t-esc="widget.model.get('phone')"/>
1030+ </t>
1031+ </span>
1032+ <span class="customer-field customer-phone">
1033+ <t t-if="widget.model.get('mobile')">
1034+ <t t-esc="widget.model.get('mobile')"/>
1035+ </t>
1036+ </span>
1037+ </a>
1038+ </li>
1039+ </t>
1040+
1041
1042 </templates>
1043
1044=== modified file 'project_issue/project_issue_view.xml'
1045--- project_issue/project_issue_view.xml 2013-10-17 11:35:03 +0000
1046+++ project_issue/project_issue_view.xml 2013-11-17 01:16:28 +0000
1047@@ -81,6 +81,8 @@
1048 <label for="task_id" groups="base.group_user"/>
1049 <div groups="base.group_user">
1050 <field name="task_id" on_change="onchange_task_id(task_id)" class="oe_inline" context="{'default_project_id':project_id}"/>
1051+ <button string="Convert To Task" name="convert_issue_task" icon="gtk-index" type="object"
1052+ attrs="{'invisible':[('task_id','!=',False)]}" />
1053 <field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/>
1054 </div>
1055 </group>

Subscribers

People subscribed via source and target branches

to all changes: