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