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