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