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