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