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