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