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