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