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