[FIX] account: corercted Quantity and Amount Currency of a move while related journal...
[odoo/odoo.git] / addons / account / account_invoice.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 from lxml import etree
24 import decimal_precision as dp
25
26 import netsvc
27 import pooler
28 from osv import fields, osv, orm
29 from tools.translate import _
30
31 class account_invoice(osv.osv):
32     def _amount_all(self, cr, uid, ids, name, args, context=None):
33         res = {}
34         for invoice in self.browse(cr, uid, ids, context=context):
35             res[invoice.id] = {
36                 'amount_untaxed': 0.0,
37                 'amount_tax': 0.0,
38                 'amount_total': 0.0
39             }
40             for line in invoice.invoice_line:
41                 res[invoice.id]['amount_untaxed'] += line.price_subtotal
42             for line in invoice.tax_line:
43                 res[invoice.id]['amount_tax'] += line.amount
44             res[invoice.id]['amount_total'] = res[invoice.id]['amount_tax'] + res[invoice.id]['amount_untaxed']
45         return res
46
47     def _get_journal(self, cr, uid, context=None):
48         if context is None:
49             context = {}
50         type_inv = context.get('type', 'out_invoice')
51         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
52         company_id = context.get('company_id', user.company_id.id)
53         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'}
54         journal_obj = self.pool.get('account.journal')
55         res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')),
56                                             ('company_id', '=', company_id)],
57                                                 limit=1)
58         return res and res[0] or False
59
60     def _get_currency(self, cr, uid, context=None):
61         res = False
62         journal_id = self._get_journal(cr, uid, context=context)
63         if journal_id:
64             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
65             res = journal.currency and journal.currency.id or journal.company_id.currency_id.id
66         return res
67
68     def _get_journal_analytic(self, cr, uid, type_inv, context=None):
69         type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
70         tt = type2journal.get(type_inv, 'sale')
71         result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
72         if not result:
73             raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,))
74         return result[0]
75
76     def _get_type(self, cr, uid, context=None):
77         if context is None:
78             context = {}
79         return context.get('type', 'out_invoice')
80
81     def _reconciled(self, cr, uid, ids, name, args, context=None):
82         res = {}
83         wf_service = netsvc.LocalService("workflow")
84         for inv in self.browse(cr, uid, ids, context=context):
85             res[inv.id] = self.test_paid(cr, uid, [inv.id])
86             if not res[inv.id] and inv.state == 'paid':
87                 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'open_test', cr)
88         return res
89
90     def _get_reference_type(self, cr, uid, context=None):
91         return [('none', _('Free Reference'))]
92
93     def _amount_residual(self, cr, uid, ids, name, args, context=None):
94         result = {}
95         for invoice in self.browse(cr, uid, ids, context=context):
96             result[invoice.id] = 0.0
97             if invoice.move_id:
98                 for m in invoice.move_id.line_id:
99                     if m.account_id.type in ('receivable','payable'):
100                         result[invoice.id] += m.amount_residual_currency
101         return result
102
103     # Give Journal Items related to the payment reconciled to this invoice
104     # Return ids of partial and total payments related to the selected invoices
105     def _get_lines(self, cr, uid, ids, name, arg, context=None):
106         res = {}
107         for invoice in self.browse(cr, uid, ids, context=context):
108             id = invoice.id
109             res[id] = []
110             if not invoice.move_id:
111                 continue
112             data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
113             partial_ids = []
114             for line in data_lines:
115                 ids_line = []
116                 if line.reconcile_id:
117                     ids_line = line.reconcile_id.line_id
118                 elif line.reconcile_partial_id:
119                     ids_line = line.reconcile_partial_id.line_partial_ids
120                 l = map(lambda x: x.id, ids_line)
121                 partial_ids.append(line.id)
122                 res[id] =[x for x in l if x <> line.id and x not in partial_ids]
123         return res
124
125     def _get_invoice_line(self, cr, uid, ids, context=None):
126         result = {}
127         for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
128             result[line.invoice_id.id] = True
129         return result.keys()
130
131     def _get_invoice_tax(self, cr, uid, ids, context=None):
132         result = {}
133         for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
134             result[tax.invoice_id.id] = True
135         return result.keys()
136
137     def _compute_lines(self, cr, uid, ids, name, args, context=None):
138         result = {}
139         for invoice in self.browse(cr, uid, ids, context=context):
140             src = []
141             lines = []
142             if invoice.move_id:
143                 for m in invoice.move_id.line_id:
144                     temp_lines = []
145                     if m.reconcile_id:
146                         temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
147                     elif m.reconcile_partial_id:
148                         temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
149                     lines += [x for x in temp_lines if x not in lines]
150                     src.append(m.id)
151
152             lines = filter(lambda x: x not in src, lines)
153             result[invoice.id] = lines
154         return result
155
156     def _get_invoice_from_line(self, cr, uid, ids, context=None):
157         move = {}
158         for line in self.pool.get('account.move.line').browse(cr, uid, ids, context=context):
159             if line.reconcile_partial_id:
160                 for line2 in line.reconcile_partial_id.line_partial_ids:
161                     move[line2.move_id.id] = True
162             if line.reconcile_id:
163                 for line2 in line.reconcile_id.line_id:
164                     move[line2.move_id.id] = True
165         invoice_ids = []
166         if move:
167             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
168         return invoice_ids
169
170     def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
171         move = {}
172         for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
173             for line in r.line_partial_ids:
174                 move[line.move_id.id] = True
175             for line in r.line_id:
176                 move[line.move_id.id] = True
177
178         invoice_ids = []
179         if move:
180             invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
181         return invoice_ids
182
183     _name = "account.invoice"
184     _description = 'Invoice'
185     _order = "id desc"
186
187     _columns = {
188         'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
189         'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
190         'type': fields.selection([
191             ('out_invoice','Customer Invoice'),
192             ('in_invoice','Supplier Invoice'),
193             ('out_refund','Customer Refund'),
194             ('in_refund','Supplier Refund'),
195             ],'Type', readonly=True, select=True, change_default=True),
196
197         'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
198         'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
199         'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
200         'reference_type': fields.selection(_get_reference_type, 'Reference Type',
201             required=True, readonly=True, states={'draft':[('readonly',False)]}),
202         'comment': fields.text('Additional Information'),
203
204         'state': fields.selection([
205             ('draft','Draft'),
206             ('proforma','Pro-forma'),
207             ('proforma2','Pro-forma'),
208             ('open','Open'),
209             ('paid','Paid'),
210             ('cancel','Cancelled')
211             ],'State', select=True, readonly=True,
212             help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
213             \n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
214             \n* The \'Open\' state is used when user create invoice,a invoice number is generated.Its in open state till user does not pay invoice. \
215             \n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
216             \n* The \'Cancelled\' state is used when user cancel invoice.'),
217         'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
218         'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
219             help="If you use payment terms, the due date will be computed automatically at the generation "\
220                 "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."),
221         'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
222         'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}),
223         'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
224         'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
225             help="If you use payment terms, the due date will be computed automatically at the generation "\
226                 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
227                 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
228         '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)]}),
229
230         'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
231         'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
232         'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
233
234         'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
235         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed',
236             store={
237                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
238                 'account.invoice.tax': (_get_invoice_tax, None, 20),
239                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
240             },
241             multi='all'),
242         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
243             store={
244                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
245                 'account.invoice.tax': (_get_invoice_tax, None, 20),
246                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
247             },
248             multi='all'),
249         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
250             store={
251                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
252                 'account.invoice.tax': (_get_invoice_tax, None, 20),
253                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
254             },
255             multi='all'),
256         'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
257         'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
258         'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
259         'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
260         'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
261             store={
262                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
263                 'account.move.line': (_get_invoice_from_line, None, 50),
264                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
265             }, help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment."),
266         'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
267             help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.', readonly=True, states={'draft':[('readonly',False)]}),
268         'move_lines':fields.function(_get_lines, type='many2many', relation='account.move.line', string='Entry Lines'),
269         'residual': fields.function(_amount_residual, digits_compute=dp.get_precision('Account'), string='Balance',
270             store={
271                 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line','move_id'], 50),
272                 'account.invoice.tax': (_get_invoice_tax, None, 50),
273                 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 50),
274                 'account.move.line': (_get_invoice_from_line, None, 50),
275                 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
276             },
277             help="Remaining amount due."),
278         'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
279         'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
280         'user_id': fields.many2one('res.users', 'Salesman', readonly=True, states={'draft':[('readonly',False)]}),
281         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
282     }
283     _defaults = {
284         'type': _get_type,
285         'state': 'draft',
286         'journal_id': _get_journal,
287         'currency_id': _get_currency,
288         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.invoice', context=c),
289         'reference_type': 'none',
290         'check_total': 0.0,
291         'internal_number': False,
292         'user_id': lambda s, cr, u, c: u,
293     }
294     _sql_constraints = [
295         ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
296     ]
297
298     def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
299         journal_obj = self.pool.get('account.journal')
300         if context is None:
301             context = {}
302
303         if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
304             partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
305             if not view_type:
306                 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
307                 view_type = 'tree'
308             if view_type == 'form':
309                 if partner['supplier'] and not partner['customer']:
310                     view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
311                 else:
312                     view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
313         if view_id and isinstance(view_id, (list, tuple)):
314             view_id = view_id[0]
315         res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
316
317         type = context.get('journal_type', False)
318         for field in res['fields']:
319             if field == 'journal_id' and type:
320                 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
321                 res['fields'][field]['selection'] = journal_select
322
323         doc = etree.XML(res['arch'])
324         
325         if context.get('type', False):
326             for node in doc.xpath("//field[@name='partner_bank_id']"):
327                 if context['type'] == 'in_refund':
328                     node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
329                 elif context['type'] == 'out_refund':
330                     node.set('domain', "[('partner_id', '=', partner_id)]")
331             res['arch'] = etree.tostring(doc)
332                 
333         if view_type == 'search':
334             if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
335                 for node in doc.xpath("//group[@name='extended filter']"):
336                     doc.remove(node)
337             res['arch'] = etree.tostring(doc)
338
339         if view_type == 'tree':
340             partner_string = _('Customer')
341             if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
342                 partner_string = _('Supplier')
343                 for node in doc.xpath("//field[@name='reference']"):
344                     node.set('invisible', '0')
345             for node in doc.xpath("//field[@name='partner_id']"):
346                 node.set('string', partner_string)
347             res['arch'] = etree.tostring(doc)
348         return res
349
350     def get_log_context(self, cr, uid, context=None):
351         if context is None:
352             context = {}
353         res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
354         view_id = res and res[1] or False
355         context['view_id'] = view_id
356         return context
357
358     def create(self, cr, uid, vals, context=None):
359         if context is None:
360             context = {}
361         try:
362             res = super(account_invoice, self).create(cr, uid, vals, context)
363             for inv_id, name in self.name_get(cr, uid, [res], context=context):
364                 ctx = context.copy()
365                 if vals.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
366                     ctx = self.get_log_context(cr, uid, context=ctx)
367                 message = _("Invoice '%s' is waiting for validation.") % name
368                 self.log(cr, uid, inv_id, message, context=ctx)
369             return res
370         except Exception, e:
371             if '"journal_id" viol' in e.args[0]:
372                 raise orm.except_orm(_('Configuration Error!'),
373                      _('There is no Accounting Journal of type Sale/Purchase defined!'))
374             else:
375                 raise orm.except_orm(_('Unknown Error'), str(e))
376
377     def confirm_paid(self, cr, uid, ids, context=None):
378         if context is None:
379             context = {}
380         self.write(cr, uid, ids, {'state':'paid'}, context=context)
381         for inv_id, name in self.name_get(cr, uid, ids, context=context):
382             message = _("Invoice '%s' is paid.") % name
383             self.log(cr, uid, inv_id, message)
384         return True
385
386     def unlink(self, cr, uid, ids, context=None):
387         if context is None:
388             context = {}
389         invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
390         unlink_ids = []
391         for t in invoices:
392             if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
393                 unlink_ids.append(t['id'])
394             else:
395                 raise osv.except_osv(_('Invalid action !'), _('You can not delete an invoice which is open or paid. We suggest you to refund it instead.'))
396         osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
397         return True
398
399     def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
400             date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
401         invoice_addr_id = False
402         contact_addr_id = False
403         partner_payment_term = False
404         acc_id = False
405         bank_id = False
406         fiscal_position = False
407
408         opt = [('uid', str(uid))]
409         if partner_id:
410
411             opt.insert(0, ('id', partner_id))
412             res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
413             contact_addr_id = res['contact']
414             invoice_addr_id = res['invoice']
415             p = self.pool.get('res.partner').browse(cr, uid, partner_id)
416             if company_id:
417                 if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
418                     property_obj = self.pool.get('ir.property')
419                     rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
420                     pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
421                     if not rec_pro_id:
422                         rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
423                     if not pay_pro_id:
424                         pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
425                     rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
426                     pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
427                     rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
428                     pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
429                     if not rec_res_id and not pay_res_id:
430                         raise osv.except_osv(_('Configuration Error !'),
431                             _('Can not find a chart of accounts for this company, you should create one.'))
432                     account_obj = self.pool.get('account.account')
433                     rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
434                     pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
435                     p.property_account_receivable = rec_obj_acc[0]
436                     p.property_account_payable = pay_obj_acc[0]
437
438             if type in ('out_invoice', 'out_refund'):
439                 acc_id = p.property_account_receivable.id
440             else:
441                 acc_id = p.property_account_payable.id
442             fiscal_position = p.property_account_position and p.property_account_position.id or False
443             partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
444             if p.bank_ids:
445                 bank_id = p.bank_ids[0].id
446
447         result = {'value': {
448             'address_contact_id': contact_addr_id,
449             'address_invoice_id': invoice_addr_id,
450             'account_id': acc_id,
451             'payment_term': partner_payment_term,
452             'fiscal_position': fiscal_position
453             }
454         }
455
456         if type in ('in_invoice', 'in_refund'):
457             result['value']['partner_bank_id'] = bank_id
458
459         if payment_term != partner_payment_term:
460             if partner_payment_term:
461                 to_update = self.onchange_payment_term_date_invoice(
462                     cr, uid, ids, partner_payment_term, date_invoice)
463                 result['value'].update(to_update['value'])
464             else:
465                 result['value']['date_due'] = False
466
467         if partner_bank_id != bank_id:
468             to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
469             result['value'].update(to_update['value'])
470         return result
471
472     def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
473         result = {}
474         if journal_id:
475             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
476             currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
477             result = {'value': {
478                     'currency_id': currency_id,
479                     }
480                 }
481         return result
482
483     def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
484         if not payment_term_id:
485             return {}
486         res = {}
487         pt_obj = self.pool.get('account.payment.term')
488         if not date_invoice:
489             date_invoice = time.strftime('%Y-%m-%d')
490
491         pterm_list = pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
492
493         if pterm_list:
494             pterm_list = [line[0] for line in pterm_list]
495             pterm_list.sort()
496             res = {'value':{'date_due': pterm_list[-1]}}
497         else:
498              raise osv.except_osv(_('Data Insufficient !'), _('The payment term of supplier does not have a payment term line!'))
499         return res
500
501     def onchange_invoice_line(self, cr, uid, ids, lines):
502         return {}
503
504     def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
505         return {'value': {}}
506
507     def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
508         val = {}
509         dom = {}
510         obj_journal = self.pool.get('account.journal')
511         account_obj = self.pool.get('account.account')
512         inv_line_obj = self.pool.get('account.invoice.line')
513         if company_id and part_id and type:
514             acc_id = False
515             partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
516             if partner_obj.property_account_payable and partner_obj.property_account_receivable:
517                 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
518                     property_obj = self.pool.get('ir.property')
519                     rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
520                     pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
521                     if not rec_pro_id:
522                         rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
523                     if not pay_pro_id:
524                         pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
525                     rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
526                     pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
527                     rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
528                     pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
529                     if not rec_res_id and not pay_res_id:
530                         raise osv.except_osv(_('Configuration Error !'),
531                             _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
532                     if type in ('out_invoice', 'out_refund'):
533                         acc_id = rec_res_id
534                     else:
535                         acc_id = pay_res_id
536                     val= {'account_id': acc_id}
537             if ids:
538                 if company_id:
539                     inv_obj = self.browse(cr,uid,ids)
540                     for line in inv_obj[0].invoice_line:
541                         if line.account_id:
542                             if line.account_id.company_id.id != company_id:
543                                 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
544                                 if not result_id:
545                                     raise osv.except_osv(_('Configuration Error !'),
546                                         _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
547                                 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
548             else:
549                 if invoice_line:
550                     for inv_line in invoice_line:
551                         obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
552                         if obj_l.company_id.id != company_id:
553                             raise osv.except_osv(_('Configuration Error !'),
554                                 _('Invoice line account company does not match with invoice company.'))
555                         else:
556                             continue
557         if company_id and type:
558             if type in ('out_invoice'):
559                 journal_type = 'sale'
560             elif type in ('out_refund'):
561                 journal_type = 'sale_refund'
562             elif type in ('in_refund'):
563                 journal_type = 'purchase_refund'
564             else:
565                 journal_type = 'purchase'
566             journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
567             if journal_ids:
568                 val['journal_id'] = journal_ids[0]
569             ir_values_obj = self.pool.get('ir.values')
570             res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
571             for r in res_journal_default:
572                 if r[1] == 'journal_id' and r[2] in journal_ids:
573                     val['journal_id'] = r[2]
574             if not val.get('journal_id', False):
575                 raise osv.except_osv(_('Configuration Error !'), (_('Can\'t find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Financial Accounting\Accounts\Journals.') % (journal_type)))
576             dom = {'journal_id':  [('id', 'in', journal_ids)]}
577         else:
578             journal_ids = obj_journal.search(cr, uid, [])
579
580         return {'value': val, 'domain': dom}
581
582     # go from canceled state to draft state
583     def action_cancel_draft(self, cr, uid, ids, *args):
584         self.write(cr, uid, ids, {'state':'draft'})
585         wf_service = netsvc.LocalService("workflow")
586         for inv_id in ids:
587             wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
588             wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
589         return True
590
591     # Workflow stuff
592     #################
593
594     # return the ids of the move lines which has the same account than the invoice
595     # whose id is in ids
596     def move_line_id_payment_get(self, cr, uid, ids, *args):
597         if not ids: return []
598         result = self.move_line_id_payment_gets(cr, uid, ids, *args)
599         return result.get(ids[0], [])
600
601     def move_line_id_payment_gets(self, cr, uid, ids, *args):
602         res = {}
603         if not ids: return res
604         cr.execute('SELECT i.id, l.id '\
605                    'FROM account_move_line l '\
606                    'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
607                    'WHERE i.id IN %s '\
608                    'AND l.account_id=i.account_id',
609                    (tuple(ids),))
610         for r in cr.fetchall():
611             res.setdefault(r[0], [])
612             res[r[0]].append( r[1] )
613         return res
614
615     def copy(self, cr, uid, id, default=None, context=None):
616         default = default or {}
617         default.update({
618             'state':'draft',
619             'number':False,
620             'move_id':False,
621             'move_name':False,
622             'internal_number': False,
623             'period_id': False,
624         })
625         if 'date_invoice' not in default:
626             default.update({
627                 'date_invoice':False
628             })
629         if 'date_due' not in default:
630             default.update({
631                 'date_due':False
632             })
633         return super(account_invoice, self).copy(cr, uid, id, default, context)
634
635     def test_paid(self, cr, uid, ids, *args):
636         res = self.move_line_id_payment_get(cr, uid, ids)
637         if not res:
638             return False
639         ok = True
640         for id in res:
641             cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
642             ok = ok and  bool(cr.fetchone()[0])
643         return ok
644
645     def button_reset_taxes(self, cr, uid, ids, context=None):
646         if context is None:
647             context = {}
648         ctx = context.copy()
649         ait_obj = self.pool.get('account.invoice.tax')
650         for id in ids:
651             cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
652             partner = self.browse(cr, uid, id, context=ctx).partner_id
653             if partner.lang:
654                 ctx.update({'lang': partner.lang})
655             for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
656                 ait_obj.create(cr, uid, taxe)
657         # Update the stored value (fields.function), so we write to trigger recompute
658         self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
659         return True
660
661     def button_compute(self, cr, uid, ids, context=None, set_total=False):
662         self.button_reset_taxes(cr, uid, ids, context)
663         for inv in self.browse(cr, uid, ids, context=context):
664             if set_total:
665                 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
666         return True
667
668     def _convert_ref(self, cr, uid, ref):
669         return (ref or '').replace('/','')
670
671     def _get_analytic_lines(self, cr, uid, id, context=None):
672         if context is None:
673             context = {}
674         inv = self.browse(cr, uid, id)
675         cur_obj = self.pool.get('res.currency')
676
677         company_currency = inv.company_id.currency_id.id
678         if inv.type in ('out_invoice', 'in_refund'):
679             sign = 1
680         else:
681             sign = -1
682
683         iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
684         for il in iml:
685             if il['account_analytic_id']:
686                 if inv.type in ('in_invoice', 'in_refund'):
687                     ref = inv.reference
688                 else:
689                     ref = self._convert_ref(cr, uid, inv.number)
690                 if not inv.journal_id.analytic_journal_id:
691                     raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
692                 il['analytic_lines'] = [(0,0, {
693                     'name': il['name'],
694                     'date': inv['date_invoice'],
695                     'account_id': il['account_analytic_id'],
696                     'unit_amount': il['quantity'],
697                     'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
698                     'product_id': il['product_id'],
699                     'product_uom_id': il['uos_id'],
700                     'general_account_id': il['account_id'],
701                     'journal_id': inv.journal_id.analytic_journal_id.id,
702                     'ref': ref,
703                 })]
704         return iml
705
706     def action_date_assign(self, cr, uid, ids, *args):
707         for inv in self.browse(cr, uid, ids):
708             res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
709             if res and res['value']:
710                 self.write(cr, uid, [inv.id], res['value'])
711         return True
712
713     def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
714         """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
715         Hook method to be overridden in additional modules to verify and possibly alter the
716         move lines to be created by an invoice, for special cases.
717         :param invoice_browse: browsable record of the invoice that is generating the move lines
718         :param move_lines: list of dictionaries with the account.move.lines (as for create())
719         :return: the (possibly updated) final move_lines to create for this invoice
720         """
721         return move_lines
722
723     def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
724         if not inv.tax_line:
725             for tax in compute_taxes.values():
726                 ait_obj.create(cr, uid, tax)
727         else:
728             tax_key = []
729             for tax in inv.tax_line:
730                 if tax.manual:
731                     continue
732                 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
733                 tax_key.append(key)
734                 if not key in compute_taxes:
735                     raise osv.except_osv(_('Warning !'), _('Global taxes defined, but they are not in invoice lines !'))
736                 base = compute_taxes[key]['base']
737                 if abs(base - tax.base) > inv.company_id.currency_id.rounding:
738                     raise osv.except_osv(_('Warning !'), _('Tax base different!\nClick on compute to update the tax base.'))
739             for key in compute_taxes:
740                 if not key in tax_key:
741                     raise osv.except_osv(_('Warning !'), _('Taxes are missing!\nClick on compute button.'))
742
743     def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
744         total = 0
745         total_currency = 0
746         cur_obj = self.pool.get('res.currency')
747         for i in invoice_move_lines:
748             if inv.currency_id.id != company_currency:
749                 i['currency_id'] = inv.currency_id.id
750                 i['amount_currency'] = i['price']
751                 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
752                         company_currency, i['price'],
753                         context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
754             else:
755                 i['amount_currency'] = False
756                 i['currency_id'] = False
757             i['ref'] = ref
758             if inv.type in ('out_invoice','in_refund'):
759                 total += i['price']
760                 total_currency += i['amount_currency'] or i['price']
761                 i['price'] = - i['price']
762             else:
763                 total -= i['price']
764                 total_currency -= i['amount_currency'] or i['price']
765         return total, total_currency, invoice_move_lines
766
767     def inv_line_characteristic_hashcode(self, invoice, invoice_line):
768         """Overridable hashcode generation for invoice lines. Lines having the same hashcode
769         will be grouped together if the journal has the 'group line' option. Of course a module
770         can add fields to invoice lines that would need to be tested too before merging lines
771         or not."""
772         return "%s-%s-%s-%s-%s"%(
773             invoice_line['account_id'],
774             invoice_line.get('tax_code_id',"False"),
775             invoice_line.get('product_id',"False"),
776             invoice_line.get('analytic_account_id',"False"),
777             invoice_line.get('date_maturity',"False"))
778
779     def group_lines(self, cr, uid, iml, line, inv):
780         """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
781         if inv.journal_id.group_invoice_lines:
782             line2 = {}
783             for x, y, l in line:
784                 tmp = self.inv_line_characteristic_hashcode(inv, l)
785
786                 if tmp in line2:
787                     am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
788                     line2[tmp]['debit'] = (am > 0) and am or 0.0
789                     line2[tmp]['credit'] = (am < 0) and -am or 0.0
790                     line2[tmp]['tax_amount'] += l['tax_amount']
791                     line2[tmp]['analytic_lines'] += l['analytic_lines']
792                     line2[tmp]['amount_currency'] += l['amount_currency']
793                     line2[tmp]['quantity'] += l['quantity']
794                 else:
795                     line2[tmp] = l
796             line = []
797             for key, val in line2.items():
798                 line.append((0,0,val))
799         return line
800
801     def action_move_create(self, cr, uid, ids, context=None):
802         """Creates invoice related analytics and financial move lines"""
803         ait_obj = self.pool.get('account.invoice.tax')
804         cur_obj = self.pool.get('res.currency')
805         period_obj = self.pool.get('account.period')
806         payment_term_obj = self.pool.get('account.payment.term')
807         journal_obj = self.pool.get('account.journal')
808         move_obj = self.pool.get('account.move')
809         if context is None:
810             context = {}
811         for inv in self.browse(cr, uid, ids, context=context):
812             if not inv.journal_id.sequence_id:
813                 raise osv.except_osv(_('Error !'), _('Please define sequence on the journal related to this invoice.'))
814             if not inv.invoice_line:
815                 raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
816             if inv.move_id:
817                 continue
818             
819             ctx = context.copy()
820             ctx.update({'lang': inv.partner_id.lang})
821             if not inv.date_invoice:
822                 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
823             company_currency = inv.company_id.currency_id.id
824             # create the analytical lines
825             # one move line per invoice line
826             iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
827             # check if taxes are all computed
828             compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
829             self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
830
831             # I disabled the check_total feature
832             #if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
833             #    raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
834
835             if inv.payment_term:
836                 total_fixed = total_percent = 0
837                 for line in inv.payment_term.line_ids:
838                     if line.value == 'fixed':
839                         total_fixed += line.value_amount
840                     if line.value == 'procent':
841                         total_percent += line.value_amount
842                 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
843                 if (total_fixed + total_percent) > 100:
844                     raise osv.except_osv(_('Error !'), _("Can not create the invoice !\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. The latest line of your payment term must be of type 'balance' to avoid rounding issues."))
845
846             # one move line per tax line
847             iml += ait_obj.move_line_get(cr, uid, inv.id)
848
849             entry_type = ''
850             if inv.type in ('in_invoice', 'in_refund'):
851                 ref = inv.reference
852                 entry_type = 'journal_pur_voucher'
853                 if inv.type == 'in_refund':
854                     entry_type = 'cont_voucher'
855             else:
856                 ref = self._convert_ref(cr, uid, inv.number)
857                 entry_type = 'journal_sale_vou'
858                 if inv.type == 'out_refund':
859                     entry_type = 'cont_voucher'
860
861             diff_currency_p = inv.currency_id.id <> company_currency
862             # create one move line for the total and possibly adjust the other lines amount
863             total = 0
864             total_currency = 0
865             total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
866             acc_id = inv.account_id.id
867
868             name = inv['name'] or '/'
869             totlines = False
870             if inv.payment_term:
871                 totlines = payment_term_obj.compute(cr,
872                         uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
873             if totlines:
874                 res_amount_currency = total_currency
875                 i = 0
876                 ctx.update({'date': inv.date_invoice})
877                 for t in totlines:
878                     if inv.currency_id.id != company_currency:
879                         amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
880                     else:
881                         amount_currency = False
882
883                     # last line add the diff
884                     res_amount_currency -= amount_currency or 0
885                     i += 1
886                     if i == len(totlines):
887                         amount_currency += res_amount_currency
888
889                     iml.append({
890                         'type': 'dest',
891                         'name': name,
892                         'price': t[1],
893                         'account_id': acc_id,
894                         'date_maturity': t[0],
895                         'amount_currency': diff_currency_p \
896                                 and amount_currency or False,
897                         'currency_id': diff_currency_p \
898                                 and inv.currency_id.id or False,
899                         'ref': ref,
900                     })
901             else:
902                 iml.append({
903                     'type': 'dest',
904                     'name': name,
905                     'price': total,
906                     'account_id': acc_id,
907                     'date_maturity': inv.date_due or False,
908                     'amount_currency': diff_currency_p \
909                             and total_currency or False,
910                     'currency_id': diff_currency_p \
911                             and inv.currency_id.id or False,
912                     'ref': ref
913             })
914
915             date = inv.date_invoice or time.strftime('%Y-%m-%d')
916             part = inv.partner_id.id
917
918             line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
919
920             line = self.group_lines(cr, uid, iml, line, inv)
921
922             journal_id = inv.journal_id.id
923             journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
924             if journal.centralisation:
925                 raise osv.except_osv(_('UserError'),
926                         _('You cannot create an invoice on a centralised journal. Uncheck the centralised counterpart box in the related journal from the configuration menu.'))
927
928             line = self.finalize_invoice_move_lines(cr, uid, inv, line)
929
930             move = {
931                 'ref': inv.reference and inv.reference or inv.name,
932                 'line_id': line,
933                 'journal_id': journal_id,
934                 'date': date,
935                 'narration':inv.comment
936             }
937             period_id = inv.period_id and inv.period_id.id or False
938             ctx.update({'company_id': inv.company_id.id})
939             if not period_id:
940                 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
941                 period_id = period_ids and period_ids[0] or False
942             if period_id:
943                 move['period_id'] = period_id
944                 for i in line:
945                     i[2]['period_id'] = period_id
946
947             move_id = move_obj.create(cr, uid, move, context=ctx)
948             new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
949             # make the invoice point to that move
950             self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
951             # Pass invoice in context in method post: used if you want to get the same
952             # account move reference when creating the same invoice after a cancelled one:
953             ctx.update({'invoice':inv})
954             move_obj.post(cr, uid, [move_id], context=ctx)
955         self._log_event(cr, uid, ids)
956         return True
957
958     def line_get_convert(self, cr, uid, x, part, date, context=None):
959         return {
960             'date_maturity': x.get('date_maturity', False),
961             'partner_id': part,
962             'name': x['name'][:64],
963             'date': date,
964             'debit': x['price']>0 and x['price'],
965             'credit': x['price']<0 and -x['price'],
966             'account_id': x['account_id'],
967             'analytic_lines': x.get('analytic_lines', []),
968             'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
969             'currency_id': x.get('currency_id', False),
970             'tax_code_id': x.get('tax_code_id', False),
971             'tax_amount': x.get('tax_amount', False),
972             'ref': x.get('ref', False),
973             'quantity': x.get('quantity',1.00),
974             'product_id': x.get('product_id', False),
975             'product_uom_id': x.get('uos_id', False),
976             'analytic_account_id': x.get('account_analytic_id', False),
977         }
978
979     def action_number(self, cr, uid, ids, context=None):
980         if context is None:
981             context = {}
982         #TODO: not correct fix but required a frech values before reading it.
983         self.write(cr, uid, ids, {})
984
985         for obj_inv in self.browse(cr, uid, ids, context=context):
986             id = obj_inv.id
987             invtype = obj_inv.type
988             number = obj_inv.number
989             move_id = obj_inv.move_id and obj_inv.move_id.id or False
990             reference = obj_inv.reference or ''
991
992             self.write(cr, uid, ids, {'internal_number':number})
993
994             if invtype in ('in_invoice', 'in_refund'):
995                 if not reference:
996                     ref = self._convert_ref(cr, uid, number)
997                 else:
998                     ref = reference
999             else:
1000                 ref = self._convert_ref(cr, uid, number)
1001
1002             cr.execute('UPDATE account_move SET ref=%s ' \
1003                     'WHERE id=%s AND (ref is null OR ref = \'\')',
1004                     (ref, move_id))
1005             cr.execute('UPDATE account_move_line SET ref=%s ' \
1006                     'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1007                     (ref, move_id))
1008             cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1009                     'FROM account_move_line ' \
1010                     'WHERE account_move_line.move_id = %s ' \
1011                         'AND account_analytic_line.move_id = account_move_line.id',
1012                         (ref, move_id))
1013
1014             for inv_id, name in self.name_get(cr, uid, [id]):
1015                 ctx = context.copy()
1016                 if obj_inv.type in ('out_invoice', 'out_refund'):
1017                     ctx = self.get_log_context(cr, uid, context=ctx)
1018                 message = _("Invoice  '%s' is validated.") % name
1019                 self.log(cr, uid, inv_id, message, context=ctx)
1020         return True
1021
1022     def action_cancel(self, cr, uid, ids, *args):
1023         context = {} # TODO: Use context from arguments
1024         account_move_obj = self.pool.get('account.move')
1025         invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1026         move_ids = [] # ones that we will need to remove
1027         for i in invoices:
1028             if i['move_id']:
1029                 move_ids.append(i['move_id'][0])
1030             if i['payment_ids']:
1031                 account_move_line_obj = self.pool.get('account.move.line')
1032                 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1033                 for move_line in pay_ids:
1034                     if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1035                         raise osv.except_osv(_('Error !'), _('You can not cancel an invoice which is partially paid! You need to unreconcile related payment entries first!'))
1036
1037         # First, set the invoices as cancelled and detach the move ids
1038         self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1039         if move_ids:
1040             # second, invalidate the move(s)
1041             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1042             # delete the move this invoice was pointing to
1043             # Note that the corresponding move_lines and move_reconciles
1044             # will be automatically deleted too
1045             account_move_obj.unlink(cr, uid, move_ids, context=context)
1046         self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1047         return True
1048
1049     ###################
1050
1051     def list_distinct_taxes(self, cr, uid, ids):
1052         invoices = self.browse(cr, uid, ids)
1053         taxes = {}
1054         for inv in invoices:
1055             for tax in inv.tax_line:
1056                 if not tax['name'] in taxes:
1057                     taxes[tax['name']] = {'name': tax['name']}
1058         return taxes.values()
1059
1060     def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1061         #TODO: implement messages system
1062         return True
1063
1064     def name_get(self, cr, uid, ids, context=None):
1065         if not ids:
1066             return []
1067         types = {
1068                 'out_invoice': 'CI: ',
1069                 'in_invoice': 'SI: ',
1070                 'out_refund': 'OR: ',
1071                 'in_refund': 'SR: ',
1072                 }
1073         return [(r['id'], (r['number']) or types[r['type']] + (r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
1074
1075     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1076         if not args:
1077             args = []
1078         if context is None:
1079             context = {}
1080         ids = []
1081         if name:
1082             ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1083         if not ids:
1084             ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1085         return self.name_get(cr, user, ids, context)
1086
1087     def _refund_cleanup_lines(self, cr, uid, lines):
1088         for line in lines:
1089             del line['id']
1090             del line['invoice_id']
1091             for field in ('company_id', 'partner_id', 'account_id', 'product_id',
1092                           'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
1093                 if line.get(field):
1094                     line[field] = line[field][0]
1095             if 'invoice_line_tax_id' in line:
1096                 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
1097         return map(lambda x: (0,0,x), lines)
1098
1099     def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
1100         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', 'user_id', 'fiscal_position'])
1101         obj_invoice_line = self.pool.get('account.invoice.line')
1102         obj_invoice_tax = self.pool.get('account.invoice.tax')
1103         obj_journal = self.pool.get('account.journal')
1104         new_ids = []
1105         for invoice in invoices:
1106             del invoice['id']
1107
1108             type_dict = {
1109                 'out_invoice': 'out_refund', # Customer Invoice
1110                 'in_invoice': 'in_refund',   # Supplier Invoice
1111                 'out_refund': 'out_invoice', # Customer Refund
1112                 'in_refund': 'in_invoice',   # Supplier Refund
1113             }
1114
1115             invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
1116             invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
1117
1118             tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
1119             tax_lines = filter(lambda l: l['manual'], tax_lines)
1120             tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
1121             if journal_id:
1122                 refund_journal_ids = [journal_id]
1123             elif invoice['type'] == 'in_invoice':
1124                 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
1125             else:
1126                 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
1127
1128             if not date:
1129                 date = time.strftime('%Y-%m-%d')
1130             invoice.update({
1131                 'type': type_dict[invoice['type']],
1132                 'date_invoice': date,
1133                 'state': 'draft',
1134                 'number': False,
1135                 'invoice_line': invoice_lines,
1136                 'tax_line': tax_lines,
1137                 'journal_id': refund_journal_ids
1138             })
1139             if period_id:
1140                 invoice.update({
1141                     'period_id': period_id,
1142                 })
1143             if description:
1144                 invoice.update({
1145                     'name': description,
1146                 })
1147             # take the id part of the tuple returned for many2one fields
1148             for field in ('address_contact_id', 'address_invoice_id', 'partner_id',
1149                     'account_id', 'currency_id', 'payment_term', 'journal_id',
1150                     'user_id', 'fiscal_position'):
1151                 invoice[field] = invoice[field] and invoice[field][0]
1152             # create the new invoice
1153             new_ids.append(self.create(cr, uid, invoice))
1154
1155         return new_ids
1156
1157     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=''):
1158         if context is None:
1159             context = {}
1160         #TODO check if we can use different period for payment and the writeoff line
1161         assert len(ids)==1, "Can only pay one invoice at a time"
1162         invoice = self.browse(cr, uid, ids[0], context=context)
1163         src_account_id = invoice.account_id.id
1164         # Take the seq as name for move
1165         types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1166         direction = types[invoice.type]
1167         #take the choosen date
1168         if 'date_p' in context and context['date_p']:
1169             date=context['date_p']
1170         else:
1171             date=time.strftime('%Y-%m-%d')
1172
1173         # Take the amount in currency and the currency of the payment
1174         if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1175             amount_currency = context['amount_currency']
1176             currency_id = context['currency_id']
1177         else:
1178             amount_currency = False
1179             currency_id = False
1180
1181         pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1182         if invoice.type in ('in_invoice', 'out_invoice'):
1183             if pay_journal['type'] == 'bank':
1184                 entry_type = 'bank_pay_voucher' # Bank payment
1185             else:
1186                 entry_type = 'pay_voucher' # Cash payment
1187         else:
1188             entry_type = 'cont_voucher'
1189         if invoice.type in ('in_invoice', 'in_refund'):
1190             ref = invoice.reference
1191         else:
1192             ref = self._convert_ref(cr, uid, invoice.number)
1193         # Pay attention to the sign for both debit/credit AND amount_currency
1194         l1 = {
1195             'debit': direction * pay_amount>0 and direction * pay_amount,
1196             'credit': direction * pay_amount<0 and - direction * pay_amount,
1197             'account_id': src_account_id,
1198             'partner_id': invoice.partner_id.id,
1199             'ref':ref,
1200             'date': date,
1201             'currency_id':currency_id,
1202             'amount_currency':amount_currency and direction * amount_currency or 0.0,
1203             'company_id': invoice.company_id.id,
1204         }
1205         l2 = {
1206             'debit': direction * pay_amount<0 and - direction * pay_amount,
1207             'credit': direction * pay_amount>0 and direction * pay_amount,
1208             'account_id': pay_account_id,
1209             'partner_id': invoice.partner_id.id,
1210             'ref':ref,
1211             'date': date,
1212             'currency_id':currency_id,
1213             'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1214             'company_id': invoice.company_id.id,
1215         }
1216
1217         if not name:
1218             name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1219         l1['name'] = name
1220         l2['name'] = name
1221
1222         lines = [(0, 0, l1), (0, 0, l2)]
1223         move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1224         move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1225
1226         line_ids = []
1227         total = 0.0
1228         line = self.pool.get('account.move.line')
1229         move_ids = [move_id,]
1230         if invoice.move_id:
1231             move_ids.append(invoice.move_id.id)
1232         cr.execute('SELECT id FROM account_move_line '\
1233                    'WHERE move_id IN %s',
1234                    ((move_id, invoice.move_id.id),))
1235         lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1236         for l in lines+invoice.payment_ids:
1237             if l.account_id.id == src_account_id:
1238                 line_ids.append(l.id)
1239                 total += (l.debit or 0.0) - (l.credit or 0.0)
1240
1241         inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1242         if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1243             self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1244         else:
1245             code = invoice.currency_id.symbol
1246             # TODO: use currency's formatting function
1247             msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining)") % \
1248                     (name, pay_amount, code, invoice.amount_total, code, total, code)
1249             self.log(cr, uid, inv_id,  msg)
1250             self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1251
1252         # Update the stored value (fields.function), so we write to trigger recompute
1253         self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1254         return True
1255
1256 account_invoice()
1257
1258 class account_invoice_line(osv.osv):
1259
1260     def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1261         res = {}
1262         tax_obj = self.pool.get('account.tax')
1263         cur_obj = self.pool.get('res.currency')
1264         for line in self.browse(cr, uid, ids):
1265             price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1266             taxes = tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, price, line.quantity, product=line.product_id, address_id=line.invoice_id.address_invoice_id, partner=line.invoice_id.partner_id)
1267             res[line.id] = taxes['total']
1268             if line.invoice_id:
1269                 cur = line.invoice_id.currency_id
1270                 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1271         return res
1272
1273     def _price_unit_default(self, cr, uid, context=None):
1274         if context is None:
1275             context = {}
1276         if context.get('check_total', False):
1277             t = context['check_total']
1278             for l in context.get('invoice_line', {}):
1279                 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1280                     tax_obj = self.pool.get('account.tax')
1281                     p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1282                     t = t - (p * l[2].get('quantity'))
1283                     taxes = l[2].get('invoice_line_tax_id')
1284                     if len(taxes[0]) >= 3 and taxes[0][2]:
1285                         taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1286                         for tax in tax_obj.compute_all(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))['taxes']:
1287                             t = t - tax['amount']
1288             return t
1289         return 0
1290
1291     _name = "account.invoice.line"
1292     _description = "Invoice Line"
1293     _columns = {
1294         'name': fields.char('Description', size=256, required=True),
1295         'origin': fields.char('Origin', size=256, help="Reference of the document that produced this invoice."),
1296         'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1297         'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1298         'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1299         '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."),
1300         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
1301         'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
1302             digits_compute= dp.get_precision('Account'), store=True),
1303         'quantity': fields.float('Quantity', required=True),
1304         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
1305         'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1306         'note': fields.text('Notes'),
1307         'account_analytic_id':  fields.many2one('account.analytic.account', 'Analytic Account'),
1308         'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1309         'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1310     }
1311     _defaults = {
1312         'quantity': 1,
1313         'discount': 0.0,
1314         'price_unit': _price_unit_default,
1315     }
1316
1317     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1318         if context is None:
1319             context = {}
1320         res = super(account_invoice_line,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
1321         if context.get('type', False):
1322             doc = etree.XML(res['arch'])
1323             for node in doc.xpath("//field[@name='product_id']"):
1324                 if context['type'] in ('in_invoice', 'in_refund'):
1325                     node.set('domain', "[('purchase_ok', '=', True)]")
1326                 else:
1327                     node.set('domain', "[('sale_ok', '=', True)]")
1328             res['arch'] = etree.tostring(doc)
1329         return res
1330
1331     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, currency_id=False, context=None, company_id=None):
1332         if context is None:
1333             context = {}
1334         company_id = company_id if company_id != None else context.get('company_id',False)
1335         context = dict(context)
1336         context.update({'company_id': company_id})
1337         if not partner_id:
1338             raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1339         if not product:
1340             if type in ('in_invoice', 'in_refund'):
1341                 return {'value': {}, 'domain':{'product_uom':[]}}
1342             else:
1343                 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1344         part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1345         fpos_obj = self.pool.get('account.fiscal.position')
1346         fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1347
1348         if part.lang:
1349             context.update({'lang': part.lang})
1350         result = {}
1351         res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1352
1353         if type in ('out_invoice','out_refund'):
1354             a = res.product_tmpl_id.property_account_income.id
1355             if not a:
1356                 a = res.categ_id.property_account_income_categ.id
1357         else:
1358             a = res.product_tmpl_id.property_account_expense.id
1359             if not a:
1360                 a = res.categ_id.property_account_expense_categ.id
1361         a = fpos_obj.map_account(cr, uid, fpos, a)
1362         if a:
1363             result['account_id'] = a
1364
1365         if type in ('out_invoice', 'out_refund'):
1366             taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1367         else:
1368             taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1369         tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1370
1371         if type in ('in_invoice', 'in_refund'):
1372             result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1373         else:
1374             result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1375         result['name'] = res.partner_ref
1376
1377         domain = {}
1378         result['uos_id'] = res.uom_id.id or uom or False
1379         result['note'] = res.description
1380         if result['uos_id']:
1381             res2 = res.uom_id.category_id.id
1382             if res2:
1383                 domain = {'uos_id':[('category_id','=',res2 )]}
1384
1385         res_final = {'value':result, 'domain':domain}
1386
1387         if not company_id or not currency_id:
1388             return res_final
1389
1390         company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1391         currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1392
1393         if company.currency_id.id != currency.id:
1394             if type in ('in_invoice', 'in_refund'):
1395                 res_final['value']['price_unit'] = res.standard_price
1396             new_price = res_final['value']['price_unit'] * currency.rate
1397             res_final['value']['price_unit'] = new_price
1398
1399         if uom:
1400             uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1401             if res.uom_id.category_id.id == uom.category_id.id:
1402                 new_price = res_final['value']['price_unit'] * uom.factor_inv
1403                 res_final['value']['price_unit'] = new_price
1404         return res_final
1405
1406     def uos_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, currency_id=False, context=None, company_id=None):
1407         if context is None:
1408             context = {}
1409         company_id = company_id if company_id != None else context.get('company_id',False)
1410         context = dict(context)
1411         context.update({'company_id': company_id})
1412         warning = {}
1413         res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, address_invoice_id, currency_id, context=context)
1414         if 'uos_id' in res['value']:
1415             del res['value']['uos_id']
1416         if not uom:
1417             res['value']['price_unit'] = 0.0
1418         if product and uom:
1419             prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1420             prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1421             if prod.uom_id.category_id.id != prod_uom.category_id.id:
1422                  warning = {
1423                     'title': _('Warning!'),
1424                     'message': _('You selected an Unit of Measure which is not compatible with the product.')
1425             }
1426             return {'value': res['value'], 'warning': warning}
1427         return res
1428
1429     def move_line_get(self, cr, uid, invoice_id, context=None):
1430         res = []
1431         tax_obj = self.pool.get('account.tax')
1432         cur_obj = self.pool.get('res.currency')
1433         if context is None:
1434             context = {}
1435         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1436         company_currency = inv.company_id.currency_id.id
1437
1438         for line in inv.invoice_line:
1439             mres = self.move_line_get_item(cr, uid, line, context)
1440             if not mres:
1441                 continue
1442             res.append(mres)
1443             tax_code_found= False
1444             for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1445                     (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1446                     line.quantity, inv.address_invoice_id.id, line.product_id,
1447                     inv.partner_id)['taxes']:
1448
1449                 if inv.type in ('out_invoice', 'in_invoice'):
1450                     tax_code_id = tax['base_code_id']
1451                     tax_amount = line.price_subtotal * tax['base_sign']
1452                 else:
1453                     tax_code_id = tax['ref_base_code_id']
1454                     tax_amount = line.price_subtotal * tax['ref_base_sign']
1455
1456                 if tax_code_found:
1457                     if not tax_code_id:
1458                         continue
1459                     res.append(self.move_line_get_item(cr, uid, line, context))
1460                     res[-1]['price'] = 0.0
1461                     res[-1]['account_analytic_id'] = False
1462                 elif not tax_code_id:
1463                     continue
1464                 tax_code_found = True
1465
1466                 res[-1]['tax_code_id'] = tax_code_id
1467                 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1468         return res
1469
1470     def move_line_get_item(self, cr, uid, line, context=None):
1471         return {
1472             'type':'src',
1473             'name': line.name[:64],
1474             'price_unit':line.price_unit,
1475             'quantity':line.quantity,
1476             'price':line.price_subtotal,
1477             'account_id':line.account_id.id,
1478             'product_id':line.product_id.id,
1479             'uos_id':line.uos_id.id,
1480             'account_analytic_id':line.account_analytic_id.id,
1481             'taxes':line.invoice_line_tax_id,
1482         }
1483     #
1484     # Set the tax field according to the account and the fiscal position
1485     #
1486     def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1487         if not account_id:
1488             return {}
1489         taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1490         fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1491         tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1492
1493         product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1494                                                        partner_id=partner_id, fposition_id=fposition_id)
1495         unique_tax_ids = set(tax_ids)
1496         if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1497             unique_tax_ids |= set(product_change_result['value']['invoice_line_tax_id'])
1498         return {'value':{'invoice_line_tax_id': list(unique_tax_ids)}}
1499
1500 account_invoice_line()
1501
1502 class account_invoice_tax(osv.osv):
1503     _name = "account.invoice.tax"
1504     _description = "Invoice Tax"
1505
1506     def _count_factor(self, cr, uid, ids, name, args, context=None):
1507         res = {}
1508         for invoice_tax in self.browse(cr, uid, ids, context=context):
1509             res[invoice_tax.id] = {
1510                 'factor_base': 1.0,
1511                 'factor_tax': 1.0,
1512             }
1513             if invoice_tax.amount <> 0.0:
1514                 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1515                 res[invoice_tax.id]['factor_tax'] = factor_tax
1516
1517             if invoice_tax.base <> 0.0:
1518                 factor_base = invoice_tax.base_amount / invoice_tax.base
1519                 res[invoice_tax.id]['factor_base'] = factor_base
1520
1521         return res
1522
1523     _columns = {
1524         'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1525         'name': fields.char('Tax Description', size=64, required=True),
1526         'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1527         'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1528         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1529         'manual': fields.boolean('Manual'),
1530         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1531         'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1532         'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1533         'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1534         'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1535         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1536         'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1537         'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1538     }
1539
1540     def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1541         cur_obj = self.pool.get('res.currency')
1542         company_obj = self.pool.get('res.company')
1543         company_currency = False
1544         factor = 1
1545         if ids:
1546             factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1547         if company_id:
1548             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1549         if currency_id and company_currency:
1550             base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1551         return {'value': {'base_amount':base}}
1552
1553     def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1554         cur_obj = self.pool.get('res.currency')
1555         company_obj = self.pool.get('res.company')
1556         company_currency = False
1557         factor = 1
1558         if ids:
1559             factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1560         if company_id:
1561             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1562         if currency_id and company_currency:
1563             amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1564         return {'value': {'tax_amount': amount}}
1565
1566     _order = 'sequence'
1567     _defaults = {
1568         'manual': 1,
1569         'base_amount': 0.0,
1570         'tax_amount': 0.0,
1571     }
1572     def compute(self, cr, uid, invoice_id, context=None):
1573         tax_grouped = {}
1574         tax_obj = self.pool.get('account.tax')
1575         cur_obj = self.pool.get('res.currency')
1576         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1577         cur = inv.currency_id
1578         company_currency = inv.company_id.currency_id.id
1579
1580         for line in inv.invoice_line:
1581             for tax in tax_obj.compute_all(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)['taxes']:
1582                 tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
1583                 val={}
1584                 val['invoice_id'] = inv.id
1585                 val['name'] = tax['name']
1586                 val['amount'] = tax['amount']
1587                 val['manual'] = False
1588                 val['sequence'] = tax['sequence']
1589                 val['base'] = tax['price_unit'] * line['quantity']
1590
1591                 if inv.type in ('out_invoice','in_invoice'):
1592                     val['base_code_id'] = tax['base_code_id']
1593                     val['tax_code_id'] = tax['tax_code_id']
1594                     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)
1595                     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)
1596                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
1597                 else:
1598                     val['base_code_id'] = tax['ref_base_code_id']
1599                     val['tax_code_id'] = tax['ref_tax_code_id']
1600                     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)
1601                     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)
1602                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
1603
1604                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1605                 if not key in tax_grouped:
1606                     tax_grouped[key] = val
1607                 else:
1608                     tax_grouped[key]['amount'] += val['amount']
1609                     tax_grouped[key]['base'] += val['base']
1610                     tax_grouped[key]['base_amount'] += val['base_amount']
1611                     tax_grouped[key]['tax_amount'] += val['tax_amount']
1612
1613         for t in tax_grouped.values():
1614             t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1615             t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1616             t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1617             t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1618         return tax_grouped
1619
1620     def move_line_get(self, cr, uid, invoice_id):
1621         res = []
1622         cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1623         for t in cr.dictfetchall():
1624             if not t['amount'] \
1625                     and not t['tax_code_id'] \
1626                     and not t['tax_amount']:
1627                 continue
1628             res.append({
1629                 'type':'tax',
1630                 'name':t['name'],
1631                 'price_unit': t['amount'],
1632                 'quantity': 1,
1633                 'price': t['amount'] or 0.0,
1634                 'account_id': t['account_id'],
1635                 'tax_code_id': t['tax_code_id'],
1636                 'tax_amount': t['tax_amount']
1637             })
1638         return res
1639
1640 account_invoice_tax()
1641
1642
1643 class res_partner(osv.osv):
1644     """ Inherits partner and adds invoice information in the partner form """
1645     _inherit = 'res.partner'
1646     _columns = {
1647         'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1648     }
1649
1650     def copy(self, cr, uid, id, default=None, context=None):
1651         default = default or {}
1652         default.update({'invoice_ids' : []})
1653         return super(res_partner, self).copy(cr, uid, id, default, context)
1654
1655 res_partner()
1656
1657 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: