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