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