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