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