[MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 10056 revid:dle@openerp.com...
[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         
518         # Keep only new orders
519         submitted_references = [o['data']['name'] for o in orders]
520         existing_order_ids = self.search(cr, uid, [('pos_reference', 'in', submitted_references)], context=context)
521         existing_orders = self.read(cr, uid, existing_order_ids, ['pos_reference'], context=context)
522         existing_references = set([o['pos_reference'] for o in existing_orders])
523         orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
524
525         order_ids = []
526         for tmp_order in orders_to_save:
527             to_invoice = tmp_order['to_invoice']
528             order = tmp_order['data']
529
530             order_id = self.create(cr, uid, {
531                 'name': order['name'],
532                 'user_id': order['user_id'] or False,
533                 'session_id': order['pos_session_id'],
534                 'lines': order['lines'],
535                 'pos_reference':order['name'],
536                 'partner_id': order['partner_id'] or False
537             }, context)
538             for payments in order['statement_ids']:
539                 payment = payments[2]
540                 self.add_payment(cr, uid, order_id, {
541                     'amount': payment['amount'] or 0.0,
542                     'payment_date': payment['name'],
543                     'statement_id': payment['statement_id'],
544                     'payment_name': payment.get('note', False),
545                     'journal': payment['journal_id']
546                 }, context=context)
547
548             if order['amount_return']:
549                 session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
550                 cash_journal = session.cash_journal_id
551                 if not cash_journal:
552                     cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
553                     if not len(cash_journal_ids):
554                         raise osv.except_osv( _('error!'),
555                             _("No cash statement found for this session. Unable to record returned cash."))
556                     cash_journal = cash_journal_ids[0].journal_id
557                 self.add_payment(cr, uid, order_id, {
558                     'amount': -order['amount_return'],
559                     'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
560                     'payment_name': _('return'),
561                     'journal': cash_journal.id,
562                 }, context=context)
563             order_ids.append(order_id)
564
565             try:
566                 self.signal_paid(cr, uid, [order_id])
567             except Exception as e:
568                 _logger.error('Could not fully process the POS Order: %s', tools.ustr(e))
569
570             if to_invoice:
571                 self.action_invoice(cr, uid, [order_id], context)
572                 order_obj = self.browse(cr, uid, order_id, context)
573                 self.pool['account.invoice'].signal_invoice_open(cr, uid, [order_obj.invoice_id.id])
574
575         return order_ids
576
577     def write(self, cr, uid, ids, vals, context=None):
578         res = super(pos_order, self).write(cr, uid, ids, vals, context=context)
579         #If you change the partner of the PoS order, change also the partner of the associated bank statement lines
580         partner_obj = self.pool.get('res.partner')
581         bsl_obj = self.pool.get("account.bank.statement.line")
582         if 'partner_id' in vals:
583             for posorder in self.browse(cr, uid, ids, context=context):
584                 if posorder.invoice_id:
585                     raise osv.except_osv( _('Error!'), _("You cannot change the partner of a POS order for which an invoice has already been issued."))
586                 if vals['partner_id']:
587                     p_id = partner_obj.browse(cr, uid, vals['partner_id'], context=context)
588                     part_id = partner_obj._find_accounting_partner(p_id).id
589                 else:
590                     part_id = False
591                 bsl_ids = [x.id for x in posorder.statement_ids]
592                 bsl_obj.write(cr, uid, bsl_ids, {'partner_id': part_id}, context=context)
593         return res
594
595     def unlink(self, cr, uid, ids, context=None):
596         for rec in self.browse(cr, uid, ids, context=context):
597             if rec.state not in ('draft','cancel'):
598                 raise osv.except_osv(_('Unable to Delete!'), _('In order to delete a sale, it must be new or cancelled.'))
599         return super(pos_order, self).unlink(cr, uid, ids, context=context)
600
601     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
602         if not part:
603             return {'value': {}}
604         pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id
605         return {'value': {'pricelist_id': pricelist}}
606
607     def _amount_all(self, cr, uid, ids, name, args, context=None):
608         tax_obj = self.pool.get('account.tax')
609         cur_obj = self.pool.get('res.currency')
610         res = {}
611         for order in self.browse(cr, uid, ids, context=context):
612             res[order.id] = {
613                 'amount_paid': 0.0,
614                 'amount_return':0.0,
615                 'amount_tax':0.0,
616             }
617             val1 = val2 = 0.0
618             cur = order.pricelist_id.currency_id
619             for payment in order.statement_ids:
620                 res[order.id]['amount_paid'] +=  payment.amount
621                 res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0)
622             for line in order.lines:
623                 val1 += line.price_subtotal_incl
624                 val2 += line.price_subtotal
625             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2)
626             res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1)
627         return res
628
629     def copy(self, cr, uid, id, default=None, context=None):
630         if not default:
631             default = {}
632         d = {
633             'state': 'draft',
634             'invoice_id': False,
635             'account_move': False,
636             'picking_id': False,
637             'statement_ids': [],
638             'nb_print': 0,
639             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'),
640         }
641         d.update(default)
642         return super(pos_order, self).copy(cr, uid, id, d, context=context)
643
644     _columns = {
645         'name': fields.char('Order Ref', size=64, required=True, readonly=True),
646         'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True),
647         'warehouse_id': fields.related('session_id', 'config_id', 'warehouse_id', relation='stock.warehouse', type='many2one', string='Warehouse', store=True, readonly=True),
648         'date_order': fields.datetime('Order Date', readonly=True, select=True),
649         '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."),
650         'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Account'), multi='all'),
651         'amount_total': fields.function(_amount_all, string='Total', multi='all'),
652         'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Account'), multi='all'),
653         'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Account'), multi='all'),
654         'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True),
655         'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True),
656         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True),
657         'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}),
658
659         'session_id' : fields.many2one('pos.session', 'Session', 
660                                         #required=True,
661                                         select=1,
662                                         domain="[('state', '=', 'opened')]",
663                                         states={'draft' : [('readonly', False)]},
664                                         readonly=True),
665
666         'state': fields.selection([('draft', 'New'),
667                                    ('cancel', 'Cancelled'),
668                                    ('paid', 'Paid'),
669                                    ('done', 'Posted'),
670                                    ('invoiced', 'Invoiced')],
671                                   'Status', readonly=True),
672
673         'invoice_id': fields.many2one('account.invoice', 'Invoice'),
674         'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True),
675         'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True),
676         'note': fields.text('Internal Notes'),
677         'nb_print': fields.integer('Number of Print', readonly=True),
678         'pos_reference': fields.char('Receipt Ref', size=64, readonly=True),
679         'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True),
680     }
681
682     def _default_session(self, cr, uid, context=None):
683         so = self.pool.get('pos.session')
684         session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context)
685         return session_ids and session_ids[0] or False
686
687     def _default_pricelist(self, cr, uid, context=None):
688         session_ids = self._default_session(cr, uid, context) 
689         if session_ids:
690             session_record = self.pool.get('pos.session').browse(cr, uid, session_ids, context=context)
691             return session_record.config_id.pricelist_id and session_record.config_id.pricelist_id.id or False
692         return False
693
694     _defaults = {
695         'user_id': lambda self, cr, uid, context: uid,
696         'state': 'draft',
697         'name': '/', 
698         'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
699         'nb_print': 0,
700         'session_id': _default_session,
701         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
702         'pricelist_id': _default_pricelist,
703     }
704
705     def create(self, cr, uid, values, context=None):
706         values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order')
707         return super(pos_order, self).create(cr, uid, values, context=context)
708
709     def test_paid(self, cr, uid, ids, context=None):
710         """A Point of Sale is paid when the sum
711         @return: True
712         """
713         for order in self.browse(cr, uid, ids, context=context):
714             if order.lines and not order.amount_total:
715                 return True
716             if (not order.lines) or (not order.statement_ids) or \
717                 (abs(order.amount_total-order.amount_paid) > 0.00001):
718                 return False
719         return True
720
721     def create_picking(self, cr, uid, ids, context=None):
722         """Create a picking for each order and validate it."""
723         picking_obj = self.pool.get('stock.picking.out')
724         partner_obj = self.pool.get('res.partner')
725         move_obj = self.pool.get('stock.move')
726
727         for order in self.browse(cr, uid, ids, context=context):
728             addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {}
729             picking_id = picking_obj.create(cr, uid, {
730                 'origin': order.name,
731                 'partner_id': addr.get('delivery',False),
732                 'type': 'out',
733                 'company_id': order.company_id.id,
734                 'move_type': 'direct',
735                 'note': order.note or "",
736                 'invoice_state': 'none',
737                 'auto_picking': True,
738             }, context=context)
739             self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context)
740             location_id = order.warehouse_id.lot_stock_id.id
741             if order.partner_id:
742                 destination_id = order.partner_id.property_stock_customer.id
743             else:
744                 destination_id = partner_obj.default_get(cr, uid, ['property_stock_customer'], context=context)['property_stock_customer']
745
746             for line in order.lines:
747                 if line.product_id and line.product_id.type == 'service':
748                     continue
749
750                 move_obj.create(cr, uid, {
751                     'name': line.name,
752                     'product_uom': line.product_id.uom_id.id,
753                     'product_uos': line.product_id.uom_id.id,
754                     'picking_id': picking_id,
755                     'product_id': line.product_id.id,
756                     'product_uos_qty': abs(line.qty),
757                     'product_qty': abs(line.qty),
758                     'tracking_id': False,
759                     'state': 'draft',
760                     'location_id': location_id if line.qty >= 0 else destination_id,
761                     'location_dest_id': destination_id if line.qty >= 0 else location_id,
762                 }, context=context)
763             
764             picking_obj.signal_button_confirm(cr, uid, [picking_id])
765             picking_obj.force_assign(cr, uid, [picking_id], context)
766         return True
767
768     def cancel_order(self, cr, uid, ids, context=None):
769         """ Changes order state to cancel
770         @return: True
771         """
772         stock_picking_obj = self.pool.get('stock.picking')
773         for order in self.browse(cr, uid, ids, context=context):
774             stock_picking_obj.signal_button_cancel(cr, uid, [order.picking_id.id])
775             if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel':
776                 raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.'))
777         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
778         return True
779
780     def add_payment(self, cr, uid, order_id, data, context=None):
781         """Create a new payment for the order"""
782         if not context:
783             context = {}
784         statement_line_obj = self.pool.get('account.bank.statement.line')
785         property_obj = self.pool.get('ir.property')
786         order = self.browse(cr, uid, order_id, context=context)
787         args = {
788             'amount': data['amount'],
789             'date': data.get('payment_date', time.strftime('%Y-%m-%d')),
790             'name': order.name + ': ' + (data.get('payment_name', '') or ''),
791         }
792
793         account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
794         args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
795                              and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
796         args['partner_id'] = order.partner_id and order.partner_id.id or None
797
798         if not args['account_id']:
799             if not args['partner_id']:
800                 msg = _('There is no receivable account defined to make payment.')
801             else:
802                 msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
803             raise osv.except_osv(_('Configuration Error!'), msg)
804
805         context.pop('pos_session_id', False)
806
807         journal_id = data.get('journal', False)
808         statement_id = data.get('statement_id', False)
809         assert journal_id or statement_id, "No statement_id or journal_id passed to the method!"
810
811         for statement in order.session_id.statement_ids:
812             if statement.id == statement_id:
813                 journal_id = statement.journal_id.id
814                 break
815             elif statement.journal_id.id == journal_id:
816                 statement_id = statement.id
817                 break
818
819         if not statement_id:
820             raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
821
822         args.update({
823             'statement_id' : statement_id,
824             'pos_statement_id' : order_id,
825             'journal_id' : journal_id,
826             'type' : 'customer',
827             'ref' : order.session_id.name,
828         })
829
830         statement_line_obj.create(cr, uid, args, context=context)
831
832         return statement_id
833
834     def refund(self, cr, uid, ids, context=None):
835         """Create a copy of order  for refund order"""
836         clone_list = []
837         line_obj = self.pool.get('pos.order.line')
838         
839         for order in self.browse(cr, uid, ids, context=context):
840             current_session_ids = self.pool.get('pos.session').search(cr, uid, [
841                 ('state', '!=', 'closed'),
842                 ('user_id', '=', uid)], context=context)
843             if not current_session_ids:
844                 raise osv.except_osv(_('Error!'), _('To return product(s), you need to open a session that will be used to register the refund.'))
845
846             clone_id = self.copy(cr, uid, order.id, {
847                 'name': order.name + ' REFUND', # not used, name forced by create
848                 'session_id': current_session_ids[0],
849                 'date_order': time.strftime('%Y-%m-%d %H:%M:%S'),
850             }, context=context)
851             clone_list.append(clone_id)
852
853         for clone in self.browse(cr, uid, clone_list, context=context):
854             for order_line in clone.lines:
855                 line_obj.write(cr, uid, [order_line.id], {
856                     'qty': -order_line.qty
857                 }, context=context)
858
859         new_order = ','.join(map(str,clone_list))
860         abs = {
861             #'domain': "[('id', 'in', ["+new_order+"])]",
862             'name': _('Return Products'),
863             'view_type': 'form',
864             'view_mode': 'form',
865             'res_model': 'pos.order',
866             'res_id':clone_list[0],
867             'view_id': False,
868             'context':context,
869             'type': 'ir.actions.act_window',
870             'nodestroy': True,
871             'target': 'current',
872         }
873         return abs
874
875     def action_invoice_state(self, cr, uid, ids, context=None):
876         return self.write(cr, uid, ids, {'state':'invoiced'}, context=context)
877
878     def action_invoice(self, cr, uid, ids, context=None):
879         inv_ref = self.pool.get('account.invoice')
880         inv_line_ref = self.pool.get('account.invoice.line')
881         product_obj = self.pool.get('product.product')
882         inv_ids = []
883
884         for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context):
885             if order.invoice_id:
886                 inv_ids.append(order.invoice_id.id)
887                 continue
888
889             if not order.partner_id:
890                 raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.'))
891
892             acc = order.partner_id.property_account_receivable.id
893             inv = {
894                 'name': order.name,
895                 'origin': order.name,
896                 'account_id': acc,
897                 'journal_id': order.sale_journal.id or None,
898                 'type': 'out_invoice',
899                 'reference': order.name,
900                 'partner_id': order.partner_id.id,
901                 'comment': order.note or '',
902                 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency
903             }
904             inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value'])
905             if not inv.get('account_id', None):
906                 inv['account_id'] = acc
907             inv_id = inv_ref.create(cr, uid, inv, context=context)
908
909             self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context)
910             inv_ids.append(inv_id)
911             for line in order.lines:
912                 inv_line = {
913                     'invoice_id': inv_id,
914                     'product_id': line.product_id.id,
915                     'quantity': line.qty,
916                 }
917                 inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1]
918                 inv_line.update(inv_line_ref.product_id_change(cr, uid, [],
919                                                                line.product_id.id,
920                                                                line.product_id.uom_id.id,
921                                                                line.qty, partner_id = order.partner_id.id,
922                                                                fposition_id=order.partner_id.property_account_position.id)['value'])
923                 if line.product_id.description_sale:
924                     inv_line['note'] = line.product_id.description_sale
925                 inv_line['price_unit'] = line.price_unit
926                 inv_line['discount'] = line.discount
927                 inv_line['name'] = inv_name
928                 inv_line['invoice_line_tax_id'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )]
929                 inv_line_ref.create(cr, uid, inv_line, context=context)
930             inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
931             self.signal_invoice(cr, uid, [order.id])
932             inv_ref.signal_validate(cr, uid, [inv_id])
933
934         if not inv_ids: return {}
935
936         mod_obj = self.pool.get('ir.model.data')
937         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
938         res_id = res and res[1] or False
939         return {
940             'name': _('Customer Invoice'),
941             'view_type': 'form',
942             'view_mode': 'form',
943             'view_id': [res_id],
944             'res_model': 'account.invoice',
945             'context': "{'type':'out_invoice'}",
946             'type': 'ir.actions.act_window',
947             'nodestroy': True,
948             'target': 'current',
949             'res_id': inv_ids and inv_ids[0] or False,
950         }
951
952     def create_account_move(self, cr, uid, ids, context=None):
953         return self._create_account_move_line(cr, uid, ids, None, None, context=context)
954
955     def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None):
956         # Tricky, via the workflow, we only have one id in the ids variable
957         """Create a account move line of order grouped by products or not."""
958         account_move_obj = self.pool.get('account.move')
959         account_period_obj = self.pool.get('account.period')
960         account_tax_obj = self.pool.get('account.tax')
961         property_obj = self.pool.get('ir.property')
962         cur_obj = self.pool.get('res.currency')
963
964         #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context))
965
966         if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)):
967             raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!'))
968
969         grouped_data = {}
970         have_to_group_by = session and session.config_id.group_by or False
971
972         def compute_tax(amount, tax, line):
973             if amount > 0:
974                 tax_code_id = tax['base_code_id']
975                 tax_amount = line.price_subtotal * tax['base_sign']
976             else:
977                 tax_code_id = tax['ref_base_code_id']
978                 tax_amount = line.price_subtotal * tax['ref_base_sign']
979
980             return (tax_code_id, tax_amount,)
981
982         for order in self.browse(cr, uid, ids, context=context):
983             if order.account_move:
984                 continue
985             if order.state != 'paid':
986                 continue
987
988             current_company = order.sale_journal.company_id
989
990             group_tax = {}
991             account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
992
993             order_account = order.partner_id and \
994                             order.partner_id.property_account_receivable and \
995                             order.partner_id.property_account_receivable.id or \
996                             account_def and account_def.id or current_company.account_receivable.id
997
998             if move_id is None:
999                 # Create an entry for the sale
1000                 move_id = account_move_obj.create(cr, uid, {
1001                     'ref' : order.name,
1002                     'journal_id': order.sale_journal.id,
1003                 }, context=context)
1004
1005             def insert_data(data_type, values):
1006                 # if have_to_group_by:
1007
1008                 sale_journal_id = order.sale_journal.id
1009                 period = account_period_obj.find(cr, uid, context=dict(context or {}, company_id=current_company.id))[0]
1010
1011                 # 'quantity': line.qty,
1012                 # 'product_id': line.product_id.id,
1013                 values.update({
1014                     'date': order.date_order[:10],
1015                     'ref': order.name,
1016                     'journal_id' : sale_journal_id,
1017                     'period_id' : period,
1018                     'move_id' : move_id,
1019                     'company_id': current_company.id,
1020                 })
1021
1022                 if data_type == 'product':
1023                     key = ('product', values['partner_id'], values['product_id'], values['debit'] > 0)
1024                 elif data_type == 'tax':
1025                     key = ('tax', values['partner_id'], values['tax_code_id'], values['debit'] > 0)
1026                 elif data_type == 'counter_part':
1027                     key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0)
1028                 else:
1029                     return
1030
1031                 grouped_data.setdefault(key, [])
1032
1033                 # if not have_to_group_by or (not grouped_data[key]):
1034                 #     grouped_data[key].append(values)
1035                 # else:
1036                 #     pass
1037
1038                 if have_to_group_by:
1039                     if not grouped_data[key]:
1040                         grouped_data[key].append(values)
1041                     else:
1042                         current_value = grouped_data[key][0]
1043                         current_value['quantity'] = current_value.get('quantity', 0.0) +  values.get('quantity', 0.0)
1044                         current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0)
1045                         current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0)
1046                         current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0)
1047                 else:
1048                     grouped_data[key].append(values)
1049
1050             #because of the weird way the pos order is written, we need to make sure there is at least one line, 
1051             #because just after the 'for' loop there are references to 'line' and 'income_account' variables (that 
1052             #are set inside the for loop)
1053             #TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack
1054             assert order.lines, _('The POS order must have lines when calling this method')
1055             # Create an move for each order line
1056
1057             cur = order.pricelist_id.currency_id
1058             for line in order.lines:
1059                 tax_amount = 0
1060                 taxes = []
1061                 for t in line.product_id.taxes_id:
1062                     if t.company_id.id == current_company.id:
1063                         taxes.append(t)
1064                 computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes']
1065
1066                 for tax in computed_taxes:
1067                     tax_amount += cur_obj.round(cr, uid, cur, tax['amount'])
1068                     group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id'])
1069
1070                     group_tax.setdefault(group_key, 0)
1071                     group_tax[group_key] += cur_obj.round(cr, uid, cur, tax['amount'])
1072
1073                 amount = line.price_subtotal
1074
1075                 # Search for the income account
1076                 if  line.product_id.property_account_income.id:
1077                     income_account = line.product_id.property_account_income.id
1078                 elif line.product_id.categ_id.property_account_income_categ.id:
1079                     income_account = line.product_id.categ_id.property_account_income_categ.id
1080                 else:
1081                     raise osv.except_osv(_('Error!'), _('Please define income '\
1082                         'account for this product: "%s" (id:%d).') \
1083                         % (line.product_id.name, line.product_id.id, ))
1084
1085                 # Empty the tax list as long as there is no tax code:
1086                 tax_code_id = False
1087                 tax_amount = 0
1088                 while computed_taxes:
1089                     tax = computed_taxes.pop(0)
1090                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1091
1092                     # If there is one we stop
1093                     if tax_code_id:
1094                         break
1095
1096                 # Create a move for the line
1097                 insert_data('product', {
1098                     'name': line.product_id.name,
1099                     'quantity': line.qty,
1100                     'product_id': line.product_id.id,
1101                     'account_id': income_account,
1102                     'credit': ((amount>0) and amount) or 0.0,
1103                     'debit': ((amount<0) and -amount) or 0.0,
1104                     'tax_code_id': tax_code_id,
1105                     'tax_amount': tax_amount,
1106                     'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1107                 })
1108
1109                 # For each remaining tax with a code, whe create a move line
1110                 for tax in computed_taxes:
1111                     tax_code_id, tax_amount = compute_tax(amount, tax, line)
1112                     if not tax_code_id:
1113                         continue
1114
1115                     insert_data('tax', {
1116                         'name': _('Tax'),
1117                         'product_id':line.product_id.id,
1118                         'quantity': line.qty,
1119                         'account_id': income_account,
1120                         'credit': 0.0,
1121                         'debit': 0.0,
1122                         'tax_code_id': tax_code_id,
1123                         'tax_amount': tax_amount,
1124                         'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1125                     })
1126
1127             # Create a move for each tax group
1128             (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3)
1129
1130             for key, tax_amount in group_tax.items():
1131                 tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context)
1132                 insert_data('tax', {
1133                     'name': _('Tax') + ' ' + tax.name,
1134                     'quantity': line.qty,
1135                     'product_id': line.product_id.id,
1136                     'account_id': key[account_pos] or income_account,
1137                     'credit': ((tax_amount>0) and tax_amount) or 0.0,
1138                     'debit': ((tax_amount<0) and -tax_amount) or 0.0,
1139                     'tax_code_id': key[tax_code_pos],
1140                     'tax_amount': tax_amount,
1141                     'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1142                 })
1143
1144             # counterpart
1145             insert_data('counter_part', {
1146                 'name': _("Trade Receivables"), #order.name,
1147                 'account_id': order_account,
1148                 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0,
1149                 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0,
1150                 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False
1151             })
1152
1153             order.write({'state':'done', 'account_move': move_id})
1154
1155         all_lines = []
1156         for group_key, group_data in grouped_data.iteritems():
1157             for value in group_data:
1158                 all_lines.append((0, 0, value),)
1159         if move_id: #In case no order was changed
1160             self.pool.get("account.move").write(cr, uid, [move_id], {'line_id':all_lines}, context=context)
1161
1162         return True
1163
1164     def action_payment(self, cr, uid, ids, context=None):
1165         return self.write(cr, uid, ids, {'state': 'payment'}, context=context)
1166
1167     def action_paid(self, cr, uid, ids, context=None):
1168         self.write(cr, uid, ids, {'state': 'paid'}, context=context)
1169         self.create_picking(cr, uid, ids, context=context)
1170         return True
1171
1172     def action_cancel(self, cr, uid, ids, context=None):
1173         self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
1174         return True
1175
1176     def action_done(self, cr, uid, ids, context=None):
1177         self.create_account_move(cr, uid, ids, context=context)
1178         return True
1179
1180 class account_bank_statement(osv.osv):
1181     _inherit = 'account.bank.statement'
1182     _columns= {
1183         'user_id': fields.many2one('res.users', 'User', readonly=True),
1184     }
1185     _defaults = {
1186         'user_id': lambda self,cr,uid,c={}: uid
1187     }
1188
1189 class account_bank_statement_line(osv.osv):
1190     _inherit = 'account.bank.statement.line'
1191     _columns= {
1192         'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'),
1193     }
1194
1195
1196 class pos_order_line(osv.osv):
1197     _name = "pos.order.line"
1198     _description = "Lines of Point of Sale"
1199     _rec_name = "product_id"
1200
1201     def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None):
1202         res = dict([(i, {}) for i in ids])
1203         account_tax_obj = self.pool.get('account.tax')
1204         cur_obj = self.pool.get('res.currency')
1205         for line in self.browse(cr, uid, ids, context=context):
1206             taxes_ids = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ]
1207             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1208             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)
1209
1210             cur = line.order_id.pricelist_id.currency_id
1211             res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total'])
1212             res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included'])
1213         return res
1214
1215     def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None):
1216        context = context or {}
1217        if not product_id:
1218             return {}
1219        if not pricelist:
1220            raise osv.except_osv(_('No Pricelist!'),
1221                _('You have to select a pricelist in the sale form !\n' \
1222                'Please set one before choosing a product.'))
1223
1224        price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1225                product_id, qty or 1.0, partner_id)[pricelist]
1226
1227        result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context)
1228        result['value']['price_unit'] = price
1229        return result
1230
1231     def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None):
1232         result = {}
1233         if not product:
1234             return result
1235         account_tax_obj = self.pool.get('account.tax')
1236         cur_obj = self.pool.get('res.currency')
1237
1238         prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1239
1240         price = price_unit * (1 - (discount or 0.0) / 100.0)
1241         taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)
1242
1243         result['price_subtotal'] = taxes['total']
1244         result['price_subtotal_incl'] = taxes['total_included']
1245         return {'value': result}
1246
1247     _columns = {
1248         'company_id': fields.many2one('res.company', 'Company', required=True),
1249         'name': fields.char('Line No', size=32, required=True),
1250         'notice': fields.char('Discount Notice', size=128),
1251         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True),
1252         'price_unit': fields.float(string='Unit Price', digits_compute=dp.get_precision('Account')),
1253         'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoS')),
1254         'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True),
1255         'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True),
1256         'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Account')),
1257         'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'),
1258         'create_date': fields.datetime('Creation Date', readonly=True),
1259     }
1260
1261     _defaults = {
1262         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'),
1263         'qty': lambda *a: 1,
1264         'discount': lambda *a: 0.0,
1265         'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
1266     }
1267
1268     def copy_data(self, cr, uid, id, default=None, context=None):
1269         if not default:
1270             default = {}
1271         default.update({
1272             'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line')
1273         })
1274         return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context)
1275
1276 import io, StringIO
1277
1278 class ean_wizard(osv.osv_memory):
1279     _name = 'pos.ean_wizard'
1280     _columns = {
1281         'ean13_pattern': fields.char('Reference', size=32, required=True, translate=True),
1282     }
1283     def sanitize_ean13(self, cr, uid, ids, context):
1284         for r in self.browse(cr,uid,ids):
1285             ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern)
1286             m = context.get('active_model')
1287             m_id =  context.get('active_id')
1288             self.pool[m].write(cr,uid,[m_id],{'ean13':ean13})
1289         return { 'type' : 'ir.actions.act_window_close' }
1290
1291 class product_product(osv.osv):
1292     _inherit = 'product.product'
1293
1294
1295     #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
1296     #    result = {}
1297     #    for obj in self.browse(cr, uid, ids, context=context):
1298     #        if not obj.product_image:
1299     #            result[obj.id] = False
1300     #            continue
1301
1302     #        image_stream = io.BytesIO(obj.product_image.decode('base64'))
1303     #        img = Image.open(image_stream)
1304     #        img.thumbnail((120, 100), Image.ANTIALIAS)
1305     #        img_stream = StringIO.StringIO()
1306     #        img.save(img_stream, "JPEG")
1307     #        result[obj.id] = img_stream.getvalue().encode('base64')
1308     #    return result
1309
1310     _columns = {
1311         '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."),
1312         '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."),
1313         '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'), 
1314         'to_weight' : fields.boolean('To Weight', help="Check if the product should be weighted (mainly used with self check-out interface)."),
1315     }
1316
1317     _defaults = {
1318         'to_weight' : False,
1319         'available_in_pos': True,
1320     }
1321
1322     def edit_ean(self, cr, uid, ids, context):
1323         return {
1324             'name': _("Assign a Custom EAN"),
1325             'type': 'ir.actions.act_window',
1326             'view_type': 'form',
1327             'view_mode': 'form',
1328             'res_model': 'pos.ean_wizard',
1329             'target' : 'new',
1330             'view_id': False,
1331             'context':context,
1332         }
1333
1334 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: