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