[FIX] point_of_sale: various fixes for the client mode on the pos
[odoo/odoo.git] / addons / point_of_sale / point_of_sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21 import pdb
22 import io
23 import addons
24
25 import time
26 from datetime import datetime
27 from dateutil.relativedelta import relativedelta
28 import logging
29 from PIL import Image
30
31 import netsvc
32 from osv import fields, osv
33 from tools.translate import _
34 from decimal import Decimal
35 import decimal_precision as dp
36
37 _logger = logging.getLogger(__name__)
38
39 class pos_config(osv.osv):
40     _name = 'pos.config'
41
42     POS_CONFIG_STATE = [
43         ('active', 'Active'),
44         ('inactive', 'Inactive'),
45         ('deprecated', 'Deprecated')
46     ]
47
48     _columns = {
49         'name' : fields.char('Point of Sale Name', size=32, select=1,
50              required=True, help="An internal identification of the point of sale"),
51         'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', 
52              'pos_config_id', 'journal_id', 'Available Payment Methods',
53              domain="[('journal_user', '=', True )]",),
54         'shop_id' : fields.many2one('sale.shop', 'Shop',
55              required=True),
56         'journal_id' : fields.many2one('account.journal', 'Sale Journal',
57              required=True, domain=[('type', '=', 'sale')],
58              help="Accounting journal used to post sales entries."),
59         'iface_self_checkout' : fields.boolean('Self Checkout Mode',
60              help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."),
61         'iface_websql' : fields.boolean('WebSQL (Faster but Chrome Only)',
62             help="If have more than 200 products, it's highly suggested to use WebSQL "\
63                 "to store the data in the browser, instead of localStore mechanism. "\
64                 "It's more efficient but works on the Chrome browser only."
65             ),
66         'iface_led' : fields.boolean('Help Notification'),
67         'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'),
68         'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'),
69         'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'),
70         'iface_barscan' : fields.boolean('BarScan Interface'), 
71         'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'),
72         'iface_print_via_proxy' : fields.boolean('Print via Proxy'),
73
74         'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True),
75         'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True,
76             help="This sequence is automatically created by OpenERP but you can change it "\
77                 "to customize the reference numbers of your orders."),
78         'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'),
79         'group_by' : fields.boolean('Group By', help="Check this if you want to group the Journal Items by Product while a Session"),
80     }
81
82     def name_get(self, cr, uid, ids, context=None):
83         result = []
84         states = {
85             'opening_control': _('Opening Control'),
86             'opened': _('In Progress'),
87             'closing_control': _('Closing Control'),
88             'closed': _('Closed & Posted'),
89         }
90         for record in self.browse(cr, uid, ids, context=context):
91             if (not record.session_ids) or (record.session_ids[0].state=='closed'):
92                 result.append((record.id, record.name+' ('+_('not used')+')'))
93                 continue
94             session = record.session_ids[0]
95             result.append((record.id, record.name + ' ('+session.user_id.name+', '+states[session.state]+')'))
96         return result
97
98
99     def _default_sale_journal(self, cr, uid, context=None):
100         res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1)
101         return res and res[0] or False
102
103     def _default_shop(self, cr, uid, context=None):
104         res = self.pool.get('sale.shop').search(cr, uid, [])
105         return res and res[0] or False
106
107     _defaults = {
108         'state' : POS_CONFIG_STATE[0][0],
109         'shop_id': _default_shop,
110         'journal_id': _default_sale_journal,
111         'group_by' : True,
112     }
113
114     def set_active(self, cr, uid, ids, context=None):
115         return self.write(cr, uid, ids, {'state' : 'active'}, context=context)
116
117     def set_inactive(self, cr, uid, ids, context=None):
118         return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context)
119
120     def set_deprecate(self, cr, uid, ids, context=None):
121         return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context)
122
123     def create(self, cr, uid, values, context=None):
124         proxy = self.pool.get('ir.sequence')
125         sequence_values = dict(
126             name='PoS %s' % values['name'],
127             padding=5,
128             prefix="%s/"  % values['name'],
129         )
130         sequence_id = proxy.create(cr, uid, sequence_values, context=context)
131         values['sequence_id'] = sequence_id
132         return super(pos_config, self).create(cr, uid, values, context=context)
133
134     def unlink(self, cr, uid, ids, context=None):
135         for obj in self.browse(cr, uid, ids, context=context):
136             if obj.sequence_id:
137                 obj.sequence_id.unlink()
138         return super(pos_config, self).unlink(cr, uid, ids, context=context)
139
140 pos_config()
141
142 class pos_session(osv.osv):
143     _name = 'pos.session'
144     _order = 'id desc'
145
146     POS_SESSION_STATE = [
147         ('opening_control', 'Opening Control'),  # Signal open
148         ('opened', 'In Progress'),                    # Signal closing
149         ('closing_control', 'Closing Control'),  # Signal close
150         ('closed', 'Closed & Posted'),
151     ]
152
153     def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None):
154         result = dict.fromkeys(ids, False)
155         for record in self.browse(cr, uid, ids, context=context):
156             for st in record.statement_ids:
157                 if st.journal_id.type == 'cash':
158                     result[record.id] = st.id
159                     break
160         return result
161
162     def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None):
163         result = {}
164
165         for record in self.browse(cr, uid, ids, context=context):
166             has_opening_control = False
167             has_closing_control = False
168
169             for journal in record.config_id.journal_ids:
170                 if journal.opening_control == True:
171                     has_opening_control = True
172                 if journal.closing_control == True:
173                     has_closing_control = True
174
175                 if has_opening_control and has_closing_control:
176                     break
177
178             values = {
179                 'has_opening_control': has_opening_control,
180                 'has_closing_control': has_closing_control,
181             }
182             result[record.id] = values
183
184         return result
185
186     _columns = {
187         'config_id' : fields.many2one('pos.config', 'Point of Sale',
188                                       help="The physical point of sale you will use.",
189                                       required=True,
190                                       select=1,
191                                       domain="[('state', '=', 'active')]",
192 #                                      readonly=True,
193 #                                      states={'draft' : [('readonly', False)]}
194                                      ),
195
196         'name' : fields.char('Session ID', size=32,
197                              required=True,
198 #                             readonly=True,
199 #                             states={'draft' : [('readonly', False)]}
200                             ),
201         'user_id' : fields.many2one('res.users', 'Responsible',
202                                     required=True,
203                                     select=1,
204 #                                    readonly=True,
205 #                                    states={'draft' : [('readonly', False)]}
206                                    ),
207         'start_at' : fields.datetime('Opening Date'), 
208         'stop_at' : fields.datetime('Closing Date'),
209
210         'state' : fields.selection(POS_SESSION_STATE, 'State',
211                 required=True, readonly=True,
212                 select=1),
213
214         'cash_register_id' : fields.function(_compute_cash_register_id, method=True, 
215                 type='many2one', relation='account.bank.statement',
216                 string='Cash Register', store=True),
217
218         'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', 
219                 type='one2many', relation='account.cashbox.line',
220                 string='Opening Cash Control'),
221         'details_ids' : fields.related('cash_register_id', 'details_ids', 
222                 type='one2many', relation='account.cashbox.line',
223                 string='Cash Control'),
224
225         'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real',
226                 type='float',
227                 digits_compute=dp.get_precision('Account'),
228                 string="Ending Balance",
229                 help="Computed using the cash control lines",
230                 readonly=True),
231         'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start',
232                 type='float',
233                 digits_compute=dp.get_precision('Account'),
234                 string="Starting Balance",
235                 help="Computed using the cash control at the opening.",
236                 readonly=True),
237         'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding',
238                 string='Total Cash Transaction',
239                 readonly=True),
240         'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end',
241                 type='float',
242                 digits_compute=dp.get_precision('Account'),
243                 string="Computed Balance",
244                 help="Computed with the initial cash control and the sum of all payments.",
245                 readonly=True),
246         'cash_register_difference' : fields.related('cash_register_id', 'difference',
247                 type='float',
248                 string='Difference',
249                 help="Difference between the counted cash control at the closing and the computed balance.",
250                 readonly=True),
251
252         'journal_ids' : fields.related('config_id', 'journal_ids',
253                                        type='many2many',
254                                        readonly=True,
255                                        relation='account.journal',
256                                        string='Available Payment Methods'),
257         'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'),
258
259         'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True),
260         'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'),
261         'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'),
262     }
263
264     _defaults = {
265         'name' : '/',
266         'user_id' : lambda obj, cr, uid, context: uid,
267         'state' : 'opening_control',
268     }
269
270     _sql_constraints = [
271         ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"),
272     ]
273
274     def _check_unicity(self, cr, uid, ids, context=None):
275         for session in self.browse(cr, uid, ids, context=None):
276             # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
277             domain = [
278                 ('state', '!=', 'closed'),
279                 ('user_id', '=', uid)
280             ]
281             count = self.search_count(cr, uid, domain, context=context)
282             if count>1:
283                 return False
284         return True
285
286     def _check_pos_config(self, cr, uid, ids, context=None):
287         for session in self.browse(cr, uid, ids, context=None):
288             domain = [
289                 ('state', '!=', 'closed'),
290                 ('config_id', '=', session.config_id.id)
291             ]
292             count = self.search_count(cr, uid, domain, context=context)
293             if count>1:
294                 return False
295         return True
296
297     _constraints = [
298         (_check_unicity, "You can not create two active sessions with the same responsible!", ['user_id', 'state']),
299         (_check_pos_config, "You can not create two active sessions related to the same point of sale!", ['config_id']),
300     ]
301
302     def create(self, cr, uid, values, context=None):
303         config_id = values.get('config_id', False) or False
304
305         pos_config = None
306         if config_id:
307             pos_config = self.pool.get('pos.config').browse(cr, uid, config_id, context=context)
308
309             bank_statement_ids = []
310             for journal in pos_config.journal_ids:
311                 bank_values = {
312                     'journal_id' : journal.id,
313                     'user_id' : uid,
314                 }
315                 statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
316                 bank_statement_ids.append(statement_id)
317
318             values.update({
319                 'name' : pos_config.sequence_id._next(),
320                 'statement_ids' : [(6, 0, bank_statement_ids)]
321             })
322
323         return super(pos_session, self).create(cr, uid, values, context=context)
324
325     def unlink(self, cr, uid, ids, context=None):
326         for obj in self.browse(cr, uid, ids, context=context):
327             for statement in obj.statement_ids:
328                 statement.unlink(context=context)
329         return True
330
331     def wkf_action_open(self, cr, uid, ids, context=None):
332         # si pas de date start_at, je balance une date, sinon on utilise celle de l'utilisateur
333         for record in self.browse(cr, uid, ids, context=context):
334             values = {}
335             if not record.start_at:
336                 values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
337             values['state'] = 'opened'
338             record.write(values, context=context)
339             for st in record.statement_ids:
340                 st.button_open(context=context)
341         return True
342
343     def wkf_action_opening_control(self, cr, uid, ids, context=None):
344         return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context)
345
346     def wkf_action_closing_control(self, cr, uid, ids, context=None):
347         for session in self.browse(cr, uid, ids, context=context):
348             for statement in session.statement_ids:
349                 if not statement.journal_id.closing_control:
350                     if statement.balance_end<>statement.balance_end_real:
351                         self.pool.get('account.bank.statement').write(cr, uid,
352                             [statement.id], {'balance_end_real': statement.balance_end})
353         return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
354
355     def wkf_action_close(self, cr, uid, ids, context=None):
356         # Close CashBox
357         bsl = self.pool.get('account.bank.statement.line')
358         for record in self.browse(cr, uid, ids, context=context):
359             for st in record.statement_ids:
360                 if abs(st.difference) > st.journal_id.amount_authorized_diff:
361                     # The pos manager can close statements with maximums.
362                     if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"):
363                         raise osv.except_osv( _('Error !'),
364                             _("Your ending balance is too different from the theorical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff))
365                 if st.difference:
366                     if st.difference > 0.0:
367                         name= _('Point of Sale Profit')
368                         account_id = st.journal_id.profit_account_id.id
369                     else:
370                         account_id = st.journal_id.loss_account_id.id
371                         name= _('Point of Sale Loss')
372                     if not account_id:
373                         raise osv.except_osv( _('Error !'),
374                         _("Please set your profit and loss accounts on your payment method '%s'.") % (st.journal_id.name,))
375                     bsl.create(cr, uid, {
376                         'statement_id': st.id,
377                         'amount': st.difference,
378                         'ref': record.name,
379                         'name': name,
380                         'account_id': account_id
381                     }, context=context)
382
383                 getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context)
384         self._confirm_orders(cr, uid, ids, context=context)
385         return self.write(cr, uid, ids, {'state' : 'closed'}, context=context)
386
387     def _confirm_orders(self, cr, uid, ids, context=None):
388         wf_service = netsvc.LocalService("workflow")
389
390         for session in self.browse(cr, uid, ids, context=context):
391             order_ids = [order.id for order in session.order_ids if order.state == 'paid']
392
393             move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context)
394
395             self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context)
396
397             for order in session.order_ids:
398                 if order.state != 'paid':
399                     raise osv.except_osv(
400                         _('Error !'),
401                         _("You can not confirm all orders of this session, because they have not the 'paid' status"))
402                 else:
403                     wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr)
404
405         return True
406
407     def open_frontend_cb(self, cr, uid, ids, context=None):
408         if not context:
409             context = {}
410
411         if not ids:
412             return {}
413
414         context.update({'session_id' : ids[0]})
415         return {
416             'type' : 'ir.actions.client',
417             'name' : 'Start Point Of Sale',
418             'tag' : 'pos.ui',
419             'context' : context,
420         }
421
422 pos_session()
423
424 class pos_order(osv.osv):
425     _name = "pos.order"
426     _description = "Point of Sale"
427     _order = "id desc"
428
429     def create_from_ui(self, cr, uid, orders, context=None):
430         #_logger.info("orders: %r", orders)
431         order_ids = []
432         for tmp_order in orders:
433             order = tmp_order['data']
434             # order :: {'name': 'Order 1329148448062', 'amount_paid': 9.42, 'lines': [[0, 0, {'discount': 0, 'price_unit': 1.46, 'product_id': 124, 'qty': 5}], [0, 0, {'discount': 0, 'price_unit': 0.53, 'product_id': 62, 'qty': 4}]], 'statement_ids': [[0, 0, {'journal_id': 7, 'amount': 9.42, 'name': '2012-02-13 15:54:12', 'account_id': 12, 'statement_id': 21}]], 'amount_tax': 0, 'amount_return': 0, 'amount_total': 9.42}
435             # get statements out of order because they will be generated with add_payment to ensure
436             # the module behavior is the same when using the front-end or the back-end
437             statement_ids = order.get('statement_ids', [])
438             order_id = self.create(cr, uid, order, context)
439             order_ids.append(order_id)
440             # call add_payment; refer to wizard/pos_payment for data structure
441             # add_payment launches the 'paid' signal to advance the workflow to the 'paid' state
442
443             data = {
444                 'journal': statement_ids[0][2]['journal_id'],
445                 'amount': order['amount_paid'],
446                 'payment_name': order['name'],
447                 'payment_date': statement_ids[0][2]['name'],
448             }
449             wf_service = netsvc.LocalService("workflow")
450             wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
451             wf_service.trg_write(uid, 'pos.order', order_id, cr)
452
453             #self.add_payment(cr, uid, order_id, data, context=context)
454         return order_ids
455
456     def unlink(self, cr, uid, ids, context=None):
457         for rec in self.browse(cr, uid, ids, context=context):
458             if rec.state not in ('draft','cancel'):
459                 raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.'))
460         return super(pos_order, self).unlink(cr, uid, ids, context=context)
461
462     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
463         if not part:
464             return {'value': {}}
465         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
466         return {'value': {'pricelist_id': pricelist}}
467
468     def _amount_all(self, cr, uid, ids, name, args, context=None):
469         tax_obj = self.pool.get('account.tax')
470         cur_obj = self.pool.get('res.currency')
471         res = {}
472         for order in self.browse(cr, uid, ids, context=context):
473             res[order.id] = {
474                 'amount_paid': 0.0,
475                 'amount_return':0.0,
476                 'amount_tax':0.0,
477             }
478             val1 = val2 = 0.0
479             cur = order.pricelist_id.currency_id
480             for payment in order.statement_ids:
481                 res[order.id]['amount_paid'] +=  payment.amount
482                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
483             for line in order.lines:
484                 val1 += line.price_subtotal_incl
485                 val2 += line.price_subtotal
486             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
487             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
488         return res
489
490     def copy(self, cr, uid, id, default=None, context=None):
491         if not default:
492             default = {}
493         d = {
494             'state': 'draft',
495             'invoice_id': False,
496             'account_move': False,
497             'picking_id': False,
498             'statement_ids': [],
499             'nb_print': 0,
500             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
501         }
502         d.update(default)
503         return super(pos_order, self).copy(cr, uid, id, d, context=context)
504
505     _columns = {
506         'name': fields.char('Order Ref', size=64, required=True, readonly=True),
507         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
508         'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True),
509         'date_order': fields.datetime('Order Date', readonly=True, select=True),
510         'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It could be a reliever, a student or an interim employee."),
511         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
512         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
513         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
514         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'),
515         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
516         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
517         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
518         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
519
520         'session_id' : fields.many2one('pos.session', 'Session', 
521                                         #required=True,
522                                         select=1,
523                                         domain="[('state', '=', 'opened')]",
524                                         states={'draft' : [('readonly', False)]},
525                                         readonly=True),
526
527         'state': fields.selection([('draft', 'New'),
528                                    ('cancel', 'Cancelled'),
529                                    ('paid', 'Paid'),
530                                    ('done', 'Posted'),
531                                    ('invoiced', 'Invoiced')],
532                                   'Status', readonly=True),
533
534         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
535         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
536         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
537         'note': fields.text('Internal Notes'),
538         'nb_print': fields.integer('Number of Print', readonly=True),
539
540         'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
541     }
542
543     def _default_session(self, cr, uid, context=None):
544         so = self.pool.get('pos.session')
545         session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
546         return session_ids and session_ids[0] or False
547
548     def _default_pricelist(self, cr, uid, context=None):
549         res = self.pool.get('sale.shop').search(cr, uid, [], context=context)
550         if res:
551             shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context)
552             return shop.pricelist_id and shop.pricelist_id.id or False
553         return False
554
555     _defaults = {
556         'user_id': lambda self, cr, uid, context: uid,
557         'state': 'draft',
558         'name': '/', 
559         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
560         'nb_print': 0,
561         'session_id': _default_session,
562         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
563         'pricelist_id': _default_pricelist,
564     }
565
566     def create(self, cr, uid, values, context=None):
567         values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
568         return super(pos_order, self).create(cr, uid, values, context=context)
569
570     def test_paid(self, cr, uid, ids, context=None):
571         """A Point of Sale is paid when the sum
572         @return: True
573         """
574         for order in self.browse(cr, uid, ids, context=context):
575             if order.lines and not order.amount_total:
576                 return True
577             if (not order.lines) or (not order.statement_ids) or \
578                 (abs(order.amount_total-order.amount_paid) > 0.00001):
579                 return False
580         return True
581
582     def create_picking(self, cr, uid, ids, context=None):
583         """Create a picking for each order and validate it."""
584         picking_obj = self.pool.get('stock.picking')
585         partner_obj = self.pool.get('res.partner')
586         move_obj = self.pool.get('stock.move')
587
588         for order in self.browse(cr, uid, ids, context=context):
589             if not order.state=='draft':
590                 continue
591             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
592             picking_id = picking_obj.create(cr, uid, {
593                 'origin': order.name,
594                 'partner_id': addr.get('delivery',False),
595                 'type': 'out',
596                 'company_id': order.company_id.id,
597                 'move_type': 'direct',
598                 'note': order.note or "",
599                 'invoice_state': 'none',
600                 'auto_picking': True,
601             }, context=context)
602             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
603             location_id = order.shop_id.warehouse_id.lot_stock_id.id
604             output_id = order.shop_id.warehouse_id.lot_output_id.id
605
606             for line in order.lines:
607                 if line.product_id and line.product_id.type == 'service':
608                     continue
609                 if line.qty < 0:
610                     location_id, output_id = output_id, location_id
611
612                 move_obj.create(cr, uid, {
613                     'name': line.name,
614                     'product_uom': line.product_id.uom_id.id,
615                     'product_uos': line.product_id.uom_id.id,
616                     'picking_id': picking_id,
617                     'product_id': line.product_id.id,
618                     'product_uos_qty': abs(line.qty),
619                     'product_qty': abs(line.qty),
620                     'tracking_id': False,
621                     'state': 'draft',
622                     'location_id': location_id,
623                     'location_dest_id': output_id,
624                 }, context=context)
625                 if line.qty < 0:
626                     location_id, output_id = output_id, location_id
627
628             wf_service = netsvc.LocalService("workflow")
629             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
630             picking_obj.force_assign(cr, uid, [picking_id], context)
631         return True
632
633     def cancel_order(self, cr, uid, ids, context=None):
634         """ Changes order state to cancel
635         @return: True
636         """
637         stock_picking_obj = self.pool.get('stock.picking')
638         for order in self.browse(cr, uid, ids, context=context):
639             wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr)
640             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
641                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
642         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
643         return True
644
645     def add_payment(self, cr, uid, order_id, data, context=None):
646         """Create a new payment for the order"""
647         if not context:
648             context = {}
649         statement_obj = self.pool.get('account.bank.statement')
650         statement_line_obj = self.pool.get('account.bank.statement.line')
651         prod_obj = self.pool.get('product.product')
652         property_obj = self.pool.get('ir.property')
653         curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
654         curr_company = curr_c.id
655         order = self.browse(cr, uid, order_id, context=context)
656         args = {
657             'amount': data['amount'],
658         }
659         if 'payment_date' in data:
660             args['date'] = data['payment_date']
661         args['name'] = order.name
662         if data.get('payment_name', False):
663             args['name'] = args['name'] + ': ' + data['payment_name']
664         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
665         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
666                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
667         args['partner_id'] = order.partner_id and order.partner_id.id or None
668
669         if not args['account_id']:
670             if not args['partner_id']:
671                 msg = _('There is no receivable account defined to make payment')
672             else:
673                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d)') % (order.partner_id.name, order.partner_id.id,)
674             raise osv.except_osv(_('Configuration Error !'), msg)
675
676         context.pop('pos_session_id', False)
677
678         try:
679             journal_id = long(data['journal'])
680         except Exception:
681             journal_id = False
682
683         statement_id = False
684         for statement in order.session_id.statement_ids:
685             if statement.journal_id.id == journal_id:
686                 statement_id = statement.id
687                 break
688
689         if not statement_id:
690             raise osv.except_osv(_('Error !'), _('You have to open at least one cashbox'))
691
692         args.update({
693             'statement_id' : statement_id,
694             'pos_statement_id' : order_id,
695             'journal_id' : journal_id,
696             'type' : 'customer',
697             'ref' : order.name,
698         })
699
700         statement_line_obj.create(cr, uid, args, context=context)
701
702         wf_service = netsvc.LocalService("workflow")
703         wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
704         wf_service.trg_write(uid, 'pos.order', order_id, cr)
705
706         return statement_id
707
708     def refund(self, cr, uid, ids, context=None):
709         """Create a copy of order  for refund order"""
710         clone_list = []
711         line_obj = self.pool.get('pos.order.line')
712         for order in self.browse(cr, uid, ids, context=context):
713             clone_id = self.copy(cr, uid, order.id, {
714                 'name': order.name + ' REFUND',
715             }, context=context)
716             clone_list.append(clone_id)
717
718         for clone in self.browse(cr, uid, clone_list, context=context):
719             for order_line in clone.lines:
720                 line_obj.write(cr, uid, [order_line.id], {
721                     'qty': -order_line.qty
722                 }, context=context)
723
724         new_order = ','.join(map(str,clone_list))
725         abs = {
726             #'domain': "[('id', 'in', ["+new_order+"])]",
727             'name': _('Return Products'),
728             'view_type': 'form',
729             'view_mode': 'form',
730             'res_model': 'pos.order',
731             'res_id':clone_list[0],
732             'view_id': False,
733             'context':context,
734             'type': 'ir.actions.act_window',
735             'nodestroy': True,
736             'target': 'current',
737         }
738         return abs
739
740     def action_invoice_state(self, cr, uid, ids, context=None):
741         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
742
743     def action_invoice(self, cr, uid, ids, context=None):
744         wf_service = netsvc.LocalService("workflow")
745         inv_ref = self.pool.get('account.invoice')
746         inv_line_ref = self.pool.get('account.invoice.line')
747         product_obj = self.pool.get('product.product')
748         inv_ids = []
749
750         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
751             if order.invoice_id:
752                 inv_ids.append(order.invoice_id.id)
753                 continue
754
755             if not order.partner_id:
756                 raise osv.except_osv(_('Error'), _('Please provide a partner for the sale.'))
757
758             acc = order.partner_id.property_account_receivable.id
759             inv = {
760                 'name': order.name,
761                 'origin': order.name,
762                 'account_id': acc,
763                 'journal_id': order.sale_journal.id or None,
764                 'type': 'out_invoice',
765                 'reference': order.name,
766                 'partner_id': order.partner_id.id,
767                 'comment': order.note or '',
768                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
769             }
770             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
771             if not inv.get('account_id', None):
772                 inv['account_id'] = acc
773             inv_id = inv_ref.create(cr, uid, inv, context=context)
774
775             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
776             inv_ids.append(inv_id)
777             for line in order.lines:
778                 inv_line = {
779                     'invoice_id': inv_id,
780                     'product_id': line.product_id.id,
781                     'quantity': line.qty,
782                 }
783                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
784                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
785                                                                line.product_id.id,
786                                                                line.product_id.uom_id.id,
787                                                                line.qty, partner_id = order.partner_id.id,
788                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
789                 if line.product_id.description_sale:
790                     inv_line['note'] = line.product_id.description_sale
791                 inv_line['price_unit'] = line.price_unit
792                 inv_line['discount'] = line.discount
793                 inv_line['name'] = inv_name
794                 inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
795                     and [(6, 0, inv_line['invoice_line_tax_id'])] or []
796                 inv_line_ref.create(cr, uid, inv_line, context=context)
797             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
798             wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
799
800         if not inv_ids: return {}
801
802         mod_obj = self.pool.get('ir.model.data')
803         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
804         res_id = res and res[1] or False
805         return {
806             'name': _('Customer Invoice'),
807             'view_type': 'form',
808             'view_mode': 'form',
809             'view_id': [res_id],
810             'res_model': 'account.invoice',
811             'context': "{'type':'out_invoice'}",
812             'type': 'ir.actions.act_window',
813             'nodestroy': True,
814             'target': 'current',
815             'res_id': inv_ids and inv_ids[0] or False,
816         }
817
818     def create_account_move(self, cr, uid, ids, context=None):
819         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
820
821     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
822         # Tricky, via the workflow, we only have one id in the ids variable
823         """Create a account move line of order grouped by products or not."""
824         account_move_obj = self.pool.get('account.move')
825         account_move_line_obj = self.pool.get('account.move.line')
826         account_period_obj = self.pool.get('account.period')
827         account_tax_obj = self.pool.get('account.tax')
828         user_proxy = self.pool.get('res.users')
829         property_obj = self.pool.get('ir.property')
830
831         period = account_period_obj.find(cr, uid, context=context)[0]
832
833         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
834
835         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
836             raise osv.except_osv(_('Error!'), _('The selected orders do not have the same session !'))
837
838         current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
839
840         grouped_data = {}
841         have_to_group_by = session and session.config_id.group_by or False
842
843         def compute_tax(amount, tax, line):
844             if amount > 0:
845                 tax_code_id = tax['base_code_id']
846                 tax_amount = line.price_subtotal * tax['base_sign']
847             else:
848                 tax_code_id = tax['ref_base_code_id']
849                 tax_amount = line.price_subtotal * tax['ref_base_sign']
850
851             return (tax_code_id, tax_amount,)
852
853         for order in self.browse(cr, uid, ids, context=context):
854             if order.account_move:
855                 continue
856             if order.state != 'paid':
857                 continue
858
859             user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
860
861             group_tax = {}
862             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id
863
864             order_account = order.partner_id and \
865                             order.partner_id.property_account_receivable and \
866                             order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id
867
868             if move_id is None:
869                 # Create an entry for the sale
870                 move_id = account_move_obj.create(cr, uid, {
871                     'ref' : order.name,
872                     'journal_id': order.sale_journal.id,
873                 }, context=context)
874
875             def insert_data(data_type, values):
876                 # if have_to_group_by:
877
878                 sale_journal_id = order.sale_journal.id
879
880                 # 'quantity': line.qty,
881                 # 'product_id': line.product_id.id,
882                 values.update({
883                     'date': order.date_order[:10],
884                     'ref': order.name,
885                     'journal_id' : sale_journal_id,
886                     'period_id' : period,
887                     'move_id' : move_id,
888                     'company_id': user_company and user_company.id or False,
889                 })
890
891                 if data_type == 'product':
892                     key = ('product', values['product_id'],)
893                 elif data_type == 'tax':
894                     key = ('tax', values['tax_code_id'],)
895                 elif data_type == 'counter_part':
896                     key = ('counter_part', values['partner_id'], values['account_id'])
897                 else:
898                     return
899
900                 grouped_data.setdefault(key, [])
901
902                 # if not have_to_group_by or (not grouped_data[key]):
903                 #     grouped_data[key].append(values)
904                 # else:
905                 #     pass
906
907                 if have_to_group_by:
908                     if not grouped_data[key]:
909                         grouped_data[key].append(values)
910                     else:
911                         current_value = grouped_data[key][0]
912                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
913                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
914                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
915                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
916                 else:
917                     grouped_data[key].append(values)
918
919             # Create an move for each order line
920
921             for line in order.lines:
922                 tax_amount = 0
923                 taxes = [t for t in line.product_id.taxes_id]
924                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
925
926                 for tax in computed_taxes:
927                     tax_amount += round(tax['amount'], 2)
928                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
929
930                     group_tax.setdefault(group_key, 0)
931                     group_tax[group_key] += round(tax['amount'], 2)
932
933                 amount = line.price_subtotal
934
935                 # Search for the income account
936                 if  line.product_id.property_account_income.id:
937                     income_account = line.product_id.property_account_income.id
938                 elif line.product_id.categ_id.property_account_income_categ.id:
939                     income_account = line.product_id.categ_id.property_account_income_categ.id
940                 else:
941                     raise osv.except_osv(_('Error !'), _('There is no income '\
942                         'account defined for this product: "%s" (id:%d)') \
943                         % (line.product_id.name, line.product_id.id, ))
944
945                 # Empty the tax list as long as there is no tax code:
946                 tax_code_id = False
947                 tax_amount = 0
948                 while computed_taxes:
949                     tax = computed_taxes.pop(0)
950                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
951
952                     # If there is one we stop
953                     if tax_code_id:
954                         break
955
956                 # Create a move for the line
957                 insert_data('product', {
958                     'name': line.product_id.name,
959                     'quantity': line.qty,
960                     'product_id': line.product_id.id,
961                     'account_id': income_account,
962                     'credit': ((amount>0) and amount) or 0.0,
963                     'debit': ((amount<0) and -amount) or 0.0,
964                     'tax_code_id': tax_code_id,
965                     'tax_amount': tax_amount,
966                     'partner_id': order.partner_id and order.partner_id.id or False
967                 })
968
969                 # For each remaining tax with a code, whe create a move line
970                 for tax in computed_taxes:
971                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
972                     if not tax_code_id:
973                         continue
974
975                     insert_data('tax', {
976                         'name': _('Tax'),
977                         'product_id':line.product_id.id,
978                         'quantity': line.qty,
979                         'account_id': income_account,
980                         'credit': 0.0,
981                         'debit': 0.0,
982                         'tax_code_id': tax_code_id,
983                         'tax_amount': tax_amount,
984                     })
985
986             # Create a move for each tax group
987             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
988
989             for key, tax_amount in group_tax.items():
990                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
991                 insert_data('tax', {
992                     'name': _('Tax') + ' ' + tax.name,
993                     'quantity': line.qty,
994                     'product_id': line.product_id.id,
995                     'account_id': key[account_pos],
996                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
997                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
998                     'tax_code_id': key[tax_code_pos],
999                     'tax_amount': tax_amount,
1000                 })
1001
1002             # counterpart
1003             insert_data('counter_part', {
1004                 'name': _("Trade Receivables"), #order.name,
1005                 'account_id': order_account,
1006                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1007                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1008                 'partner_id': order.partner_id and order.partner_id.id or False
1009             })
1010
1011             order.write({'state':'done', 'account_move': move_id})
1012
1013         for group_key, group_data in grouped_data.iteritems():
1014             for value in group_data:
1015                 account_move_line_obj.create(cr, uid, value, context=context)
1016
1017         return True
1018
1019     def action_payment(self, cr, uid, ids, context=None):
1020         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1021
1022     def action_paid(self, cr, uid, ids, context=None):
1023         self.create_picking(cr, uid, ids, context=context)
1024         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1025         return True
1026
1027     def action_cancel(self, cr, uid, ids, context=None):
1028         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1029         return True
1030
1031     def action_done(self, cr, uid, ids, context=None):
1032         self.create_account_move(cr, uid, ids, context=context)
1033         return True
1034
1035 pos_order()
1036
1037 class account_bank_statement(osv.osv):
1038     _inherit = 'account.bank.statement'
1039     _columns= {
1040         'user_id': fields.many2one('res.users', 'User', readonly=True),
1041     }
1042     _defaults = {
1043         'user_id': lambda self,cr,uid,c={}: uid
1044     }
1045 account_bank_statement()
1046
1047 class account_bank_statement_line(osv.osv):
1048     _inherit = 'account.bank.statement.line'
1049     _columns= {
1050         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1051     }
1052 account_bank_statement_line()
1053
1054 class pos_order_line(osv.osv):
1055     _name = "pos.order.line"
1056     _description = "Lines of Point of Sale"
1057     _rec_name = "product_id"
1058
1059     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1060         res = dict([(i, {}) for i in ids])
1061         account_tax_obj = self.pool.get('account.tax')
1062         cur_obj = self.pool.get('res.currency')
1063         for line in self.browse(cr, uid, ids, context=context):
1064             taxes = line.product_id.taxes_id
1065             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1066             taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
1067
1068             cur = line.order_id.pricelist_id.currency_id
1069             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1070             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1071         return res
1072
1073     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1074        context = context or {}
1075        if not product_id:
1076             return {}
1077        if not pricelist:
1078            raise osv.except_osv(_('No Pricelist !'),
1079                _('You have to select a pricelist in the sale form !\n' \
1080                'Please set one before choosing a product.'))
1081
1082        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1083                product_id, qty or 1.0, partner_id)[pricelist]
1084
1085        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1086        result['value']['price_unit'] = price
1087        return result
1088
1089     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1090         result = {}
1091         if not product:
1092             return result
1093         account_tax_obj = self.pool.get('account.tax')
1094         cur_obj = self.pool.get('res.currency')
1095
1096         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1097
1098         taxes = prod.taxes_id
1099         price = price_unit * (1 - (discount or 0.0) / 100.0)
1100         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1101
1102         result['price_subtotal'] = taxes['total']
1103         result['price_subtotal_incl'] = taxes['total_included']
1104         return {'value': result}
1105
1106     _columns = {
1107         'company_id': fields.many2one('res.company', 'Company', required=True),
1108         'name': fields.char('Line No', size=32, required=True),
1109         'notice': fields.char('Discount Notice', size=128),
1110         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1111         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1112         'qty': fields.float('Quantity', digits=(16, 2)),
1113         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1114         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1115         'discount': fields.float('Discount (%)', digits=(16, 2)),
1116         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1117         'create_date': fields.datetime('Creation Date', readonly=True),
1118     }
1119
1120     _defaults = {
1121         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1122         'qty': lambda *a: 1,
1123         'discount': lambda *a: 0.0,
1124         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1125     }
1126
1127     def copy_data(self, cr, uid, id, default=None, context=None):
1128         if not default:
1129             default = {}
1130         default.update({
1131             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1132         })
1133         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1134
1135 pos_order_line()
1136
1137 class pos_category(osv.osv):
1138     _name = 'pos.category'
1139     _description = "Point of Sale Category"
1140     _order = "sequence, name"
1141     def _check_recursion(self, cr, uid, ids, context=None):
1142         level = 100
1143         while len(ids):
1144             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1145             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1146             if not level:
1147                 return False
1148             level -= 1
1149         return True
1150
1151     _constraints = [
1152         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1153     ]
1154
1155     def name_get(self, cr, uid, ids, context=None):
1156         if not len(ids):
1157             return []
1158         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1159         res = []
1160         for record in reads:
1161             name = record['name']
1162             if record['parent_id']:
1163                 name = record['parent_id'][1]+' / '+name
1164             res.append((record['id'], name))
1165         return res
1166
1167     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1168         res = self.name_get(cr, uid, ids, context=context)
1169         return dict(res)
1170
1171     def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1172         result = {}
1173         for obj in self.browse(cr, uid, ids, context=context):
1174             if not obj.category_image:
1175                 result[obj.id] = False
1176                 continue
1177
1178             image_stream = io.BytesIO(obj.category_image.decode('base64'))
1179             img = Image.open(image_stream)
1180             img.thumbnail((120, 100), Image.ANTIALIAS)
1181             img_stream = StringIO.StringIO()
1182             img.save(img_stream, "JPEG")
1183             result[obj.id] = img_stream.getvalue().encode('base64')
1184         return result
1185
1186     _columns = {
1187         'name': fields.char('Name', size=64, required=True, translate=True),
1188         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1189         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1190         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1191         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1192         'category_image': fields.binary('Image'),
1193         'category_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1194             store = {
1195                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['category_image'], 10),
1196             }),
1197     }
1198
1199 pos_category()
1200
1201 import io, StringIO
1202
1203 class product_product(osv.osv):
1204     _inherit = 'product.product'
1205     def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1206         result = {}
1207         for obj in self.browse(cr, uid, ids, context=context):
1208             if not obj.product_image:
1209                 result[obj.id] = False
1210                 continue
1211
1212             image_stream = io.BytesIO(obj.product_image.decode('base64'))
1213             img = Image.open(image_stream)
1214             img.thumbnail((120, 100), Image.ANTIALIAS)
1215             img_stream = StringIO.StringIO()
1216             img.save(img_stream, "JPEG")
1217             result[obj.id] = img_stream.getvalue().encode('base64')
1218         return result
1219
1220     _columns = {
1221         'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."),
1222         'expense_pdt': fields.boolean('Point of Sale Cash Out', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."),
1223         'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1224             help="If you want to sell this product through the point of sale, select the category it belongs to."),
1225         'product_image_small': fields.function(_get_small_image, string='Small Image', type="binary",
1226             store = {
1227                 'product.product': (lambda self, cr, uid, ids, c={}: ids, ['product_image'], 10),
1228             }),
1229         'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"),
1230     }
1231     _defaults = {
1232         'to_weight' : False,
1233     }
1234
1235 product_product()
1236
1237
1238 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: