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