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