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