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