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