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