[FIX] account: partner balance general totals
[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.bank_ids:
536                 bank_id = p.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, tax.account_analytic_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 and inv.reference 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_cancel(self, cr, uid, ids, context=None):
1126         if context is None:
1127             context = {}
1128         account_move_obj = self.pool.get('account.move')
1129         invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1130         move_ids = [] # ones that we will need to remove
1131         for i in invoices:
1132             if i['move_id']:
1133                 move_ids.append(i['move_id'][0])
1134             if i['payment_ids']:
1135                 account_move_line_obj = self.pool.get('account.move.line')
1136                 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1137                 for move_line in pay_ids:
1138                     if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1139                         raise osv.except_osv(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
1140
1141         # First, set the invoices as cancelled and detach the move ids
1142         self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1143         if move_ids:
1144             # second, invalidate the move(s)
1145             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1146             # delete the move this invoice was pointing to
1147             # Note that the corresponding move_lines and move_reconciles
1148             # will be automatically deleted too
1149             account_move_obj.unlink(cr, uid, move_ids, context=context)
1150         self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1151         return True
1152
1153     ###################
1154
1155     def list_distinct_taxes(self, cr, uid, ids):
1156         invoices = self.browse(cr, uid, ids)
1157         taxes = {}
1158         for inv in invoices:
1159             for tax in inv.tax_line:
1160                 if not tax['name'] in taxes:
1161                     taxes[tax['name']] = {'name': tax['name']}
1162         return taxes.values()
1163
1164     def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1165         #TODO: implement messages system
1166         return True
1167
1168     def name_get(self, cr, uid, ids, context=None):
1169         if not ids:
1170             return []
1171         types = {
1172                 'out_invoice': _('Invoice'),
1173                 'in_invoice': _('Supplier Invoice'),
1174                 'out_refund': _('Refund'),
1175                 'in_refund': _('Supplier Refund'),
1176                 }
1177         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')]
1178
1179     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1180         if not args:
1181             args = []
1182         if context is None:
1183             context = {}
1184         ids = []
1185         if name:
1186             ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1187         if not ids:
1188             ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1189         return self.name_get(cr, user, ids, context)
1190
1191     def _refund_cleanup_lines(self, cr, uid, lines, context=None):
1192         """Convert records to dict of values suitable for one2many line creation
1193
1194             :param list(browse_record) lines: records to convert
1195             :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...]
1196         """
1197         clean_lines = []
1198         for line in lines:
1199             clean_line = {}
1200             for field in line._all_columns.keys():
1201                 if line._all_columns[field].column._type == 'many2one':
1202                     clean_line[field] = line[field].id
1203                 elif line._all_columns[field].column._type not in ['many2many','one2many']:
1204                     clean_line[field] = line[field]
1205                 elif field == 'invoice_line_tax_id':
1206                     tax_list = []
1207                     for tax in line[field]:
1208                         tax_list.append(tax.id)
1209                     clean_line[field] = [(6,0, tax_list)]
1210             clean_lines.append(clean_line)
1211         return map(lambda x: (0,0,x), clean_lines)
1212
1213     def _prepare_refund(self, cr, uid, invoice, date=None, period_id=None, description=None, journal_id=None, context=None):
1214         """Prepare the dict of values to create the new refund from the invoice.
1215             This method may be overridden to implement custom
1216             refund generation (making sure to call super() to establish
1217             a clean extension chain).
1218
1219             :param integer invoice_id: id of the invoice to refund
1220             :param dict invoice: read of the invoice to refund
1221             :param string date: refund creation date from the wizard
1222             :param integer period_id: force account.period from the wizard
1223             :param string description: description of the refund from the wizard
1224             :param integer journal_id: account.journal from the wizard
1225             :return: dict of value to create() the refund
1226         """
1227         obj_journal = self.pool.get('account.journal')
1228
1229         type_dict = {
1230             'out_invoice': 'out_refund', # Customer Invoice
1231             'in_invoice': 'in_refund',   # Supplier Invoice
1232             'out_refund': 'out_invoice', # Customer Refund
1233             'in_refund': 'in_invoice',   # Supplier Refund
1234         }
1235         invoice_data = {}
1236         for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id',
1237                 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']:
1238             if invoice._all_columns[field].column._type == 'many2one':
1239                 invoice_data[field] = invoice[field].id
1240             else:
1241                 invoice_data[field] = invoice[field] if invoice[field] else False
1242
1243         invoice_lines = self._refund_cleanup_lines(cr, uid, invoice.invoice_line, context=context)
1244
1245         tax_lines = filter(lambda l: l['manual'], invoice.tax_line)
1246         tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines, context=context)
1247         if journal_id:
1248             refund_journal_ids = [journal_id]
1249         elif invoice['type'] == 'in_invoice':
1250             refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')], context=context)
1251         else:
1252             refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')], context=context)
1253
1254         if not date:
1255             date = fields.date.context_today(self, cr, uid, context=context)
1256         invoice_data.update({
1257             'type': type_dict[invoice['type']],
1258             'date_invoice': date,
1259             'state': 'draft',
1260             'number': False,
1261             'invoice_line': invoice_lines,
1262             'tax_line': tax_lines,
1263             'journal_id': refund_journal_ids and refund_journal_ids[0] or False,
1264         })
1265         if period_id:
1266             invoice_data['period_id'] = period_id
1267         if description:
1268             invoice_data['name'] = description
1269         return invoice_data
1270
1271     def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None, context=None):
1272         new_ids = []
1273         for invoice in self.browse(cr, uid, ids, context=context):
1274             invoice = self._prepare_refund(cr, uid, invoice,
1275                                                 date=date,
1276                                                 period_id=period_id,
1277                                                 description=description,
1278                                                 journal_id=journal_id,
1279                                                 context=context)
1280             # create the new invoice
1281             new_ids.append(self.create(cr, uid, invoice, context=context))
1282
1283         return new_ids
1284
1285     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=''):
1286         if context is None:
1287             context = {}
1288         #TODO check if we can use different period for payment and the writeoff line
1289         assert len(ids)==1, "Can only pay one invoice at a time."
1290         invoice = self.browse(cr, uid, ids[0], context=context)
1291         src_account_id = invoice.account_id.id
1292         # Take the seq as name for move
1293         types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1294         direction = types[invoice.type]
1295         #take the choosen date
1296         if 'date_p' in context and context['date_p']:
1297             date=context['date_p']
1298         else:
1299             date=time.strftime('%Y-%m-%d')
1300
1301         # Take the amount in currency and the currency of the payment
1302         if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1303             amount_currency = context['amount_currency']
1304             currency_id = context['currency_id']
1305         else:
1306             amount_currency = False
1307             currency_id = False
1308
1309         pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1310         if invoice.type in ('in_invoice', 'out_invoice'):
1311             if pay_journal['type'] == 'bank':
1312                 entry_type = 'bank_pay_voucher' # Bank payment
1313             else:
1314                 entry_type = 'pay_voucher' # Cash payment
1315         else:
1316             entry_type = 'cont_voucher'
1317         if invoice.type in ('in_invoice', 'in_refund'):
1318             ref = invoice.reference
1319         else:
1320             ref = self._convert_ref(cr, uid, invoice.number)
1321         partner = self.pool['res.partner']._find_accounting_partner(invoice.partner_id)
1322         # Pay attention to the sign for both debit/credit AND amount_currency
1323         l1 = {
1324             'debit': direction * pay_amount>0 and direction * pay_amount,
1325             'credit': direction * pay_amount<0 and - direction * pay_amount,
1326             'account_id': src_account_id,
1327             'partner_id': partner.id,
1328             'ref':ref,
1329             'date': date,
1330             'currency_id':currency_id,
1331             'amount_currency':amount_currency and direction * amount_currency or 0.0,
1332             'company_id': invoice.company_id.id,
1333         }
1334         l2 = {
1335             'debit': direction * pay_amount<0 and - direction * pay_amount,
1336             'credit': direction * pay_amount>0 and direction * pay_amount,
1337             'account_id': pay_account_id,
1338             'partner_id': partner.id,
1339             'ref':ref,
1340             'date': date,
1341             'currency_id':currency_id,
1342             'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1343             'company_id': invoice.company_id.id,
1344         }
1345
1346         if not name:
1347             name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1348         l1['name'] = name
1349         l2['name'] = name
1350
1351         lines = [(0, 0, l1), (0, 0, l2)]
1352         move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1353         move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1354
1355         line_ids = []
1356         total = 0.0
1357         line = self.pool.get('account.move.line')
1358         move_ids = [move_id,]
1359         if invoice.move_id:
1360             move_ids.append(invoice.move_id.id)
1361         cr.execute('SELECT id FROM account_move_line '\
1362                    'WHERE move_id IN %s',
1363                    ((move_id, invoice.move_id.id),))
1364         lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1365         for l in lines+invoice.payment_ids:
1366             if l.account_id.id == src_account_id:
1367                 line_ids.append(l.id)
1368                 total += (l.debit or 0.0) - (l.credit or 0.0)
1369
1370         inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1371         if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1372             self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1373         else:
1374             code = invoice.currency_id.symbol
1375             # TODO: use currency's formatting function
1376             msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \
1377                     (pay_amount, code, invoice.amount_total, code, total, code)
1378             self.message_post(cr, uid, [inv_id], body=msg, context=context)
1379             self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1380
1381         # Update the stored value (fields.function), so we write to trigger recompute
1382         self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1383         return True
1384
1385
1386 class account_invoice_line(osv.osv):
1387
1388     def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1389         res = {}
1390         tax_obj = self.pool.get('account.tax')
1391         cur_obj = self.pool.get('res.currency')
1392         for line in self.browse(cr, uid, ids):
1393             price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1394             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)
1395             res[line.id] = taxes['total']
1396             if line.invoice_id:
1397                 cur = line.invoice_id.currency_id
1398                 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1399         return res
1400
1401     def _price_unit_default(self, cr, uid, context=None):
1402         if context is None:
1403             context = {}
1404         if context.get('check_total', False):
1405             t = context['check_total']
1406             for l in context.get('invoice_line', {}):
1407                 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1408                     tax_obj = self.pool.get('account.tax')
1409                     p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1410                     t = t - (p * l[2].get('quantity'))
1411                     taxes = l[2].get('invoice_line_tax_id')
1412                     if len(taxes[0]) >= 3 and taxes[0][2]:
1413                         taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1414                         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']:
1415                             t = t - tax['amount']
1416             return t
1417         return 0
1418
1419     _name = "account.invoice.line"
1420     _description = "Invoice Line"
1421     _order = "invoice_id,sequence,id"
1422     _columns = {
1423         'name': fields.text('Description', required=True),
1424         'origin': fields.char('Source Document', size=256, help="Reference of the document that produced this invoice."),
1425         'sequence': fields.integer('Sequence', help="Gives the sequence of this line when displaying the invoice."),
1426         'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1427         'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null', select=True),
1428         'product_id': fields.many2one('product.product', 'Product', ondelete='set null', select=True),
1429         '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."),
1430         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
1431         'price_subtotal': fields.function(_amount_line, string='Amount', type="float",
1432             digits_compute= dp.get_precision('Account'), store=True),
1433         'quantity': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
1434         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')),
1435         'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1436         'account_analytic_id':  fields.many2one('account.analytic.account', 'Analytic Account'),
1437         'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1438         'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1439     }
1440
1441     def _default_account_id(self, cr, uid, context=None):
1442         # XXX this gets the default account for the user's company,
1443         # it should get the default account for the invoice's company
1444         # however, the invoice's company does not reach this point
1445         if context is None:
1446             context = {}
1447         if context.get('type') in ('out_invoice','out_refund'):
1448             prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context)
1449         else:
1450             prop = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category', context=context)
1451         return prop and prop.id or False
1452
1453     _defaults = {
1454         'quantity': 1,
1455         'discount': 0.0,
1456         'price_unit': _price_unit_default,
1457         'account_id': _default_account_id,
1458         'sequence': 10,
1459     }
1460
1461     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1462         if context is None:
1463             context = {}
1464         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)
1465         if context.get('type', False):
1466             doc = etree.XML(res['arch'])
1467             for node in doc.xpath("//field[@name='product_id']"):
1468                 if context['type'] in ('in_invoice', 'in_refund'):
1469                     node.set('domain', "[('purchase_ok', '=', True)]")
1470                 else:
1471                     node.set('domain', "[('sale_ok', '=', True)]")
1472             res['arch'] = etree.tostring(doc)
1473         return res
1474
1475     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):
1476         if context is None:
1477             context = {}
1478         company_id = company_id if company_id != None else context.get('company_id',False)
1479         context = dict(context)
1480         context.update({'company_id': company_id, 'force_company': company_id})
1481         if not partner_id:
1482             raise osv.except_osv(_('No Partner Defined!'),_("You must first select a partner!") )
1483         if not product:
1484             if type in ('in_invoice', 'in_refund'):
1485                 return {'value': {}, 'domain':{'product_uom':[]}}
1486             else:
1487                 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1488         part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1489         fpos_obj = self.pool.get('account.fiscal.position')
1490         fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1491
1492         if part.lang:
1493             context.update({'lang': part.lang})
1494         result = {}
1495         res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1496
1497         if type in ('out_invoice','out_refund'):
1498             a = res.property_account_income.id
1499             if not a:
1500                 a = res.categ_id.property_account_income_categ.id
1501         else:
1502             a = res.property_account_expense.id
1503             if not a:
1504                 a = res.categ_id.property_account_expense_categ.id
1505         a = fpos_obj.map_account(cr, uid, fpos, a)
1506         if a:
1507             result['account_id'] = a
1508
1509         if type in ('out_invoice', 'out_refund'):
1510             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)
1511         else:
1512             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)
1513         tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1514
1515         if type in ('in_invoice', 'in_refund'):
1516             result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1517         else:
1518             result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1519         result['name'] = res.partner_ref
1520
1521         result['uos_id'] = uom_id or res.uom_id.id
1522         if res.description:
1523             result['name'] += '\n'+res.description
1524
1525         domain = {'uos_id':[('category_id','=',res.uom_id.category_id.id)]}
1526
1527         res_final = {'value':result, 'domain':domain}
1528
1529         if not company_id or not currency_id:
1530             return res_final
1531
1532         company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1533         currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1534
1535         if company.currency_id.id != currency.id:
1536             if type in ('in_invoice', 'in_refund'):
1537                 res_final['value']['price_unit'] = res.standard_price
1538             new_price = res_final['value']['price_unit'] * currency.rate
1539             res_final['value']['price_unit'] = new_price
1540
1541         if result['uos_id'] and result['uos_id'] != res.uom_id.id:
1542             selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context)
1543             new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id'])
1544             res_final['value']['price_unit'] = new_price
1545         return res_final
1546
1547     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):
1548         if context is None:
1549             context = {}
1550         company_id = company_id if company_id != None else context.get('company_id',False)
1551         context = dict(context)
1552         context.update({'company_id': company_id})
1553         warning = {}
1554         res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context)
1555         if not uom:
1556             res['value']['price_unit'] = 0.0
1557         if product and uom:
1558             prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1559             prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1560             if prod.uom_id.category_id.id != prod_uom.category_id.id:
1561                 warning = {
1562                     'title': _('Warning!'),
1563                     'message': _('The selected unit of measure is not compatible with the unit of measure of the product.')
1564                 }
1565                 res['value'].update({'uos_id': prod.uom_id.id})
1566             return {'value': res['value'], 'warning': warning}
1567         return res
1568
1569     def move_line_get(self, cr, uid, invoice_id, context=None):
1570         res = []
1571         tax_obj = self.pool.get('account.tax')
1572         cur_obj = self.pool.get('res.currency')
1573         if context is None:
1574             context = {}
1575         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1576         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
1577         for line in inv.invoice_line:
1578             mres = self.move_line_get_item(cr, uid, line, context)
1579             mres['invl_id'] = line.id
1580             res.append(mres)
1581             tax_code_found= False
1582             for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1583                     (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1584                     line.quantity, line.product_id,
1585                     inv.partner_id)['taxes']:
1586
1587                 if inv.type in ('out_invoice', 'in_invoice'):
1588                     tax_code_id = tax['base_code_id']
1589                     tax_amount = line.price_subtotal * tax['base_sign']
1590                 else:
1591                     tax_code_id = tax['ref_base_code_id']
1592                     tax_amount = line.price_subtotal * tax['ref_base_sign']
1593
1594                 if tax_code_found:
1595                     if not tax_code_id:
1596                         continue
1597                     res.append(self.move_line_get_item(cr, uid, line, context))
1598                     res[-1]['price'] = 0.0
1599                     res[-1]['account_analytic_id'] = False
1600                 elif not tax_code_id:
1601                     continue
1602                 tax_code_found = True
1603
1604                 res[-1]['tax_code_id'] = tax_code_id
1605                 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1606         return res
1607
1608     def move_line_get_item(self, cr, uid, line, context=None):
1609         return {
1610             'type':'src',
1611             'name': line.name.split('\n')[0][:64],
1612             'price_unit':line.price_unit,
1613             'quantity':line.quantity,
1614             'price':line.price_subtotal,
1615             'account_id':line.account_id.id,
1616             'product_id':line.product_id.id,
1617             'uos_id':line.uos_id.id,
1618             'account_analytic_id':line.account_analytic_id.id,
1619             'taxes':line.invoice_line_tax_id,
1620         }
1621     #
1622     # Set the tax field according to the account and the fiscal position
1623     #
1624     def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1625         if not account_id:
1626             return {}
1627         unique_tax_ids = []
1628         fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1629         account = self.pool.get('account.account').browse(cr, uid, account_id)
1630         if not product_id:
1631             taxes = account.tax_ids
1632             unique_tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1633         else:
1634             product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1635                 partner_id=partner_id, fposition_id=fposition_id,
1636                 company_id=account.company_id.id)
1637             if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1638                 unique_tax_ids = product_change_result['value']['invoice_line_tax_id']
1639         return {'value':{'invoice_line_tax_id': unique_tax_ids}}
1640
1641 account_invoice_line()
1642
1643 class account_invoice_tax(osv.osv):
1644     _name = "account.invoice.tax"
1645     _description = "Invoice Tax"
1646
1647     def _count_factor(self, cr, uid, ids, name, args, context=None):
1648         res = {}
1649         for invoice_tax in self.browse(cr, uid, ids, context=context):
1650             res[invoice_tax.id] = {
1651                 'factor_base': 1.0,
1652                 'factor_tax': 1.0,
1653             }
1654             if invoice_tax.amount <> 0.0:
1655                 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1656                 res[invoice_tax.id]['factor_tax'] = factor_tax
1657
1658             if invoice_tax.base <> 0.0:
1659                 factor_base = invoice_tax.base_amount / invoice_tax.base
1660                 res[invoice_tax.id]['factor_base'] = factor_base
1661
1662         return res
1663
1664     _columns = {
1665         'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1666         'name': fields.char('Tax Description', size=64, required=True),
1667         'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1668         'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
1669         'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1670         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1671         'manual': fields.boolean('Manual'),
1672         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1673         'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1674         'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1675         'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1676         'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1677         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1678         'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1679         'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1680     }
1681
1682     def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1683         cur_obj = self.pool.get('res.currency')
1684         company_obj = self.pool.get('res.company')
1685         company_currency = False
1686         factor = 1
1687         if ids:
1688             factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1689         if company_id:
1690             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1691         if currency_id and company_currency:
1692             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)
1693         return {'value': {'base_amount':base}}
1694
1695     def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1696         cur_obj = self.pool.get('res.currency')
1697         company_obj = self.pool.get('res.company')
1698         company_currency = False
1699         factor = 1
1700         if ids:
1701             factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1702         if company_id:
1703             company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1704         if currency_id and company_currency:
1705             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)
1706         return {'value': {'tax_amount': amount}}
1707
1708     _order = 'sequence'
1709     _defaults = {
1710         'manual': 1,
1711         'base_amount': 0.0,
1712         'tax_amount': 0.0,
1713     }
1714     def compute(self, cr, uid, invoice_id, context=None):
1715         tax_grouped = {}
1716         tax_obj = self.pool.get('account.tax')
1717         cur_obj = self.pool.get('res.currency')
1718         inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1719         cur = inv.currency_id
1720         company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id
1721         for line in inv.invoice_line:
1722             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']:
1723                 val={}
1724                 val['invoice_id'] = inv.id
1725                 val['name'] = tax['name']
1726                 val['amount'] = tax['amount']
1727                 val['manual'] = False
1728                 val['sequence'] = tax['sequence']
1729                 val['base'] = cur_obj.round(cr, uid, cur, tax['price_unit'] * line['quantity'])
1730
1731                 if inv.type in ('out_invoice','in_invoice'):
1732                     val['base_code_id'] = tax['base_code_id']
1733                     val['tax_code_id'] = tax['tax_code_id']
1734                     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)
1735                     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)
1736                     val['account_id'] = tax['account_collected_id'] or line.account_id.id
1737                     val['account_analytic_id'] = tax['account_analytic_collected_id']
1738                 else:
1739                     val['base_code_id'] = tax['ref_base_code_id']
1740                     val['tax_code_id'] = tax['ref_tax_code_id']
1741                     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)
1742                     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)
1743                     val['account_id'] = tax['account_paid_id'] or line.account_id.id
1744                     val['account_analytic_id'] = tax['account_analytic_paid_id']
1745
1746                 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1747                 if not key in tax_grouped:
1748                     tax_grouped[key] = val
1749                 else:
1750                     tax_grouped[key]['amount'] += val['amount']
1751                     tax_grouped[key]['base'] += val['base']
1752                     tax_grouped[key]['base_amount'] += val['base_amount']
1753                     tax_grouped[key]['tax_amount'] += val['tax_amount']
1754
1755         for t in tax_grouped.values():
1756             t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1757             t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1758             t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1759             t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1760         return tax_grouped
1761
1762     def move_line_get(self, cr, uid, invoice_id):
1763         res = []
1764         cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1765         for t in cr.dictfetchall():
1766             if not t['amount'] \
1767                     and not t['tax_code_id'] \
1768                     and not t['tax_amount']:
1769                 continue
1770             res.append({
1771                 'type':'tax',
1772                 'name':t['name'],
1773                 'price_unit': t['amount'],
1774                 'quantity': 1,
1775                 'price': t['amount'] or 0.0,
1776                 'account_id': t['account_id'],
1777                 'tax_code_id': t['tax_code_id'],
1778                 'tax_amount': t['tax_amount'],
1779                 'account_analytic_id': t['account_analytic_id'],
1780             })
1781         return res
1782
1783
1784 class res_partner(osv.osv):
1785     """ Inherits partner and adds invoice information in the partner form """
1786     _inherit = 'res.partner'
1787     _columns = {
1788         'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1789     }
1790
1791     def _find_accounting_partner(self, partner):
1792         '''
1793         Find the partner for which the accounting entries will be created
1794         '''
1795         # FIXME: after 7.0, to replace by function field partner.commercial_partner_id
1796
1797         #if the chosen partner is not a company and has a parent company, use the parent for the journal entries
1798         #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
1799         while not partner.is_company and partner.parent_id:
1800             partner = partner.parent_id
1801         return partner
1802
1803     def copy(self, cr, uid, id, default=None, context=None):
1804         default = default or {}
1805         default.update({'invoice_ids' : []})
1806         return super(res_partner, self).copy(cr, uid, id, default, context)
1807
1808
1809 class mail_compose_message(osv.Model):
1810     _inherit = 'mail.compose.message'
1811
1812     def send_mail(self, cr, uid, ids, context=None):
1813         context = context or {}
1814         if context.get('default_model') == 'account.invoice' and context.get('default_res_id') and context.get('mark_invoice_as_sent'):
1815             context = dict(context, mail_post_autofollow=True)
1816             self.pool.get('account.invoice').write(cr, uid, [context['default_res_id']], {'sent': True}, context=context)
1817         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1818
1819 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: