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