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