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