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