[FIX] Account : Correction on Residual amount calculation on payment by bank statement
[odoo/odoo.git] / addons / account / invoice.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import time
24 from operator import itemgetter
25
26 import netsvc
27 from osv import fields, osv
28 from osv.orm import except_orm
29 import pooler
30 from tools import config
31 from tools.translate import _
32
33 class fiscalyear_seq(osv.osv):
34     _name = "fiscalyear.seq"
35     _description = "Maintains Invoice sequences with Fiscal Year"
36     _rec_name = 'fiscalyear_id'
37     _columns = {
38         'journal_id': fields.many2one('account.journal', 'Journal'),
39         'fiscalyear_id': fields.many2one('account.fiscalyear', 'Fiscal Year',required=True),
40         'sequence_id':fields.many2one('ir.sequence', 'Sequence',required=True),
41     }
42
43 fiscalyear_seq()
44
45 class account_invoice(osv.osv):
46     def _amount_all(self, cr, uid, ids, name, args, context=None):
47         res = {}
48         for invoice in self.browse(cr,uid,ids, context=context):
49             res[invoice.id] = {
50                 'amount_untaxed': 0.0,
51                 'amount_tax': 0.0,
52                 'amount_total': 0.0
53             }
54             for line in invoice.invoice_line:
55                 res[invoice.id]['amount_untaxed'] += line.price_subtotal
56             for line in invoice.tax_line:
57                 res[invoice.id]['amount_tax'] += line.amount
58             res[invoice.id]['amount_total'] = res[invoice.id]['amount_tax'] + res[invoice.id]['amount_untaxed']
59         return res
60
61     def _get_journal(self, cr, uid, context):
62         if context is None:
63             context = {}
64         type_inv = context.get('type', 'out_invoice')
65         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
66         refund_journal = {'out_invoice': False, 'in_invoice': False, 'out_refund': True, 'in_refund': True}
67         journal_obj = self.pool.get('account.journal')
68         res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')), ('refund_journal', '=', refund_journal.get(type_inv, False))], limit=1)
69         if res:
70             return res[0]
71         else:
72             return False
73
74     def _get_currency(self, cr, uid, context):
75         user = pooler.get_pool(cr.dbname).get('res.users').browse(cr, uid, [uid])[0]
76         if user.company_id:
77             return user.company_id.currency_id.id
78         else:
79             return pooler.get_pool(cr.dbname).get('res.currency').search(cr, uid, [('rate','=',1.0)])[0]
80
81     def _get_journal_analytic(self, cr, uid, type_inv, context=None):
82         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
83         tt = type2journal.get(type_inv, 'sale')
84         result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
85         if not result:
86             raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s' !") % (tt,))
87         return result[0]
88
89     def _get_type(self, cr, uid, context=None):
90         if context is None:
91             context = {}
92         type = context.get('type', 'out_invoice')
93         return type
94
95     def _reconciled(self, cr, uid, ids, name, args, context):
96         res = {}
97         for id in ids:
98             res[id] = self.test_paid(cr, uid, [id])
99         return res
100
101     def _get_reference_type(self, cr, uid, context=None):
102         return [('none', _('Free Reference'))]
103
104     def _amount_residual(self, cr, uid, ids, name, args, context=None):
105         if context is None:
106             context = {}
107         res = {}
108         data_inv = self.browse(cr, uid, ids)
109         cur_obj = self.pool.get('res.currency')
110         for inv in data_inv:
111             debit = credit = 0.0
112             context_unreconciled = context.copy()
113             # If one of the invoice line is not in the currency of the invoice,
114             # we use the currency of the company to compute the residual amount.
115             # All the lines must use the same currency source (company or invoice)
116             fromcompany = False
117             
118             for lines in inv.move_lines:
119                 if lines.currency_id and lines.currency_id.id <> inv.currency_id.id:
120                     fromcompany = True
121                     break
122             for lines in inv.move_lines:
123                 # If currency conversion needed
124                 if fromcompany:
125                     context_unreconciled.update({'date':lines.date})
126                     # If amount currency setted, compute for debit and credit in company currency
127                     if lines.credit:
128                         credit += abs(cur_obj.compute(cr, uid, inv.company_id.currency_id.id, inv.currency_id.id, lines.credit, round=False,context=context_unreconciled))
129                     else:
130                         debit += abs(cur_obj.compute(cr, uid, inv.company_id.currency_id.id, inv.currency_id.id, lines.debit, round=False,context=context_unreconciled))
131                 else:
132                     if lines.amount_currency:
133                         debit += lines.debit > 0 and abs(lines.amount_currency) or 0.00
134                         credit += lines.credit > 0 and abs(lines.amount_currency) or 0.00
135                     else:
136                         debit += lines.debit or 0.00
137                         credit += lines.credit or 0.00
138             
139             if inv.type in ('out_invoice','in_refund'):
140                 amount = credit-debit
141             else:
142                 amount = debit-credit
143             
144             result = inv.amount_total - amount
145             res[inv.id] =  self.pool.get('res.currency').round(cr, uid, inv.currency_id, result)
146         return res
147
148     def _get_lines(self, cr, uid, ids, name, arg, context=None):
149         res = {}
150         for id in ids:
151             move_lines = self.move_line_id_payment_get(cr,uid,[id])
152             if not move_lines:
153                 res[id] = []
154                 continue
155             res[id] = []
156             data_lines = self.pool.get('account.move.line').browse(cr,uid,move_lines)
157             partial_ids = []# Keeps the track of ids where partial payments are done with payment terms
158             for line in data_lines:
159                 ids_line = []
160                 if line.reconcile_id:
161                     ids_line = line.reconcile_id.line_id
162                 elif line.reconcile_partial_id:
163                     ids_line = line.reconcile_partial_id.line_partial_ids
164                 l = map(lambda x: x.id, ids_line)
165                 partial_ids.append(line.id)
166                 res[id] =[x for x in l if x <> line.id and x not in partial_ids]
167         return res
168
169     def _get_invoice_line(self, cr, uid, ids, context=None):
170         result = {}
171         for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
172             result[line.invoice_id.id] = True
173         return result.keys()
174
175     def _get_invoice_tax(self, cr, uid, ids, context=None):
176         result = {}
177         for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
178             result[tax.invoice_id.id] = True
179         return result.keys()
180
181     def _compute_lines(self, cr, uid, ids, name, args, context=None):
182         result = {}
183         for invoice in self.browse(cr, uid, ids, context):
184             moves = self.move_line_id_payment_get(cr, uid, [invoice.id])
185             src = []
186             lines = []
187             for m in self.pool.get('account.move.line').browse(cr, uid, moves, context):
188                 temp_lines = []#Added temp list to avoid duplicate records
189                 if m.reconcile_id:
190                     temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
191                 elif m.reconcile_partial_id:
192                     temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
193                 lines += [x for x in temp_lines if x not in lines]
194                 src.append(m.id)
195             lines = filter(lambda x: x not in src, lines)
196             result[invoice.id] = lines
197         return result
198
199     def _get_invoice_from_line(self, cr, uid, ids, context={}):
200         move = {}
201         for line in self.pool.get('account.move.line').browse(cr, uid, ids):
202             if line.reconcile_partial_id:
203                 for line2 in line.reconcile_partial_id.line_partial_ids:
204                     move[line2.move_id.id] = True
205             if line.reconcile_id:
206                 for line2 in line.reconcile_id.line_id:
207                     move[line2.move_id.id] = True
208         invoice_ids = []
209         if move:
210             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
211         return invoice_ids
212
213     def _get_invoice_from_reconcile(self, cr, uid, ids, context={}):
214         move = {}
215         for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids):
216             for line in r.line_partial_ids:
217                 move[line.move_id.id] = True
218             for line in r.line_id:
219                 move[line.move_id.id] = True
220         
221         invoice_ids = []
222         if move:
223             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
224         return invoice_ids
225
226     _name = "account.invoice"
227     _description = 'Invoice'
228     _order = "number"
229     _columns = {
230         'name': fields.char('Description', size=64, select=True,readonly=True, states={'draft':[('readonly',False)]}),
231         'origin': fields.char('Origin', size=64, help="Reference of the document that produced this invoice."),
232         'type': fields.selection([
233             ('out_invoice','Customer Invoice'),
234             ('in_invoice','Supplier Invoice'),
235             ('out_refund','Customer Refund'),
236             ('in_refund','Supplier Refund'),
237             ],'Type', readonly=True, select=True),
238
239         'number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
240         'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
241         'reference_type': fields.selection(_get_reference_type, 'Reference Type',
242             required=True),
243         'comment': fields.text('Additional Information'),
244
245         'state': fields.selection([
246             ('draft','Draft'),
247             ('proforma','Pro-forma'),
248             ('proforma2','Pro-forma'),
249             ('open','Open'),
250             ('paid','Done'),
251             ('cancel','Cancelled')
252         ],'State', select=True, readonly=True),
253
254         'date_invoice': fields.date('Date Invoiced', states={'open':[('readonly',True)],'close':[('readonly',True)]}, help="Keep empty to use the current date"),
255         'date_due': fields.date('Due Date', states={'open':[('readonly',True)],'close':[('readonly',True)]},
256             help="If you use payment terms, the due date will be computed automatically at the generation "\
257                 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. The payment term may compute several due dates, for example 50% now, 50% in one month."),
258         'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
259         'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}),
260         'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
261         'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
262             help="If you use payment terms, the due date will be computed automatically at the generation "\
263                 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
264                 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
265         'period_id': fields.many2one('account.period', 'Force Period', domain=[('state','<>','done')], help="Keep empty to use the period of the validation(invoice) date.", readonly=True, states={'draft':[('readonly',False)]}),
266
267         'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
268         'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
269         'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
270
271         'move_id': fields.many2one('account.move', 'Invoice Movement', readonly=True, help="Link to the automatically generated account moves."),
272         'amount_untaxed': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])),string='Untaxed',
273             store={
274                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
275                 'account.invoice.tax': (_get_invoice_tax, None, 20),
276                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount'], 20),
277             },
278             multi='all'),
279         'amount_tax': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Tax',
280             store={
281                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
282                 'account.invoice.tax': (_get_invoice_tax, None, 20),
283                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount'], 20),
284             },
285             multi='all'),
286         'amount_total': fields.function(_amount_all, method=True, digits=(16, int(config['price_accuracy'])), string='Total',
287             store={
288                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
289                 'account.invoice.tax': (_get_invoice_tax, None, 20),
290                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount'], 20),
291             },
292             multi='all'),
293         'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
294         'journal_id': fields.many2one('account.journal', 'Journal', required=True,readonly=True, states={'draft':[('readonly',False)]}),
295         'company_id': fields.many2one('res.company', 'Company', required=True),
296         'check_total': fields.float('Total', digits=(16, int(config['price_accuracy'])), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
297         'reconciled': fields.function(_reconciled, method=True, string='Paid/Reconciled', type='boolean',
298             store={
299                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
300                 'account.move.line': (_get_invoice_from_line, None, 50),
301                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
302             }, help="The account moves of the invoice have been reconciled with account moves of the payment(s)."),
303         'partner_bank': fields.many2one('res.partner.bank', 'Bank Account',
304             help='The bank account to pay to or to be paid from'),
305         'move_lines':fields.function(_get_lines , method=True,type='many2many' , relation='account.move.line',string='Move Lines'),
306         'residual': fields.function(_amount_residual, method=True, digits=(16, int(config['price_accuracy'])),string='Residual',
307             store={
308                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 50),
309                 'account.invoice.tax': (_get_invoice_tax, None, 50),
310                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount'], 50),
311                 'account.move.line': (_get_invoice_from_line, None, 50),
312                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
313             },
314             help="Remaining amount due."),
315         'payment_ids': fields.function(_compute_lines, method=True, relation='account.move.line', type="many2many", string='Payments'),
316         'move_name': fields.char('Account Move', size=64),
317         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position')
318     }
319     _defaults = {
320         'type': _get_type,
321         #'date_invoice': lambda *a: time.strftime('%Y-%m-%d'),
322         'state': lambda *a: 'draft',
323         'journal_id': _get_journal,
324         'currency_id': _get_currency,
325         'company_id': lambda self, cr, uid, context: \
326                 self.pool.get('res.users').browse(cr, uid, uid,
327                     context=context).company_id.id,
328         'reference_type': lambda *a: 'none',
329         'check_total': lambda *a: 0.0,
330     }
331     
332     def create(self, cr, uid, vals, context={}):
333         try:
334             res = super(account_invoice, self).create(cr, uid, vals, context)
335             return res
336         except Exception,e:
337             if '"journal_id" viol' in e.args[0]:
338                 raise except_orm(_('Configuration Error!'),
339                      _('There is no Accounting Journal of type Sale/Purchase defined!'))
340             else:
341                 raise except_orm(_('UnknownError'), str(e))
342             
343     def unlink(self, cr, uid, ids, context=None):
344         invoices = self.read(cr, uid, ids, ['state'])
345         unlink_ids = []
346         for t in invoices:
347             if t['state'] in ('draft', 'cancel'):
348                 unlink_ids.append(t['id'])
349             else:
350                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete invoice(s) that are already opened or paid !'))
351         osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
352         return True
353
354 #   def get_invoice_address(self, cr, uid, ids):
355 #       res = self.pool.get('res.partner').address_get(cr, uid, [part], ['invoice'])
356 #       return [{}]
357
358     def onchange_partner_id(self, cr, uid, ids, type, partner_id,
359             date_invoice=False, payment_term=False, partner_bank=False):
360         invoice_addr_id = False
361         contact_addr_id = False
362         partner_payment_term = False
363         acc_id = False
364         bank_id = False
365         fiscal_position = False
366
367         opt = [('uid', str(uid))]
368         if partner_id:
369
370             opt.insert(0, ('id', partner_id))
371             res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
372             contact_addr_id = res['contact']
373             invoice_addr_id = res['invoice']
374             p = self.pool.get('res.partner').browse(cr, uid, partner_id)
375             if type in ('out_invoice', 'out_refund'):
376                 acc_id = p.property_account_receivable.id
377             else:
378                 acc_id = p.property_account_payable.id
379             fiscal_position = p.property_account_position and p.property_account_position.id or False
380             partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
381             if p.bank_ids:
382                 bank_id = p.bank_ids[0].id
383
384         result = {'value': {
385             'address_contact_id': contact_addr_id,
386             'address_invoice_id': invoice_addr_id,
387             'account_id': acc_id,
388             'payment_term': partner_payment_term,
389             'fiscal_position': fiscal_position
390             }
391         }
392
393         if type in ('in_invoice', 'in_refund'):
394             result['value']['partner_bank'] = bank_id
395
396         if partner_bank != bank_id:
397             to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
398             result['value'].update(to_update['value'])
399         return result
400
401     def onchange_currency_id(self, cr, uid, ids, curr_id):
402         return {}
403
404     def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
405         if not payment_term_id:
406             return {}
407         res={}
408         pt_obj= self.pool.get('account.payment.term')
409         if not date_invoice :
410             date_invoice = time.strftime('%Y-%m-%d')
411
412         pterm_list = pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
413
414         if pterm_list:
415             pterm_list = [line[0] for line in pterm_list]
416             pterm_list.sort()
417             res= {'value':{'date_due': pterm_list[-1]}}
418         else:
419              raise osv.except_osv(_('Data Insufficient !'), _('The Payment Term of Supplier does not have Payment Term Lines(Computation) defined !'))
420
421         return res
422
423     def onchange_invoice_line(self, cr, uid, ids, lines):
424         return {}
425
426     def onchange_partner_bank(self, cursor, user, ids, partner_bank):
427         return {'value': {}}
428
429     # go from canceled state to draft state
430     def action_cancel_draft(self, cr, uid, ids, *args):
431         self.write(cr, uid, ids, {'state':'draft'})
432         wf_service = netsvc.LocalService("workflow")
433         for inv_id in ids:
434             wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
435         return True
436
437     # Workflow stuff
438     #################
439
440     # return the ids of the move lines which has the same account than the invoice
441     # whose id is in ids
442     def move_line_id_payment_get(self, cr, uid, ids, *args):
443         res = []
444         if not ids: return res
445         cr.execute('SELECT l.id '\
446                    'FROM account_move_line l '\
447                    'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
448                    'WHERE i.id IN %s '\
449                    'AND l.account_id=i.account_id',
450                    (tuple(ids),))
451         res = map(itemgetter(0), cr.fetchall())
452         return res
453
454     def copy(self, cr, uid, id, default=None, context=None):
455         if default is None:
456             default = {}
457         default = default.copy()
458         default.update({'state':'draft', 'number':False, 'move_id':False, 'move_name':False,})
459         if 'date_invoice' not in default:
460             default['date_invoice'] = False
461         if 'date_due' not in default:
462             default['date_due'] = False
463         return super(account_invoice, self).copy(cr, uid, id, default, context)
464
465     def test_paid(self, cr, uid, ids, *args):
466         res = self.move_line_id_payment_get(cr, uid, ids)
467         if not res:
468             return False
469         ok = True
470         for id in res:
471             cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
472             ok = ok and  bool(cr.fetchone()[0])
473         return ok
474
475     def button_reset_taxes(self, cr, uid, ids, context=None):
476         if not context:
477             context = {}
478         ctx = context.copy()
479         ait_obj = self.pool.get('account.invoice.tax')
480         for id in ids:
481             cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s", (id,))
482             partner = self.browse(cr, uid, id,context=ctx).partner_id
483             if partner.lang:
484                 ctx.update({'lang': partner.lang})
485             for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
486                 ait_obj.create(cr, uid, taxe)
487          # Update the stored value (fields.function), so we write to trigger recompute
488         self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)    
489 #        self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
490         return True
491
492     def button_compute(self, cr, uid, ids, context=None, set_total=False):
493         self.button_reset_taxes(cr, uid, ids, context)
494         for inv in self.browse(cr, uid, ids):
495             if set_total:
496                 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
497         return True
498
499     def _convert_ref(self, cr, uid, ref):
500         return (ref or '').replace('/','')
501
502     def _get_analytic_lines(self, cr, uid, id):
503         inv = self.browse(cr, uid, [id])[0]
504         cur_obj = self.pool.get('res.currency')
505
506         company_currency = inv.company_id.currency_id.id
507         if inv.type in ('out_invoice', 'in_refund'):
508             sign = 1
509         else:
510             sign = -1
511
512         iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id)
513         for il in iml:
514             if il['account_analytic_id']:
515                 if inv.type in ('in_invoice', 'in_refund'):
516                     ref = inv.reference
517                 else:
518                     ref = self._convert_ref(cr, uid, inv.number)
519                 il['analytic_lines'] = [(0,0, {
520                     'name': il['name'],
521                     'date': inv['date_invoice'],
522                     'account_id': il['account_analytic_id'],
523                     'unit_amount': il['quantity'],
524                     'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
525                     'product_id': il['product_id'],
526                     'product_uom_id': il['uos_id'],
527                     'general_account_id': il['account_id'],
528                     'journal_id': self._get_journal_analytic(cr, uid, inv.type),
529                     'ref': ref,
530                 })]
531         return iml
532
533     def action_date_assign(self, cr, uid, ids, *args):
534         for inv in self.browse(cr, uid, ids):
535             res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
536             if res and res['value']:
537                 self.write(cr, uid, [inv.id], res['value'])
538         return True
539     
540     def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
541         """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
542         Hook method to be overridden in additional modules to verify and possibly alter the 
543         move lines to be created by an invoice, for special cases.
544         :param invoice_browse: browsable record of the invoice that is generating the move lines
545         :param move_lines: list of dictionaries with the account.move.lines (as for create())
546         :return: the (possibly updated) final move_lines to create for this invoice 
547         """
548         return move_lines
549
550     def action_move_create(self, cr, uid, ids, *args):
551         ait_obj = self.pool.get('account.invoice.tax')
552         cur_obj = self.pool.get('res.currency')
553         context = {}
554         for inv in self.browse(cr, uid, ids):
555             if inv.move_id:
556                 continue
557
558             if not inv.date_invoice:
559                 self.write(cr, uid, [inv.id], {'date_invoice':time.strftime('%Y-%m-%d')})
560             company_currency = inv.company_id.currency_id.id
561             # create the analytical lines
562             line_ids = self.read(cr, uid, [inv.id], ['invoice_line'])[0]['invoice_line']
563             # one move line per invoice line
564             iml = self._get_analytic_lines(cr, uid, inv.id)
565             # check if taxes are all computed
566
567             context.update({'lang': inv.partner_id.lang})
568             compute_taxes = ait_obj.compute(cr, uid, inv.id, context=context)
569             if not inv.tax_line:
570                 for tax in compute_taxes.values():
571                     ait_obj.create(cr, uid, tax)
572             else:
573                 tax_key = []
574                 for tax in inv.tax_line:
575                     if tax.manual:
576                         continue
577                     key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
578                     tax_key.append(key)
579                     if not key in compute_taxes:
580                         raise osv.except_osv(_('Warning !'), _('Global taxes defined, but are not in invoice lines !'))
581                     base = compute_taxes[key]['base']
582                     if abs(base - tax.base) > inv.company_id.currency_id.rounding:
583                         raise osv.except_osv(_('Warning !'), _('Tax base different !\nClick on compute to update tax base'))
584                 for key in compute_taxes:
585                     if not key in tax_key:
586                         raise osv.except_osv(_('Warning !'), _('Taxes missing !'))
587
588             if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
589                 raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
590
591             # one move line per tax line
592             iml += ait_obj.move_line_get(cr, uid, inv.id)
593
594             if inv.type in ('in_invoice', 'in_refund'):
595                 ref = inv.reference
596             else:
597                 ref = self._convert_ref(cr, uid, inv.number)
598
599             diff_currency_p = inv.currency_id.id <> company_currency
600             # create one move line for the total and possibly adjust the other lines amount
601             total = 0
602             total_currency = 0
603             for i in iml:
604                 if inv.currency_id.id != company_currency:
605                     i['currency_id'] = inv.currency_id.id
606                     i['amount_currency'] = i['price']
607                     i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
608                             company_currency, i['price'],
609                             context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
610                 else:
611                     i['amount_currency'] = False
612                     i['currency_id'] = False
613                 i['ref'] = ref
614                 if inv.type in ('out_invoice','in_refund'):
615                     total += i['price']
616                     total_currency += i['amount_currency'] or i['price']
617                     i['price'] = - i['price']
618                 else:
619                     total -= i['price']
620                     total_currency -= i['amount_currency'] or i['price']
621             acc_id = inv.account_id.id
622
623             name = inv['name'] or '/'
624             totlines = False
625             if inv.payment_term:
626                 totlines = self.pool.get('account.payment.term').compute(cr,
627                         uid, inv.payment_term.id, total, inv.date_invoice or False)
628             if totlines:
629                 res_amount_currency = total_currency
630                 i = 0
631                 for t in totlines:
632                     if inv.currency_id.id != company_currency:
633                         amount_currency = cur_obj.compute(cr, uid,
634                                 company_currency, inv.currency_id.id, t[1])
635                     else:
636                         amount_currency = False
637
638                     # last line add the diff
639                     res_amount_currency -= amount_currency or 0
640                     i += 1
641                     if i == len(totlines):
642                         amount_currency += res_amount_currency
643
644                     iml.append({
645                         'type': 'dest',
646                         'name': name,
647                         'price': t[1],
648                         'account_id': acc_id,
649                         'date_maturity': t[0],
650                         'amount_currency': diff_currency_p \
651                                 and  amount_currency or False,
652                         'currency_id': diff_currency_p \
653                                 and inv.currency_id.id or False,
654                         'ref': ref,
655                     })
656             else:
657                 iml.append({
658                     'type': 'dest',
659                     'name': name,
660                     'price': total,
661                     'account_id': acc_id,
662                     'date_maturity' : inv.date_due or False,
663                     'amount_currency': diff_currency_p \
664                             and total_currency or False,
665                     'currency_id': diff_currency_p \
666                             and inv.currency_id.id or False,
667                     'ref': ref
668             })
669
670             date = inv.date_invoice or time.strftime('%Y-%m-%d')
671             part = inv.partner_id.id
672
673             line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context={})) ,iml)
674
675             if inv.journal_id.group_invoice_lines:
676                 line2 = {}
677                 for x, y, l in line:
678                     tmp = str(l['account_id'])
679                     tmp += '-'+str(l.get('tax_code_id',"False"))
680                     tmp += '-'+str(l.get('product_id',"False"))
681                     tmp += '-'+str(l.get('analytic_account_id',"False"))
682                     tmp += '-'+str(l.get('date_maturity',"False"))
683                     
684                     if tmp in line2:
685                         am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
686                         line2[tmp]['debit'] = (am > 0) and am or 0.0
687                         line2[tmp]['credit'] = (am < 0) and -am or 0.0
688                         line2[tmp]['tax_amount'] += l['tax_amount']
689                         line2[tmp]['analytic_lines'] += l['analytic_lines']
690                     else:
691                         line2[tmp] = l
692                 line = []
693                 for key, val in line2.items():
694                     line.append((0,0,val))
695
696             journal_id = inv.journal_id.id #self._get_journal(cr, uid, {'type': inv['type']})
697             journal = self.pool.get('account.journal').browse(cr, uid, journal_id)
698             if journal.centralisation:
699                 raise osv.except_osv(_('UserError'),
700                         _('Cannot create invoice move on centralised journal'))
701
702             line = self.finalize_invoice_move_lines(cr, uid, inv, line)
703
704             move = {'ref': inv.number, 'line_id': line, 'journal_id': journal_id, 'date': date}
705             period_id=inv.period_id and inv.period_id.id or False
706             if not period_id:
707                 period_ids= self.pool.get('account.period').search(cr,uid,[('date_start','<=',inv.date_invoice or time.strftime('%Y-%m-%d')),('date_stop','>=',inv.date_invoice or time.strftime('%Y-%m-%d'))])
708                 if len(period_ids):
709                     period_id=period_ids[0]
710             if period_id:
711                 move['period_id'] = period_id
712                 for i in line:
713                     i[2]['period_id'] = period_id
714
715             move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
716             new_move_name = self.pool.get('account.move').browse(cr, uid, move_id).name
717             # make the invoice point to that move
718             self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name})
719             # Pass invoice in context in method post: used if you want to get the same
720             # account move reference when creating the same invoice after a cancelled one:
721             self.pool.get('account.move').post(cr, uid, [move_id], context={'invoice':inv})
722         self._log_event(cr, uid, ids)
723         return True
724
725     def line_get_convert(self, cr, uid, x, part, date, context=None):
726         return {
727             'date_maturity': x.get('date_maturity', False),
728             'partner_id':part,
729             'name':x['name'][:64],
730             'date': date,
731             'debit':x['price']>0 and x['price'],
732             'credit':x['price']<0 and -x['price'],
733             'account_id':x['account_id'],
734             'analytic_lines':x.get('analytic_lines', []),
735             'amount_currency':x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
736             'currency_id':x.get('currency_id', False),
737             'tax_code_id': x.get('tax_code_id', False),
738             'tax_amount': x.get('tax_amount', False),
739             'ref':x.get('ref',False),
740             'quantity':x.get('quantity',1.00),
741             'product_id':x.get('product_id', False),
742             'product_uom_id':x.get('uos_id',False),
743             'analytic_account_id':x.get('account_analytic_id',False),
744         }
745
746     def action_number(self, cr, uid, ids, *args):
747         cr.execute('SELECT id, type, number, move_id, reference ' \
748                    'FROM account_invoice ' \
749                    'WHERE id IN %s',
750                    (tuple(ids),))
751         obj_inv = self.browse(cr, uid, ids)[0]
752         for (id, invtype, number, move_id, reference) in cr.fetchall():
753             if not number:
754                 tmp_context = {
755                     'fiscalyear_id' : obj_inv.period_id.fiscalyear_id.id,
756                 }
757                 if obj_inv.journal_id.invoice_sequence_id:
758                     sid = obj_inv.journal_id.invoice_sequence_id.id
759                     number = self.pool.get('ir.sequence').get_id(cr, uid, sid, 'id=%s', context=tmp_context)
760                 else:
761                     number = self.pool.get('ir.sequence').get_id(cr, uid,
762                                                                  'account.invoice.' + invtype,
763                                                                  'code=%s',
764                                                                  context=tmp_context)
765                 if not number:
766                     raise osv.except_osv(_('Warning !'), _('There is no active invoice sequence defined for the journal !'))
767
768                 if invtype in ('in_invoice', 'in_refund'):
769                     ref = reference
770                 else:
771                     ref = self._convert_ref(cr, uid, number)
772                 cr.execute('UPDATE account_invoice SET number=%s ' \
773                         'WHERE id=%s', (number, id))
774                 cr.execute('UPDATE account_move SET ref=%s ' \
775                         'WHERE id=%s AND (ref is null OR ref = \'\')',
776                         (ref, move_id))
777                 cr.execute('UPDATE account_move_line SET ref=%s ' \
778                         'WHERE move_id=%s AND (ref is null OR ref = \'\')',
779                         (ref, move_id))
780                 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
781                         'FROM account_move_line ' \
782                         'WHERE account_move_line.move_id = %s ' \
783                             'AND account_analytic_line.move_id = account_move_line.id',
784                             (ref, move_id))
785         return True
786
787     def action_cancel(self, cr, uid, ids, *args):
788         account_move_obj = self.pool.get('account.move')
789         invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
790         for i in invoices:
791             if i['move_id']:
792                 account_move_obj.button_cancel(cr, uid, [i['move_id'][0]])
793                 # delete the move this invoice was pointing to
794                 # Note that the corresponding move_lines and move_reconciles
795                 # will be automatically deleted too
796                 account_move_obj.unlink(cr, uid, [i['move_id'][0]])
797             if i['payment_ids']:
798                 account_move_line_obj = self.pool.get('account.move.line')
799                 pay_ids = account_move_line_obj.browse(cr, uid , i['payment_ids'])
800                 for move_line in pay_ids:
801                     if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
802                         raise osv.except_osv(_('Error !'), _('You cannot cancel the Invoice which is Partially Paid! You need to unreconcile concerned payment entries!'))
803
804         self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
805         self._log_event(cr, uid, ids,-1.0, 'Cancel Invoice')
806         return True
807
808     ###################
809
810     def list_distinct_taxes(self, cr, uid, ids):
811         invoices = self.browse(cr, uid, ids)
812         taxes = {}
813         for inv in invoices:
814             for tax in inv.tax_line:
815                 if not tax['name'] in taxes:
816                     taxes[tax['name']] = {'name': tax['name']}
817         return taxes.values()
818
819     def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
820         invs = self.read(cr, uid, ids, ['type','partner_id','amount_untaxed'])
821         for inv in invs:
822             part=inv['partner_id'] and inv['partner_id'][0]
823             pc = pr = 0.0
824             cr.execute('select sum(quantity*price_unit) from account_invoice_line where invoice_id=%s', (inv['id'],))
825             total = inv['amount_untaxed']
826             if inv['type'] in ('in_invoice','in_refund'):
827                 partnertype='supplier'
828                 eventtype = 'purchase'
829                 pc = total*factor
830             else:
831                 partnertype = 'customer'
832                 eventtype = 'sale'
833                 pr = total*factor
834             if self.pool.get('res.partner.event.type').check(cr, uid, 'invoice_open'):
835                 self.pool.get('res.partner.event').create(cr, uid, {'name':'Invoice: '+name, 'som':False, 'description':name+' '+str(inv['id']), 'document':name, 'partner_id':part, 'date':time.strftime('%Y-%m-%d %H:%M:%S'), 'canal_id':False, 'user_id':uid, 'partner_type':partnertype, 'probability':1.0, 'planned_revenue':pr, 'planned_cost':pc, 'type':eventtype})
836         return len(invs)
837
838     def name_get(self, cr, uid, ids, context=None):
839         if not len(ids):
840             return []
841         types = {
842                 'out_invoice': 'CI: ',
843                 'in_invoice': 'SI: ',
844                 'out_refund': 'OR: ',
845                 'in_refund': 'SR: ',
846                 }
847         return [(r['id'], types[r['type']]+(r['number'] or '')+' '+(r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
848
849     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
850         if not args:
851             args=[]
852         if context is None:
853             context={}
854         ids = []
855         if name:
856             ids = self.search(cr, user, [('number','=',name)]+ args, limit=limit, context=context)
857         if not ids:
858             ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context)
859         return self.name_get(cr, user, ids, context)
860
861     def _refund_cleanup_lines(self, cr, uid, lines):
862         for line in lines:
863             del line['id']
864             del line['invoice_id']
865             if 'account_id' in line:
866                 line['account_id'] = line.get('account_id', False) and line['account_id'][0]
867             if 'product_id' in line:
868                 line['product_id'] = line.get('product_id', False) and line['product_id'][0]
869             if 'uos_id' in line:
870                 line['uos_id'] = line.get('uos_id', False) and line['uos_id'][0]
871             if 'invoice_line_tax_id' in line:
872                 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
873             if 'account_analytic_id' in line:
874                 line['account_analytic_id'] = line.get('account_analytic_id', False) and line['account_analytic_id'][0]
875             if 'tax_code_id' in line :
876                 if isinstance(line['tax_code_id'],tuple)  and len(line['tax_code_id']) >0 :
877                     line['tax_code_id'] = line['tax_code_id'][0]
878             if 'base_code_id' in line :
879                 if isinstance(line['base_code_id'],tuple)  and len(line['base_code_id']) >0 :
880                     line['base_code_id'] = line['base_code_id'][0]
881         return map(lambda x: (0,0,x), lines)
882
883     def refund(self, cr, uid, ids, date=None, period_id=None, description=None):
884         invoices = self.read(cr, uid, ids, ['name', 'type', 'number', 'reference', 'comment', 'date_due', 'partner_id', 'address_contact_id', 'address_invoice_id', 'partner_contact', 'partner_insite', 'partner_ref', 'payment_term', 'account_id', 'currency_id', 'invoice_line', 'tax_line', 'journal_id'])
885
886         new_ids = []
887         for invoice in invoices:
888             del invoice['id']
889
890             type_dict = {
891                 'out_invoice': 'out_refund', # Customer Invoice
892                 'in_invoice': 'in_refund',   # Supplier Invoice
893                 'out_refund': 'out_invoice', # Customer Refund
894                 'in_refund': 'in_invoice',   # Supplier Refund
895             }
896
897
898             invoice_lines = self.pool.get('account.invoice.line').read(cr, uid, invoice['invoice_line'])
899             invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
900
901             tax_lines = self.pool.get('account.invoice.tax').read(cr, uid, invoice['tax_line'])
902             tax_lines = filter(lambda l: l['manual'], tax_lines)
903             tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
904             if not date :
905                 date = time.strftime('%Y-%m-%d')
906             invoice.update({
907                 'type': type_dict[invoice['type']],
908                 'date_invoice': date,
909                 'state': 'draft',
910                 'number': False,
911                 'invoice_line': invoice_lines,
912                 'tax_line': tax_lines
913             })
914             if period_id :
915                 invoice.update({
916                     'period_id': period_id,
917                 })
918             if description :
919                 invoice.update({
920                     'name': description,
921                 })
922             # take the id part of the tuple returned for many2one fields
923             for field in ('address_contact_id', 'address_invoice_id', 'partner_id',
924                     'account_id', 'currency_id', 'payment_term', 'journal_id'):
925                 invoice[field] = invoice[field] and invoice[field][0]
926             # create the new invoice
927             new_ids.append(self.create(cr, uid, invoice))
928         return new_ids
929
930     def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id, writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
931         if context is None:
932             context = {}
933         #TODO check if we can use different period for payment and the writeoff line
934         assert len(ids)==1, "Can only pay one invoice at a time"
935         invoice = self.browse(cr, uid, ids[0])
936         src_account_id = invoice.account_id.id
937         # Take the seq as name for move
938         types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
939         direction = types[invoice.type]
940         #take the choosen date
941         if 'date_p' in context and context['date_p']:
942             date=context['date_p']
943         else:
944             date=time.strftime('%Y-%m-%d')
945             
946         # Take the amount in currency and the currency of the payment
947         if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
948             amount_currency = context['amount_currency']
949             currency_id = context['currency_id']
950         else:
951             amount_currency = False
952             currency_id = False
953         
954         if invoice.type in ('in_invoice', 'in_refund'):
955             ref = invoice.reference
956         else:
957             ref = self._convert_ref(cr, uid, invoice.number)        
958         # Pay attention to the sign for both debit/credit AND amount_currency
959         l1 = {
960             'debit': direction * pay_amount>0 and direction * pay_amount,
961             'credit': direction * pay_amount<0 and - direction * pay_amount,
962             'account_id': src_account_id,
963             'partner_id': invoice.partner_id.id,
964             'ref':ref,
965             'date': date,
966             'currency_id':currency_id,
967             'amount_currency':amount_currency and direction * amount_currency or 0.0,
968         }
969         l2 = {
970             'debit': direction * pay_amount<0 and - direction * pay_amount,
971             'credit': direction * pay_amount>0 and direction * pay_amount,
972             'account_id': pay_account_id,
973             'partner_id': invoice.partner_id.id,
974             'ref':ref,
975             'date': date,
976             'currency_id':currency_id,
977             'amount_currency':amount_currency and - direction * amount_currency or 0.0,
978         }
979
980         if not name:
981             name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
982         l1['name'] = name
983         l2['name'] = name
984
985         lines = [(0, 0, l1), (0, 0, l2)]
986         move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
987         move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
988
989         line_ids = []
990         total = 0.0
991         line = self.pool.get('account.move.line')
992         cr.execute('SELECT id FROM account_move_line '\
993                    'WHERE move_id in %s',
994                    ((move_id, invoice.move_id.id),))
995         lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
996
997         for l in lines+invoice.payment_ids:
998             if l.account_id.id==src_account_id:
999                 line_ids.append(l.id)
1000                 total += (l.debit or 0.0) - (l.credit or 0.0)
1001
1002         if (not round(total,int(config['price_accuracy']))) or writeoff_acc_id:
1003             self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1004         else:
1005             self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1006
1007         # Update the stored value (fields.function), so we write to trigger recompute
1008         self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1009         return True
1010 account_invoice()
1011
1012 class account_invoice_line(osv.osv):
1013     def _amount_line(self, cr, uid, ids, prop, unknow_none,unknow_dict):
1014         res = {}
1015         cur_obj=self.pool.get('res.currency')
1016         for line in self.browse(cr, uid, ids):
1017             if line.invoice_id:
1018                 res[line.id] = line.price_unit * line.quantity * (1-(line.discount or 0.0)/100.0)
1019                 cur = line.invoice_id.currency_id
1020                 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1021             else:
1022                 res[line.id] = round(line.price_unit * line.quantity * (1-(line.discount or 0.0)/100.0),int(config['price_accuracy']))
1023         return res
1024
1025
1026     def _price_unit_default(self, cr, uid, context=None):
1027         if context is None:
1028             context = {}
1029         if 'check_total' in context:
1030             t = context['check_total']
1031             for l in context.get('invoice_line', {}):
1032                 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1033                     tax_obj = self.pool.get('account.tax')
1034                     p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1035                     t = t - (p * l[2].get('quantity'))
1036                     taxes = l[2].get('invoice_line_tax_id')
1037                     if len(taxes[0]) >= 3 and taxes[0][2]:
1038                         taxes=tax_obj.browse(cr, uid, taxes[0][2])
1039                         for tax in tax_obj.compute(cr, uid, taxes, p,l[2].get('quantity'), context.get('address_invoice_id', False), l[2].get('product_id', False), context.get('partner_id', False)):
1040                             t = t - tax['amount']
1041             return t
1042         return 0
1043
1044     _name = "account.invoice.line"
1045     _description = "Invoice line"
1046     _columns = {
1047         'name': fields.char('Description', size=256, required=True),
1048         'origin': fields.char('Origin', size=256, help="Reference of the document that produced this invoice."),
1049         'invoice_id': fields.many2one('account.invoice', 'Invoice Ref', ondelete='cascade', select=True),
1050         'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1051         'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1052         'account_id': fields.many2one('account.account', 'Account', required=True, domain=[('type','<>','view'), ('type', '<>', 'closed')], help="The income or expense account related to the selected product."),
1053         'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
1054         'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',store=True, type="float", digits=(16, int(config['price_accuracy']))),
1055         'quantity': fields.float('Quantity', required=True),
1056         'discount': fields.float('Discount (%)', digits=(16, int(config['price_accuracy']))),
1057         'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1058         'note': fields.text('Notes'),
1059         'account_analytic_id':  fields.many2one('account.analytic.account', 'Analytic Account'),
1060     }
1061     _defaults = {
1062         'quantity': lambda *a: 1,
1063         'discount': lambda *a: 0.0,
1064         'price_unit': _price_unit_default,
1065     }
1066
1067     def product_id_change_unit_price_inv(self, cr, uid, tax_id, price_unit, qty, address_invoice_id, product, partner_id, context=None):
1068         tax_obj = self.pool.get('account.tax')
1069         if price_unit:
1070             taxes = tax_obj.browse(cr, uid, tax_id)
1071             for tax in tax_obj.compute_inv(cr, uid, taxes, price_unit, qty, address_invoice_id, product, partner_id):
1072                 price_unit = price_unit - tax['amount']
1073         return {'price_unit': price_unit,'invoice_line_tax_id': tax_id}
1074
1075     def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, address_invoice_id=False, context=None):
1076         if context is None:
1077             context = {}
1078         if not partner_id:
1079             raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1080         if not product:
1081             if type in ('in_invoice', 'in_refund'):
1082                 return {'value':{}, 'domain':{'product_uom':[]}}
1083             else:
1084                 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1085         part = self.pool.get('res.partner').browse(cr, uid, partner_id)
1086         fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1087
1088         if part.lang:
1089             context.update({'lang': part.lang})
1090         result = {}
1091         res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1092
1093         if type in ('out_invoice','out_refund'):
1094             a =  res.product_tmpl_id.property_account_income.id
1095             if not a:
1096                 a = res.categ_id.property_account_income_categ.id
1097         else:
1098             a =  res.product_tmpl_id.property_account_expense.id
1099             if not a:
1100                 a = res.categ_id.property_account_expense_categ.id
1101
1102         a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
1103         if a:
1104             result['account_id'] = a
1105
1106         taxep=None
1107         tax_obj = self.pool.get('account.tax')
1108         if type in ('out_invoice', 'out_refund'):
1109             taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid,a).tax_ids or False)
1110             tax_id = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1111         else:
1112             taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid,a).tax_ids or False)
1113             tax_id = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1114         if type in ('in_invoice', 'in_refund'):
1115             to_update = self.product_id_change_unit_price_inv(cr, uid, tax_id, price_unit or res.standard_price, qty, address_invoice_id, product, partner_id, context=context)
1116             result.update(to_update)
1117         else:
1118             result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1119
1120         if not name:
1121             result['name'] = res.partner_ref
1122
1123         domain = {}
1124         result['uos_id'] = uom or res.uom_id.id or False
1125         if result['uos_id']:
1126             res2 = res.uom_id.category_id.id
1127             if res2 :
1128                 domain = {'uos_id':[('category_id','=',res2 )]}
1129         return {'value':result, 'domain':domain}
1130
1131     def move_line_get(self, cr, uid, invoice_id, context=None):
1132         res = []
1133         tax_grouped = {}
1134         tax_obj = self.pool.get('account.tax')
1135         cur_obj = self.pool.get('res.currency')
1136         ait_obj = self.pool.get('account.invoice.tax')
1137         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id)
1138         company_currency = inv.company_id.currency_id.id
1139         cur = inv.currency_id
1140
1141         for line in inv.invoice_line:
1142             mres = self.move_line_get_item(cr, uid, line, context)
1143             if not mres:
1144                 continue
1145             res.append(mres)
1146             tax_code_found= False
1147             for tax in tax_obj.compute(cr, uid, line.invoice_line_tax_id,
1148                     (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1149                     line.quantity, inv.address_invoice_id.id, line.product_id,
1150                     inv.partner_id):
1151
1152                 if inv.type in ('out_invoice', 'in_invoice'):
1153                     tax_code_id = tax['base_code_id']
1154                     tax_amount = line.price_subtotal * tax['base_sign']
1155                 else:
1156                     tax_code_id = tax['ref_base_code_id']
1157                     tax_amount = line.price_subtotal * tax['ref_base_sign']
1158
1159                 if tax_code_found:
1160                     if not tax_code_id:
1161                         continue
1162                     res.append(self.move_line_get_item(cr, uid, line, context))
1163                     res[-1]['price'] = 0.0
1164                     res[-1]['account_analytic_id'] = False
1165                 elif not tax_code_id:
1166                     continue
1167                 tax_code_found = True
1168
1169                 res[-1]['tax_code_id'] = tax_code_id
1170                 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1171         return res
1172
1173     def move_line_get_item(self, cr, uid, line, context=None):
1174         return {
1175             'type':'src',
1176             'name': line.name[:64],
1177             'price_unit':line.price_unit,
1178             'quantity':line.quantity,
1179             'price':line.price_subtotal,
1180             'account_id':line.account_id.id,
1181             'product_id':line.product_id.id,
1182             'uos_id':line.uos_id.id,
1183             'account_analytic_id':line.account_analytic_id.id,
1184             'taxes':line.invoice_line_tax_id,
1185         }
1186     #
1187     # Set the tax field according to the account and the fiscal position
1188     #
1189     def onchange_account_id(self, cr, uid, ids, fposition_id, account_id):
1190         if not account_id:
1191             return {}
1192         taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1193         fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1194         res = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1195         r = {'value':{'invoice_line_tax_id': res}}
1196         return r
1197 account_invoice_line()
1198
1199 class account_invoice_tax(osv.osv):
1200     _name = "account.invoice.tax"
1201     _description = "Invoice Tax"
1202     _columns = {
1203         'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1204         'name': fields.char('Tax Description', size=64, required=True),
1205         'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1206         'base': fields.float('Base', digits=(16,int(config['price_accuracy']))),
1207         'amount': fields.float('Amount', digits=(16,int(config['price_accuracy']))),
1208         'manual': fields.boolean('Manual'),
1209         'sequence': fields.integer('Sequence'),
1210
1211         'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1212         'base_amount': fields.float('Base Code Amount', digits=(16,int(config['price_accuracy']))),
1213         'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1214         'tax_amount': fields.float('Tax Code Amount', digits=(16,int(config['price_accuracy']))),
1215     }
1216     
1217     def base_change(self, cr, uid, ids, base,currency_id=False,company_id=False,date_invoice=False):
1218         cur_obj = self.pool.get('res.currency')
1219         company_obj = self.pool.get('res.company')
1220         company_currency=False
1221         if company_id:            
1222             company_currency = company_obj.read(cr,uid,[company_id],['currency_id'])[0]['currency_id'][0]
1223         if currency_id and company_currency:
1224             base = cur_obj.compute(cr, uid, currency_id, company_currency, base, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1225         return {'value': {'base_amount':base}}
1226
1227     def amount_change(self, cr, uid, ids, amount,currency_id=False,company_id=False,date_invoice=False):
1228         cur_obj = self.pool.get('res.currency')
1229         company_obj = self.pool.get('res.company')
1230         company_currency=False
1231         if company_id:
1232             company_currency = company_obj.read(cr,uid,[company_id],['currency_id'])[0]['currency_id'][0]
1233         if currency_id and company_currency:
1234             amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1235         return {'value': {'tax_amount':amount}}
1236     
1237     _order = 'sequence'
1238     _defaults = {
1239         'manual': lambda *a: 1,
1240         'base_amount': lambda *a: 0.0,
1241         'tax_amount': lambda *a: 0.0,
1242     }
1243     def compute(self, cr, uid, invoice_id, context={}):
1244         tax_grouped = {}
1245         tax_obj = self.pool.get('account.tax')
1246         cur_obj = self.pool.get('res.currency')
1247         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context)
1248         cur = inv.currency_id
1249         company_currency = inv.company_id.currency_id.id
1250
1251         for line in inv.invoice_line:
1252             for tax in tax_obj.compute(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, inv.address_invoice_id.id, line.product_id, inv.partner_id):
1253                 val={}
1254                 val['invoice_id'] = inv.id
1255                 val['name'] = tax['name']
1256                 val['amount'] = tax['amount']
1257                 val['manual'] = False
1258                 val['sequence'] = tax['sequence']
1259                 val['base'] = tax['price_unit'] * line['quantity']
1260
1261                 if inv.type in ('out_invoice','in_invoice'):
1262                     val['base_code_id'] = tax['base_code_id']
1263                     val['tax_code_id'] = tax['tax_code_id']
1264                     val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['base_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1265                     val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['tax_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1266                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
1267                 else:
1268                     val['base_code_id'] = tax['ref_base_code_id']
1269                     val['tax_code_id'] = tax['ref_tax_code_id']
1270                     val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['ref_base_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1271                     val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['ref_tax_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1272                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
1273
1274                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1275                 if not key in tax_grouped:
1276                     tax_grouped[key] = val
1277                 else:
1278                     tax_grouped[key]['amount'] += val['amount']
1279                     tax_grouped[key]['base'] += val['base']
1280                     tax_grouped[key]['base_amount'] += val['base_amount']
1281                     tax_grouped[key]['tax_amount'] += val['tax_amount']
1282
1283         for t in tax_grouped.values():
1284             t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1285             t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1286             t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1287             t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1288         return tax_grouped
1289
1290     def move_line_get(self, cr, uid, invoice_id):
1291         res = []
1292         cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1293         for t in cr.dictfetchall():
1294             if not t['amount'] \
1295                     and not t['tax_code_id'] \
1296                     and not t['tax_amount']:
1297                 continue
1298             res.append({
1299                 'type':'tax',
1300                 'name':t['name'],
1301                 'price_unit': t['amount'],
1302                 'quantity': 1,
1303                 'price': t['amount'] or 0.0,
1304                 'account_id': t['account_id'],
1305                 'tax_code_id': t['tax_code_id'],
1306                 'tax_amount': t['tax_amount']
1307             })
1308         return res
1309 account_invoice_tax()
1310
1311 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1312