2674d9de5cf16df64d690b1dbcce23d8da7c7677
[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')
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         wf_service = netsvc.LocalService("workflow")
761         for order in self.browse(cr, uid, ids, context=context):
762             stock_picking_obj.action_cancel(cr, uid, [order.picking_id.id])
763             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
764                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
765         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
766         return True
767
768     def add_payment(self, cr, uid, order_id, data, context=None):
769         """Create a new payment for the order"""
770         if not context:
771             context = {}
772         statement_line_obj = self.pool.get('account.bank.statement.line')
773         property_obj = self.pool.get('ir.property')
774         order = self.browse(cr, uid, order_id, context=context)
775         args = {
776             'amount': data['amount'],
777             'date': data.get('payment_date', time.strftime('%Y-%m-%d')),
778             'name': order.name + ': ' + (data.get('payment_name', '') or ''),
779         }
780
781         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
782         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
783                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
784         args['partner_id'] = order.partner_id and order.partner_id.id or None
785
786         if not args['account_id']:
787             if not args['partner_id']:
788                 msg = _('There is no receivable account defined to make payment.')
789             else:
790                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
791             raise osv.except_osv(_('Configuration Error!'), msg)
792
793         context.pop('pos_session_id', False)
794
795         journal_id = data.get('journal', False)
796         statement_id = data.get('statement_id', False)
797         assert journal_id or statement_id, "No statement_id or journal_id passed to the method!"
798
799         for statement in order.session_id.statement_ids:
800             if statement.id == statement_id:
801                 journal_id = statement.journal_id.id
802                 break
803             elif statement.journal_id.id == journal_id:
804                 statement_id = statement.id
805                 break
806
807         if not statement_id:
808             raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
809
810         args.update({
811             'statement_id' : statement_id,
812             'pos_statement_id' : order_id,
813             'journal_id' : journal_id,
814             'type' : 'customer',
815             'ref' : order.session_id.name,
816         })
817
818         statement_line_obj.create(cr, uid, args, context=context)
819
820         return statement_id
821
822     def refund(self, cr, uid, ids, context=None):
823         """Create a copy of order  for refund order"""
824         clone_list = []
825         line_obj = self.pool.get('pos.order.line')
826         
827         for order in self.browse(cr, uid, ids, context=context):
828             current_session_ids = self.pool.get('pos.session').search(cr, uid, [
829                 ('state', '!=', 'closed'),
830                 ('user_id', '=', uid)], context=context)
831             if not current_session_ids:
832                 raise osv.except_osv(_('Error!'), _('To return product(s), you need to open a session that will be used to register the refund.'))
833
834             clone_id = self.copy(cr, uid, order.id, {
835                 'name': order.name + ' REFUND', # not used, name forced by create
836                 'session_id': current_session_ids[0],
837                 'date_order': time.strftime('%Y-%m-%d %H:%M:%S'),
838             }, context=context)
839             clone_list.append(clone_id)
840
841         for clone in self.browse(cr, uid, clone_list, context=context):
842             for order_line in clone.lines:
843                 line_obj.write(cr, uid, [order_line.id], {
844                     'qty': -order_line.qty
845                 }, context=context)
846
847         new_order = ','.join(map(str,clone_list))
848         abs = {
849             #'domain': "[('id', 'in', ["+new_order+"])]",
850             'name': _('Return Products'),
851             'view_type': 'form',
852             'view_mode': 'form',
853             'res_model': 'pos.order',
854             'res_id':clone_list[0],
855             'view_id': False,
856             'context':context,
857             'type': 'ir.actions.act_window',
858             'nodestroy': True,
859             'target': 'current',
860         }
861         return abs
862
863     def action_invoice_state(self, cr, uid, ids, context=None):
864         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
865
866     def action_invoice(self, cr, uid, ids, context=None):
867         inv_ref = self.pool.get('account.invoice')
868         inv_line_ref = self.pool.get('account.invoice.line')
869         product_obj = self.pool.get('product.product')
870         inv_ids = []
871
872         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
873             if order.invoice_id:
874                 inv_ids.append(order.invoice_id.id)
875                 continue
876
877             if not order.partner_id:
878                 raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.'))
879
880             acc = order.partner_id.property_account_receivable.id
881             inv = {
882                 'name': order.name,
883                 'origin': order.name,
884                 'account_id': acc,
885                 'journal_id': order.sale_journal.id or None,
886                 'type': 'out_invoice',
887                 'reference': order.name,
888                 'partner_id': order.partner_id.id,
889                 'comment': order.note or '',
890                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
891             }
892             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
893             if not inv.get('account_id', None):
894                 inv['account_id'] = acc
895             inv_id = inv_ref.create(cr, uid, inv, context=context)
896
897             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
898             inv_ids.append(inv_id)
899             for line in order.lines:
900                 inv_line = {
901                     'invoice_id': inv_id,
902                     'product_id': line.product_id.id,
903                     'quantity': line.qty,
904                 }
905                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
906                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
907                                                                line.product_id.id,
908                                                                line.product_id.uom_id.id,
909                                                                line.qty, partner_id = order.partner_id.id,
910                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
911                 if line.product_id.description_sale:
912                     inv_line['note'] = line.product_id.description_sale
913                 inv_line['price_unit'] = line.price_unit
914                 inv_line['discount'] = line.discount
915                 inv_line['name'] = inv_name
916                 inv_line['invoice_line_tax_id'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )]
917                 inv_line_ref.create(cr, uid, inv_line, context=context)
918             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
919             self.signal_invoice(cr, uid, [order.id])
920             inv_ref.signal_validate(cr, uid, [inv_id])
921
922         if not inv_ids: return {}
923
924         mod_obj = self.pool.get('ir.model.data')
925         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
926         res_id = res and res[1] or False
927         return {
928             'name': _('Customer Invoice'),
929             'view_type': 'form',
930             'view_mode': 'form',
931             'view_id': [res_id],
932             'res_model': 'account.invoice',
933             'context': "{'type':'out_invoice'}",
934             'type': 'ir.actions.act_window',
935             'nodestroy': True,
936             'target': 'current',
937             'res_id': inv_ids and inv_ids[0] or False,
938         }
939
940     def create_account_move(self, cr, uid, ids, context=None):
941         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
942
943     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
944         # Tricky, via the workflow, we only have one id in the ids variable
945         """Create a account move line of order grouped by products or not."""
946         account_move_obj = self.pool.get('account.move')
947         account_move_line_obj = self.pool.get('account.move.line')
948         account_period_obj = self.pool.get('account.period')
949         account_tax_obj = self.pool.get('account.tax')
950         user_proxy = self.pool.get('res.users')
951         property_obj = self.pool.get('ir.property')
952         cur_obj = self.pool.get('res.currency')
953
954         period = account_period_obj.find(cr, uid, context=context)[0]
955
956         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
957
958         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
959             raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
960
961         current_company = user_proxy.browse(cr, uid, uid, context=context).company_id
962
963         grouped_data = {}
964         have_to_group_by = session and session.config_id.group_by or False
965
966         def compute_tax(amount, tax, line):
967             if amount > 0:
968                 tax_code_id = tax['base_code_id']
969                 tax_amount = line.price_subtotal * tax['base_sign']
970             else:
971                 tax_code_id = tax['ref_base_code_id']
972                 tax_amount = line.price_subtotal * tax['ref_base_sign']
973
974             return (tax_code_id, tax_amount,)
975
976         for order in self.browse(cr, uid, ids, context=context):
977             if order.account_move:
978                 continue
979             if order.state != 'paid':
980                 continue
981
982             user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id
983
984             group_tax = {}
985             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
986
987             order_account = order.partner_id and \
988                             order.partner_id.property_account_receivable and \
989                             order.partner_id.property_account_receivable.id or \
990                             account_def and account_def.id or current_company.account_receivable.id
991
992             if move_id is None:
993                 # Create an entry for the sale
994                 move_id = account_move_obj.create(cr, uid, {
995                     'ref' : order.name,
996                     'journal_id': order.sale_journal.id,
997                 }, context=context)
998
999             def insert_data(data_type, values):
1000                 # if have_to_group_by:
1001
1002                 sale_journal_id = order.sale_journal.id
1003
1004                 # 'quantity': line.qty,
1005                 # 'product_id': line.product_id.id,
1006                 values.update({
1007                     'date': order.date_order[:10],
1008                     'ref': order.name,
1009                     'journal_id' : sale_journal_id,
1010                     'period_id' : period,
1011                     'move_id' : move_id,
1012                     'company_id': user_company and user_company.id or False,
1013                 })
1014
1015                 if data_type == 'product':
1016                     key = ('product', values['partner_id'], values['product_id'], values['debit'] > 0)
1017                 elif data_type == 'tax':
1018                     key = ('tax', values['partner_id'], values['tax_code_id'], values['debit'] > 0)
1019                 elif data_type == 'counter_part':
1020                     key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0)
1021                 else:
1022                     return
1023
1024                 grouped_data.setdefault(key, [])
1025
1026                 # if not have_to_group_by or (not grouped_data[key]):
1027                 #     grouped_data[key].append(values)
1028                 # else:
1029                 #     pass
1030
1031                 if have_to_group_by:
1032                     if not grouped_data[key]:
1033                         grouped_data[key].append(values)
1034                     else:
1035                         current_value = grouped_data[key][0]
1036                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
1037                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
1038                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
1039                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
1040                 else:
1041                     grouped_data[key].append(values)
1042
1043             #because of the weird way the pos order is written, we need to make sure there is at least one line, 
1044             #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that 
1045             #are set inside the for loop)
1046             #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack
1047             assert order.lines, _('The POS order must have lines when calling this method')
1048             # Create an move for each order line
1049
1050             cur = order.pricelist_id.currency_id
1051             for line in order.lines:
1052                 tax_amount = 0
1053                 taxes = [t for t in line.product_id.taxes_id]
1054                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
1055
1056                 for tax in computed_taxes:
1057                     tax_amount += cur_obj.round(cr, uid, cur, tax['amount'])
1058                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
1059
1060                     group_tax.setdefault(group_key, 0)
1061                     group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount'])
1062
1063                 amount = line.price_subtotal
1064
1065                 # Search for the income account
1066                 if  line.product_id.property_account_income.id:
1067                     income_account = line.product_id.property_account_income.id
1068                 elif line.product_id.categ_id.property_account_income_categ.id:
1069                     income_account = line.product_id.categ_id.property_account_income_categ.id
1070                 else:
1071                     raise osv.except_osv(_('Error!'), _('Please define income '\
1072                         'account for this product: "%s" (id:%d).') \
1073                         % (line.product_id.name, line.product_id.id, ))
1074
1075                 # Empty the tax list as long as there is no tax code:
1076                 tax_code_id = False
1077                 tax_amount = 0
1078                 while computed_taxes:
1079                     tax = computed_taxes.pop(0)
1080                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1081
1082                     # If there is one we stop
1083                     if tax_code_id:
1084                         break
1085
1086                 # Create a move for the line
1087                 insert_data('product', {
1088                     'name': line.product_id.name,
1089                     'quantity': line.qty,
1090                     'product_id': line.product_id.id,
1091                     'account_id': income_account,
1092                     'credit': ((amount>0) and amount) or 0.0,
1093                     'debit': ((amount<0) and -amount) or 0.0,
1094                     'tax_code_id': tax_code_id,
1095                     'tax_amount': tax_amount,
1096                     'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1097                 })
1098
1099                 # For each remaining tax with a code, whe create a move line
1100                 for tax in computed_taxes:
1101                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1102                     if not tax_code_id:
1103                         continue
1104
1105                     insert_data('tax', {
1106                         'name': _('Tax'),
1107                         'product_id':line.product_id.id,
1108                         'quantity': line.qty,
1109                         'account_id': income_account,
1110                         'credit': 0.0,
1111                         'debit': 0.0,
1112                         'tax_code_id': tax_code_id,
1113                         'tax_amount': tax_amount,
1114                         'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1115                     })
1116
1117             # Create a move for each tax group
1118             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1119
1120             for key, tax_amount in group_tax.items():
1121                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1122                 insert_data('tax', {
1123                     'name': _('Tax') + ' ' + tax.name,
1124                     'quantity': line.qty,
1125                     'product_id': line.product_id.id,
1126                     'account_id': key[account_pos] or income_account,
1127                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
1128                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1129                     'tax_code_id': key[tax_code_pos],
1130                     'tax_amount': tax_amount,
1131                     'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1132                 })
1133
1134             # counterpart
1135             insert_data('counter_part', {
1136                 'name': _("Trade Receivables"), #order.name,
1137                 'account_id': order_account,
1138                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1139                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1140                 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1141             })
1142
1143             order.write({'state':'done', 'account_move': move_id})
1144
1145         all_lines = []
1146         for group_key, group_data in grouped_data.iteritems():
1147             for value in group_data:
1148                 all_lines.append((0, 0, value),)
1149         if move_id: #In case no order was changed
1150             self.pool.get("account.move").write(cr, uid, [move_id], {'line_id':all_lines}, context=context)
1151
1152         return True
1153
1154     def action_payment(self, cr, uid, ids, context=None):
1155         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1156
1157     def action_paid(self, cr, uid, ids, context=None):
1158         self.create_picking(cr, uid, ids, context=context)
1159         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1160         return True
1161
1162     def action_cancel(self, cr, uid, ids, context=None):
1163         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1164         return True
1165
1166     def action_done(self, cr, uid, ids, context=None):
1167         self.create_account_move(cr, uid, ids, context=context)
1168         return True
1169
1170 class account_bank_statement(osv.osv):
1171     _inherit = 'account.bank.statement'
1172     _columns= {
1173         'user_id': fields.many2one('res.users', 'User', readonly=True),
1174     }
1175     _defaults = {
1176         'user_id': lambda self,cr,uid,c={}: uid
1177     }
1178
1179 class account_bank_statement_line(osv.osv):
1180     _inherit = 'account.bank.statement.line'
1181     _columns= {
1182         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1183     }
1184
1185
1186 class pos_order_line(osv.osv):
1187     _name = "pos.order.line"
1188     _description = "Lines of Point of Sale"
1189     _rec_name = "product_id"
1190
1191     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1192         res = dict([(i, {}) for i in ids])
1193         account_tax_obj = self.pool.get('account.tax')
1194         cur_obj = self.pool.get('res.currency')
1195         for line in self.browse(cr, uid, ids, context=context):
1196             taxes_ids = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ]
1197             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1198             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)
1199
1200             cur = line.order_id.pricelist_id.currency_id
1201             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1202             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1203         return res
1204
1205     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1206        context = context or {}
1207        if not product_id:
1208             return {}
1209        if not pricelist:
1210            raise osv.except_osv(_('No Pricelist!'),
1211                _('You have to select a pricelist in the sale form !\n' \
1212                'Please set one before choosing a product.'))
1213
1214        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1215                product_id, qty or 1.0, partner_id)[pricelist]
1216
1217        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1218        result['value']['price_unit'] = price
1219        return result
1220
1221     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1222         result = {}
1223         if not product:
1224             return result
1225         account_tax_obj = self.pool.get('account.tax')
1226         cur_obj = self.pool.get('res.currency')
1227
1228         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1229
1230         price = price_unit * (1 - (discount or 0.0) / 100.0)
1231         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1232
1233         result['price_subtotal'] = taxes['total']
1234         result['price_subtotal_incl'] = taxes['total_included']
1235         return {'value': result}
1236
1237     _columns = {
1238         'company_id': fields.many2one('res.company', 'Company', required=True),
1239         'name': fields.char('Line No', size=32, required=True),
1240         'notice': fields.char('Discount Notice', size=128),
1241         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1242         'price_unit': fields.float(string='Unit Price', digits=(16, 2)),
1243         'qty': fields.float('Quantity', digits=(16, 2)),
1244         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1245         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1246         'discount': fields.float('Discount (%)', digits=(16, 2)),
1247         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1248         'create_date': fields.datetime('Creation Date', readonly=True),
1249     }
1250
1251     _defaults = {
1252         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1253         'qty': lambda *a: 1,
1254         'discount': lambda *a: 0.0,
1255         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1256     }
1257
1258     def copy_data(self, cr, uid, id, default=None, context=None):
1259         if not default:
1260             default = {}
1261         default.update({
1262             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1263         })
1264         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1265
1266 class pos_category(osv.osv):
1267     _name = 'pos.category'
1268     _description = "Point of Sale Category"
1269     _order = "sequence, name"
1270     def _check_recursion(self, cr, uid, ids, context=None):
1271         level = 100
1272         while len(ids):
1273             cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),))
1274             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
1275             if not level:
1276                 return False
1277             level -= 1
1278         return True
1279
1280     _constraints = [
1281         (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
1282     ]
1283
1284     def name_get(self, cr, uid, ids, context=None):
1285         if not len(ids):
1286             return []
1287         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
1288         res = []
1289         for record in reads:
1290             name = record['name']
1291             if record['parent_id']:
1292                 name = record['parent_id'][1]+' / '+name
1293             res.append((record['id'], name))
1294         return res
1295
1296     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
1297         res = self.name_get(cr, uid, ids, context=context)
1298         return dict(res)
1299
1300     def _get_image(self, cr, uid, ids, name, args, context=None):
1301         result = dict.fromkeys(ids, False)
1302         for obj in self.browse(cr, uid, ids, context=context):
1303             result[obj.id] = tools.image_get_resized_images(obj.image)
1304         return result
1305     
1306     def _set_image(self, cr, uid, id, name, value, args, context=None):
1307         return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
1308
1309     _columns = {
1310         'name': fields.char('Name', size=64, required=True, translate=True),
1311         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
1312         'parent_id': fields.many2one('pos.category','Parent Category', select=True),
1313         'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
1314         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
1315         
1316         # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
1317         # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
1318         # In this case, the default image is set by the js code.
1319         # NOTE2: image: all image fields are base64 encoded and PIL-supported
1320         'image': fields.binary("Image",
1321             help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
1322         'image_medium': fields.function(_get_image, fnct_inv=_set_image,
1323             string="Medium-sized image", type="binary", multi="_get_image",
1324             store={
1325                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1326             },
1327             help="Medium-sized image of the category. It is automatically "\
1328                  "resized as a 128x128px image, with aspect ratio preserved. "\
1329                  "Use this field in form views or some kanban views."),
1330         'image_small': fields.function(_get_image, fnct_inv=_set_image,
1331             string="Smal-sized image", type="binary", multi="_get_image",
1332             store={
1333                 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
1334             },
1335             help="Small-sized image of the category. It is automatically "\
1336                  "resized as a 64x64px image, with aspect ratio preserved. "\
1337                  "Use this field anywhere a small image is required."),
1338     }
1339
1340 import io, StringIO
1341
1342 class ean_wizard(osv.osv_memory):
1343     _name = 'pos.ean_wizard'
1344     _columns = {
1345         'ean13_pattern': fields.char('Reference', size=32, required=True, translate=True),
1346     }
1347     def sanitize_ean13(self, cr, uid, ids, context):
1348         for r in self.browse(cr,uid,ids):
1349             ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern)
1350             m = context.get('active_model')
1351             m_id =  context.get('active_id')
1352             self.pool[m].write(cr,uid,[m_id],{'ean13':ean13})
1353         return { 'type' : 'ir.actions.act_window_close' }
1354
1355 class product_product(osv.osv):
1356     _inherit = 'product.product'
1357
1358
1359     #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1360     #    result = {}
1361     #    for obj in self.browse(cr, uid, ids, context=context):
1362     #        if not obj.product_image:
1363     #            result[obj.id] = False
1364     #            continue
1365
1366     #        image_stream = io.BytesIO(obj.product_image.decode('base64'))
1367     #        img = Image.open(image_stream)
1368     #        img.thumbnail((120, 100), Image.ANTIALIAS)
1369     #        img_stream = StringIO.StringIO()
1370     #        img.save(img_stream, "JPEG")
1371     #        result[obj.id] = img_stream.getvalue().encode('base64')
1372     #    return result
1373
1374     _columns = {
1375         '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."),
1376         '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."),
1377         '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'), 
1378         'pos_categ_id': fields.many2one('pos.category','Point of Sale Category',
1379             help="These products belong to those categories that are used to group similar products and are specific to the Point of Sale."),
1380         'to_weight' : fields.boolean('To Weight', help="Check if the product should be weighted (mainly used with self check-out interface)."),
1381     }
1382
1383
1384     _defaults = {
1385         'to_weight' : False,
1386         'available_in_pos': True,
1387     }
1388
1389     def edit_ean(self, cr, uid, ids, context):
1390         return {
1391             'name': _("Assign a Custom EAN"),
1392             'type': 'ir.actions.act_window',
1393             'view_type': 'form',
1394             'view_mode': 'form',
1395             'res_model': 'pos.ean_wizard',
1396             'target' : 'new',
1397             'view_id': False,
1398             'context':context,
1399         }
1400
1401 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: