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