1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
23 from lxml import etree
24 import decimal_precision as dp
28 from osv import fields, osv, orm
29 from tools.translate import _
31 class account_invoice(osv.osv):
32 def _amount_all(self, cr, uid, ids, name, args, context=None):
34 for invoice in self.browse(cr, uid, ids, context=context):
36 'amount_untaxed': 0.0,
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']
47 def _get_journal(self, cr, uid, context=None):
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)],
58 return res and res[0] or False
60 def _get_currency(self, cr, uid, context=None):
62 journal_id = self._get_journal(cr, uid, context=context)
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
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)
73 raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,))
76 def _get_type(self, cr, uid, context=None):
79 return context.get('type', 'out_invoice')
81 def _reconciled(self, cr, uid, ids, name, args, context=None):
83 wf_service = netsvc.LocalService("workflow")
84 for inv in self.browse(cr, uid, ids, context=context):
85 res[inv.id] = self.test_paid(cr, uid, [inv.id])
86 if not res[inv.id] and inv.state == 'paid':
87 wf_service.trg_validate(uid, 'account.invoice', inv.id, 'open_test', cr)
90 def _get_reference_type(self, cr, uid, context=None):
91 return [('none', _('Free Reference'))]
93 def _amount_residual(self, cr, uid, ids, name, args, context=None):
95 for invoice in self.browse(cr, uid, ids, context=context):
96 result[invoice.id] = 0.0
98 for m in invoice.move_id.line_id:
99 if m.account_id.type in ('receivable','payable'):
100 result[invoice.id] += m.amount_residual_currency
103 # Give Journal Items related to the payment reconciled to this invoice
104 # Return ids of partial and total payments related to the selected invoices
105 def _get_lines(self, cr, uid, ids, name, arg, context=None):
107 for invoice in self.browse(cr, uid, ids, context=context):
110 if not invoice.move_id:
112 data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
114 for line in data_lines:
116 if line.reconcile_id:
117 ids_line = line.reconcile_id.line_id
118 elif line.reconcile_partial_id:
119 ids_line = line.reconcile_partial_id.line_partial_ids
120 l = map(lambda x: x.id, ids_line)
121 partial_ids.append(line.id)
122 res[id] =[x for x in l if x <> line.id and x not in partial_ids]
125 def _get_invoice_line(self, cr, uid, ids, context=None):
127 for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
128 result[line.invoice_id.id] = True
131 def _get_invoice_tax(self, cr, uid, ids, context=None):
133 for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
134 result[tax.invoice_id.id] = True
137 def _compute_lines(self, cr, uid, ids, name, args, context=None):
139 for invoice in self.browse(cr, uid, ids, context=context):
143 for m in invoice.move_id.line_id:
146 temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
147 elif m.reconcile_partial_id:
148 temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
149 lines += [x for x in temp_lines if x not in lines]
152 lines = filter(lambda x: x not in src, lines)
153 result[invoice.id] = lines
156 def _get_invoice_from_line(self, cr, uid, ids, context=None):
158 for line in self.pool.get('account.move.line').browse(cr, uid, ids, context=context):
159 if line.reconcile_partial_id:
160 for line2 in line.reconcile_partial_id.line_partial_ids:
161 move[line2.move_id.id] = True
162 if line.reconcile_id:
163 for line2 in line.reconcile_id.line_id:
164 move[line2.move_id.id] = True
167 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
170 def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
172 for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
173 for line in r.line_partial_ids:
174 move[line.move_id.id] = True
175 for line in r.line_id:
176 move[line.move_id.id] = True
180 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
183 _name = "account.invoice"
184 _description = 'Invoice'
188 'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
189 'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
190 'type': fields.selection([
191 ('out_invoice','Customer Invoice'),
192 ('in_invoice','Supplier Invoice'),
193 ('out_refund','Customer Refund'),
194 ('in_refund','Supplier Refund'),
195 ],'Type', readonly=True, select=True, change_default=True),
197 'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
198 'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
199 'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
200 'reference_type': fields.selection(_get_reference_type, 'Reference Type',
201 required=True, readonly=True, states={'draft':[('readonly',False)]}),
202 'comment': fields.text('Additional Information'),
204 'state': fields.selection([
206 ('proforma','Pro-forma'),
207 ('proforma2','Pro-forma'),
210 ('cancel','Cancelled')
211 ],'State', select=True, readonly=True,
212 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
213 \n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
214 \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. \
215 \n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
216 \n* The \'Cancelled\' state is used when user cancel invoice.'),
217 'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
218 'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
219 help="If you use payment terms, the due date will be computed automatically at the generation "\
220 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. The payment term may compute several due dates, for example 50% now, 50% in one month."),
221 'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
222 'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}),
223 'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft':[('readonly',False)]}),
224 'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
225 help="If you use payment terms, the due date will be computed automatically at the generation "\
226 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
227 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
228 '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)]}),
230 'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
231 'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
232 'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
234 'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
235 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed',
237 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
238 'account.invoice.tax': (_get_invoice_tax, None, 20),
239 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
242 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
244 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
245 'account.invoice.tax': (_get_invoice_tax, None, 20),
246 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
249 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
251 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
252 'account.invoice.tax': (_get_invoice_tax, None, 20),
253 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
256 'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
257 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
258 'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
259 'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
260 'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
262 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
263 'account.move.line': (_get_invoice_from_line, None, 50),
264 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
265 }, 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."),
266 'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
267 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)]}),
268 'move_lines':fields.function(_get_lines, type='many2many', relation='account.move.line', string='Entry Lines'),
269 'residual': fields.function(_amount_residual, digits_compute=dp.get_precision('Account'), string='Balance',
271 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line','move_id'], 50),
272 'account.invoice.tax': (_get_invoice_tax, None, 50),
273 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 50),
274 'account.move.line': (_get_invoice_from_line, None, 50),
275 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
277 help="Remaining amount due."),
278 'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
279 'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
280 'user_id': fields.many2one('res.users', 'Salesman', readonly=True, states={'draft':[('readonly',False)]}),
281 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
286 'journal_id': _get_journal,
287 'currency_id': _get_currency,
288 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.invoice', context=c),
289 'reference_type': 'none',
291 'internal_number': False,
292 'user_id': lambda s, cr, u, c: u,
295 ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
298 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
299 journal_obj = self.pool.get('account.journal')
303 if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
304 partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
306 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
308 if view_type == 'form':
309 if partner['supplier'] and not partner['customer']:
310 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
312 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
313 if view_id and isinstance(view_id, (list, tuple)):
315 res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
317 type = context.get('journal_type', False)
318 for field in res['fields']:
319 if field == 'journal_id' and type:
320 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
321 res['fields'][field]['selection'] = journal_select
323 doc = etree.XML(res['arch'])
325 if context.get('type', False):
326 for node in doc.xpath("//field[@name='partner_bank_id']"):
327 if context['type'] == 'in_refund':
328 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
329 elif context['type'] == 'out_refund':
330 node.set('domain', "[('partner_id', '=', partner_id)]")
331 res['arch'] = etree.tostring(doc)
333 if view_type == 'search':
334 if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
335 for node in doc.xpath("//group[@name='extended filter']"):
337 res['arch'] = etree.tostring(doc)
339 if view_type == 'tree':
340 partner_string = _('Customer')
341 if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
342 partner_string = _('Supplier')
343 for node in doc.xpath("//field[@name='reference']"):
344 node.set('invisible', '0')
345 for node in doc.xpath("//field[@name='partner_id']"):
346 node.set('string', partner_string)
347 res['arch'] = etree.tostring(doc)
350 def get_log_context(self, cr, uid, context=None):
353 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
354 view_id = res and res[1] or False
355 context['view_id'] = view_id
358 def create(self, cr, uid, vals, context=None):
362 res = super(account_invoice, self).create(cr, uid, vals, context)
363 for inv_id, name in self.name_get(cr, uid, [res], context=context):
365 if vals.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
366 ctx = self.get_log_context(cr, uid, context=ctx)
367 message = _("Invoice '%s' is waiting for validation.") % name
368 self.log(cr, uid, inv_id, message, context=ctx)
371 if '"journal_id" viol' in e.args[0]:
372 raise orm.except_orm(_('Configuration Error!'),
373 _('There is no Accounting Journal of type Sale/Purchase defined!'))
375 raise orm.except_orm(_('Unknown Error'), str(e))
377 def confirm_paid(self, cr, uid, ids, context=None):
380 self.write(cr, uid, ids, {'state':'paid'}, context=context)
381 for inv_id, name in self.name_get(cr, uid, ids, context=context):
382 message = _("Invoice '%s' is paid.") % name
383 self.log(cr, uid, inv_id, message)
386 def unlink(self, cr, uid, ids, context=None):
389 invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
392 if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
393 unlink_ids.append(t['id'])
395 raise osv.except_osv(_('Invalid action !'), _('You can not delete an invoice which is open or paid. We suggest you to refund it instead.'))
396 osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
399 def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
400 date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
401 invoice_addr_id = False
402 contact_addr_id = False
403 partner_payment_term = False
406 fiscal_position = False
408 opt = [('uid', str(uid))]
411 opt.insert(0, ('id', partner_id))
412 res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
413 contact_addr_id = res['contact']
414 invoice_addr_id = res['invoice']
415 p = self.pool.get('res.partner').browse(cr, uid, partner_id)
417 if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
418 property_obj = self.pool.get('ir.property')
419 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
420 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
422 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
424 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
425 rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
426 pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
427 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
428 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
429 if not rec_res_id and not pay_res_id:
430 raise osv.except_osv(_('Configuration Error !'),
431 _('Can not find a chart of accounts for this company, you should create one.'))
432 account_obj = self.pool.get('account.account')
433 rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
434 pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
435 p.property_account_receivable = rec_obj_acc[0]
436 p.property_account_payable = pay_obj_acc[0]
438 if type in ('out_invoice', 'out_refund'):
439 acc_id = p.property_account_receivable.id
441 acc_id = p.property_account_payable.id
442 fiscal_position = p.property_account_position and p.property_account_position.id or False
443 partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
445 bank_id = p.bank_ids[0].id
448 'address_contact_id': contact_addr_id,
449 'address_invoice_id': invoice_addr_id,
450 'account_id': acc_id,
451 'payment_term': partner_payment_term,
452 'fiscal_position': fiscal_position
456 if type in ('in_invoice', 'in_refund'):
457 result['value']['partner_bank_id'] = bank_id
459 if payment_term != partner_payment_term:
460 if partner_payment_term:
461 to_update = self.onchange_payment_term_date_invoice(
462 cr, uid, ids, partner_payment_term, date_invoice)
463 result['value'].update(to_update['value'])
465 result['value']['date_due'] = False
467 if partner_bank_id != bank_id:
468 to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
469 result['value'].update(to_update['value'])
472 def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
475 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
476 currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
478 'currency_id': currency_id,
483 def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
484 if not payment_term_id:
487 pt_obj = self.pool.get('account.payment.term')
489 date_invoice = time.strftime('%Y-%m-%d')
491 pterm_list = pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
494 pterm_list = [line[0] for line in pterm_list]
496 res = {'value':{'date_due': pterm_list[-1]}}
498 raise osv.except_osv(_('Data Insufficient !'), _('The payment term of supplier does not have a payment term line!'))
501 def onchange_invoice_line(self, cr, uid, ids, lines):
504 def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
507 def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
510 obj_journal = self.pool.get('account.journal')
511 account_obj = self.pool.get('account.account')
512 inv_line_obj = self.pool.get('account.invoice.line')
513 if company_id and part_id and type:
515 partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
516 if partner_obj.property_account_payable and partner_obj.property_account_receivable:
517 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
518 property_obj = self.pool.get('ir.property')
519 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
520 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
522 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
524 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
525 rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
526 pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
527 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
528 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
529 if not rec_res_id and not pay_res_id:
530 raise osv.except_osv(_('Configuration Error !'),
531 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
532 if type in ('out_invoice', 'out_refund'):
536 val= {'account_id': acc_id}
539 inv_obj = self.browse(cr,uid,ids)
540 for line in inv_obj[0].invoice_line:
542 if line.account_id.company_id.id != company_id:
543 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
545 raise osv.except_osv(_('Configuration Error !'),
546 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
547 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
550 for inv_line in invoice_line:
551 obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
552 if obj_l.company_id.id != company_id:
553 raise osv.except_osv(_('Configuration Error !'),
554 _('Invoice line account company does not match with invoice company.'))
557 if company_id and type:
558 if type in ('out_invoice'):
559 journal_type = 'sale'
560 elif type in ('out_refund'):
561 journal_type = 'sale_refund'
562 elif type in ('in_refund'):
563 journal_type = 'purchase_refund'
565 journal_type = 'purchase'
566 journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
568 val['journal_id'] = journal_ids[0]
569 ir_values_obj = self.pool.get('ir.values')
570 res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
571 for r in res_journal_default:
572 if r[1] == 'journal_id' and r[2] in journal_ids:
573 val['journal_id'] = r[2]
574 if not val.get('journal_id', False):
575 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)))
576 dom = {'journal_id': [('id', 'in', journal_ids)]}
578 journal_ids = obj_journal.search(cr, uid, [])
580 return {'value': val, 'domain': dom}
582 # go from canceled state to draft state
583 def action_cancel_draft(self, cr, uid, ids, *args):
584 self.write(cr, uid, ids, {'state':'draft'})
585 wf_service = netsvc.LocalService("workflow")
587 wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
588 wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
594 # return the ids of the move lines which has the same account than the invoice
596 def move_line_id_payment_get(self, cr, uid, ids, *args):
597 if not ids: return []
598 result = self.move_line_id_payment_gets(cr, uid, ids, *args)
599 return result.get(ids[0], [])
601 def move_line_id_payment_gets(self, cr, uid, ids, *args):
603 if not ids: return res
604 cr.execute('SELECT i.id, l.id '\
605 'FROM account_move_line l '\
606 'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
608 'AND l.account_id=i.account_id',
610 for r in cr.fetchall():
611 res.setdefault(r[0], [])
612 res[r[0]].append( r[1] )
615 def copy(self, cr, uid, id, default=None, context=None):
616 default = default or {}
622 'internal_number': False,
625 if 'date_invoice' not in default:
629 if 'date_due' not in default:
633 return super(account_invoice, self).copy(cr, uid, id, default, context)
635 def test_paid(self, cr, uid, ids, *args):
636 res = self.move_line_id_payment_get(cr, uid, ids)
641 cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
642 ok = ok and bool(cr.fetchone()[0])
645 def button_reset_taxes(self, cr, uid, ids, context=None):
649 ait_obj = self.pool.get('account.invoice.tax')
651 cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
652 partner = self.browse(cr, uid, id, context=ctx).partner_id
654 ctx.update({'lang': partner.lang})
655 for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
656 ait_obj.create(cr, uid, taxe)
657 # Update the stored value (fields.function), so we write to trigger recompute
658 self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
661 def button_compute(self, cr, uid, ids, context=None, set_total=False):
662 self.button_reset_taxes(cr, uid, ids, context)
663 for inv in self.browse(cr, uid, ids, context=context):
665 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
668 def _convert_ref(self, cr, uid, ref):
669 return (ref or '').replace('/','')
671 def _get_analytic_lines(self, cr, uid, id, context=None):
674 inv = self.browse(cr, uid, id)
675 cur_obj = self.pool.get('res.currency')
677 company_currency = inv.company_id.currency_id.id
678 if inv.type in ('out_invoice', 'in_refund'):
683 iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
685 if il['account_analytic_id']:
686 if inv.type in ('in_invoice', 'in_refund'):
689 ref = self._convert_ref(cr, uid, inv.number)
690 if not inv.journal_id.analytic_journal_id:
691 raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
692 il['analytic_lines'] = [(0,0, {
694 'date': inv['date_invoice'],
695 'account_id': il['account_analytic_id'],
696 'unit_amount': il['quantity'],
697 'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
698 'product_id': il['product_id'],
699 'product_uom_id': il['uos_id'],
700 'general_account_id': il['account_id'],
701 'journal_id': inv.journal_id.analytic_journal_id.id,
706 def action_date_assign(self, cr, uid, ids, *args):
707 for inv in self.browse(cr, uid, ids):
708 res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
709 if res and res['value']:
710 self.write(cr, uid, [inv.id], res['value'])
713 def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
714 """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
715 Hook method to be overridden in additional modules to verify and possibly alter the
716 move lines to be created by an invoice, for special cases.
717 :param invoice_browse: browsable record of the invoice that is generating the move lines
718 :param move_lines: list of dictionaries with the account.move.lines (as for create())
719 :return: the (possibly updated) final move_lines to create for this invoice
723 def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
725 for tax in compute_taxes.values():
726 ait_obj.create(cr, uid, tax)
729 for tax in inv.tax_line:
732 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
734 if not key in compute_taxes:
735 raise osv.except_osv(_('Warning !'), _('Global taxes defined, but they are not in invoice lines !'))
736 base = compute_taxes[key]['base']
737 if abs(base - tax.base) > inv.company_id.currency_id.rounding:
738 raise osv.except_osv(_('Warning !'), _('Tax base different!\nClick on compute to update the tax base.'))
739 for key in compute_taxes:
740 if not key in tax_key:
741 raise osv.except_osv(_('Warning !'), _('Taxes are missing!\nClick on compute button.'))
743 def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
746 cur_obj = self.pool.get('res.currency')
747 for i in invoice_move_lines:
748 if inv.currency_id.id != company_currency:
749 i['currency_id'] = inv.currency_id.id
750 i['amount_currency'] = i['price']
751 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
752 company_currency, i['price'],
753 context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
755 i['amount_currency'] = False
756 i['currency_id'] = False
758 if inv.type in ('out_invoice','in_refund'):
760 total_currency += i['amount_currency'] or i['price']
761 i['price'] = - i['price']
764 total_currency -= i['amount_currency'] or i['price']
765 return total, total_currency, invoice_move_lines
767 def inv_line_characteristic_hashcode(self, invoice, invoice_line):
768 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
769 will be grouped together if the journal has the 'group line' option. Of course a module
770 can add fields to invoice lines that would need to be tested too before merging lines
772 return "%s-%s-%s-%s-%s"%(
773 invoice_line['account_id'],
774 invoice_line.get('tax_code_id',"False"),
775 invoice_line.get('product_id',"False"),
776 invoice_line.get('analytic_account_id',"False"),
777 invoice_line.get('date_maturity',"False"))
779 def group_lines(self, cr, uid, iml, line, inv):
780 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
781 if inv.journal_id.group_invoice_lines:
784 tmp = self.inv_line_characteristic_hashcode(inv, l)
787 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
788 line2[tmp]['debit'] = (am > 0) and am or 0.0
789 line2[tmp]['credit'] = (am < 0) and -am or 0.0
790 line2[tmp]['tax_amount'] += l['tax_amount']
791 line2[tmp]['analytic_lines'] += l['analytic_lines']
792 line2[tmp]['amount_currency'] += l['amount_currency']
793 line2[tmp]['quantity'] += l['quantity']
797 for key, val in line2.items():
798 line.append((0,0,val))
801 def action_move_create(self, cr, uid, ids, context=None):
802 """Creates invoice related analytics and financial move lines"""
803 ait_obj = self.pool.get('account.invoice.tax')
804 cur_obj = self.pool.get('res.currency')
805 period_obj = self.pool.get('account.period')
806 payment_term_obj = self.pool.get('account.payment.term')
807 journal_obj = self.pool.get('account.journal')
808 move_obj = self.pool.get('account.move')
811 for inv in self.browse(cr, uid, ids, context=context):
812 if not inv.journal_id.sequence_id:
813 raise osv.except_osv(_('Error !'), _('Please define sequence on the journal related to this invoice.'))
814 if not inv.invoice_line:
815 raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
820 ctx.update({'lang': inv.partner_id.lang})
821 if not inv.date_invoice:
822 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
823 company_currency = inv.company_id.currency_id.id
824 # create the analytical lines
825 # one move line per invoice line
826 iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
827 # check if taxes are all computed
828 compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
829 self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
831 # I disabled the check_total feature
832 #if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
833 # raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
836 total_fixed = total_percent = 0
837 for line in inv.payment_term.line_ids:
838 if line.value == 'fixed':
839 total_fixed += line.value_amount
840 if line.value == 'procent':
841 total_percent += line.value_amount
842 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
843 if (total_fixed + total_percent) > 100:
844 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."))
846 # one move line per tax line
847 iml += ait_obj.move_line_get(cr, uid, inv.id)
850 if inv.type in ('in_invoice', 'in_refund'):
852 entry_type = 'journal_pur_voucher'
853 if inv.type == 'in_refund':
854 entry_type = 'cont_voucher'
856 ref = self._convert_ref(cr, uid, inv.number)
857 entry_type = 'journal_sale_vou'
858 if inv.type == 'out_refund':
859 entry_type = 'cont_voucher'
861 diff_currency_p = inv.currency_id.id <> company_currency
862 # create one move line for the total and possibly adjust the other lines amount
865 total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
866 acc_id = inv.account_id.id
868 name = inv['name'] or '/'
871 totlines = payment_term_obj.compute(cr,
872 uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
874 res_amount_currency = total_currency
876 ctx.update({'date': inv.date_invoice})
878 if inv.currency_id.id != company_currency:
879 amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
881 amount_currency = False
883 # last line add the diff
884 res_amount_currency -= amount_currency or 0
886 if i == len(totlines):
887 amount_currency += res_amount_currency
893 'account_id': acc_id,
894 'date_maturity': t[0],
895 'amount_currency': diff_currency_p \
896 and amount_currency or False,
897 'currency_id': diff_currency_p \
898 and inv.currency_id.id or False,
906 'account_id': acc_id,
907 'date_maturity': inv.date_due or False,
908 'amount_currency': diff_currency_p \
909 and total_currency or False,
910 'currency_id': diff_currency_p \
911 and inv.currency_id.id or False,
915 date = inv.date_invoice or time.strftime('%Y-%m-%d')
916 part = inv.partner_id.id
918 line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
920 line = self.group_lines(cr, uid, iml, line, inv)
922 journal_id = inv.journal_id.id
923 journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
924 if journal.centralisation:
925 raise osv.except_osv(_('UserError'),
926 _('You cannot create an invoice on a centralised journal. Uncheck the centralised counterpart box in the related journal from the configuration menu.'))
928 line = self.finalize_invoice_move_lines(cr, uid, inv, line)
931 'ref': inv.reference and inv.reference or inv.name,
933 'journal_id': journal_id,
935 'narration':inv.comment
937 period_id = inv.period_id and inv.period_id.id or False
938 ctx.update({'company_id': inv.company_id.id})
940 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
941 period_id = period_ids and period_ids[0] or False
943 move['period_id'] = period_id
945 i[2]['period_id'] = period_id
947 move_id = move_obj.create(cr, uid, move, context=ctx)
948 new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
949 # make the invoice point to that move
950 self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
951 # Pass invoice in context in method post: used if you want to get the same
952 # account move reference when creating the same invoice after a cancelled one:
953 ctx.update({'invoice':inv})
954 move_obj.post(cr, uid, [move_id], context=ctx)
955 self._log_event(cr, uid, ids)
958 def line_get_convert(self, cr, uid, x, part, date, context=None):
960 'date_maturity': x.get('date_maturity', False),
962 'name': x['name'][:64],
964 'debit': x['price']>0 and x['price'],
965 'credit': x['price']<0 and -x['price'],
966 'account_id': x['account_id'],
967 'analytic_lines': x.get('analytic_lines', []),
968 'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
969 'currency_id': x.get('currency_id', False),
970 'tax_code_id': x.get('tax_code_id', False),
971 'tax_amount': x.get('tax_amount', False),
972 'ref': x.get('ref', False),
973 'quantity': x.get('quantity',1.00),
974 'product_id': x.get('product_id', False),
975 'product_uom_id': x.get('uos_id', False),
976 'analytic_account_id': x.get('account_analytic_id', False),
979 def action_number(self, cr, uid, ids, context=None):
982 #TODO: not correct fix but required a frech values before reading it.
983 self.write(cr, uid, ids, {})
985 for obj_inv in self.browse(cr, uid, ids, context=context):
987 invtype = obj_inv.type
988 number = obj_inv.number
989 move_id = obj_inv.move_id and obj_inv.move_id.id or False
990 reference = obj_inv.reference or ''
992 self.write(cr, uid, ids, {'internal_number':number})
994 if invtype in ('in_invoice', 'in_refund'):
996 ref = self._convert_ref(cr, uid, number)
1000 ref = self._convert_ref(cr, uid, number)
1002 cr.execute('UPDATE account_move SET ref=%s ' \
1003 'WHERE id=%s AND (ref is null OR ref = \'\')',
1005 cr.execute('UPDATE account_move_line SET ref=%s ' \
1006 'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1008 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1009 'FROM account_move_line ' \
1010 'WHERE account_move_line.move_id = %s ' \
1011 'AND account_analytic_line.move_id = account_move_line.id',
1014 for inv_id, name in self.name_get(cr, uid, [id]):
1015 ctx = context.copy()
1016 if obj_inv.type in ('out_invoice', 'out_refund'):
1017 ctx = self.get_log_context(cr, uid, context=ctx)
1018 message = _("Invoice '%s' is validated.") % name
1019 self.log(cr, uid, inv_id, message, context=ctx)
1022 def action_cancel(self, cr, uid, ids, *args):
1023 context = {} # TODO: Use context from arguments
1024 account_move_obj = self.pool.get('account.move')
1025 invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1026 move_ids = [] # ones that we will need to remove
1029 move_ids.append(i['move_id'][0])
1030 if i['payment_ids']:
1031 account_move_line_obj = self.pool.get('account.move.line')
1032 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1033 for move_line in pay_ids:
1034 if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1035 raise osv.except_osv(_('Error !'), _('You can not cancel an invoice which is partially paid! You need to unreconcile related payment entries first!'))
1037 # First, set the invoices as cancelled and detach the move ids
1038 self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1040 # second, invalidate the move(s)
1041 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1042 # delete the move this invoice was pointing to
1043 # Note that the corresponding move_lines and move_reconciles
1044 # will be automatically deleted too
1045 account_move_obj.unlink(cr, uid, move_ids, context=context)
1046 self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1051 def list_distinct_taxes(self, cr, uid, ids):
1052 invoices = self.browse(cr, uid, ids)
1054 for inv in invoices:
1055 for tax in inv.tax_line:
1056 if not tax['name'] in taxes:
1057 taxes[tax['name']] = {'name': tax['name']}
1058 return taxes.values()
1060 def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1061 #TODO: implement messages system
1064 def name_get(self, cr, uid, ids, context=None):
1068 'out_invoice': 'CI: ',
1069 'in_invoice': 'SI: ',
1070 'out_refund': 'OR: ',
1071 'in_refund': 'SR: ',
1073 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')]
1075 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1082 ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1084 ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1085 return self.name_get(cr, user, ids, context)
1087 def _refund_cleanup_lines(self, cr, uid, lines):
1090 del line['invoice_id']
1091 for field in ('company_id', 'partner_id', 'account_id', 'product_id',
1092 'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
1094 line[field] = line[field][0]
1095 if 'invoice_line_tax_id' in line:
1096 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
1097 return map(lambda x: (0,0,x), lines)
1099 def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
1100 invoices = self.read(cr, uid, ids, ['name', 'type', 'number', 'reference', 'comment', 'date_due', 'partner_id', 'address_contact_id', 'address_invoice_id', 'partner_contact', 'partner_insite', 'partner_ref', 'payment_term', 'account_id', 'currency_id', 'invoice_line', 'tax_line', 'journal_id', 'user_id', 'fiscal_position'])
1101 obj_invoice_line = self.pool.get('account.invoice.line')
1102 obj_invoice_tax = self.pool.get('account.invoice.tax')
1103 obj_journal = self.pool.get('account.journal')
1105 for invoice in invoices:
1109 'out_invoice': 'out_refund', # Customer Invoice
1110 'in_invoice': 'in_refund', # Supplier Invoice
1111 'out_refund': 'out_invoice', # Customer Refund
1112 'in_refund': 'in_invoice', # Supplier Refund
1115 invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
1116 invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
1118 tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
1119 tax_lines = filter(lambda l: l['manual'], tax_lines)
1120 tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
1122 refund_journal_ids = [journal_id]
1123 elif invoice['type'] == 'in_invoice':
1124 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
1126 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
1129 date = time.strftime('%Y-%m-%d')
1131 'type': type_dict[invoice['type']],
1132 'date_invoice': date,
1135 'invoice_line': invoice_lines,
1136 'tax_line': tax_lines,
1137 'journal_id': refund_journal_ids
1141 'period_id': period_id,
1145 'name': description,
1147 # take the id part of the tuple returned for many2one fields
1148 for field in ('address_contact_id', 'address_invoice_id', 'partner_id',
1149 'account_id', 'currency_id', 'payment_term', 'journal_id',
1150 'user_id', 'fiscal_position'):
1151 invoice[field] = invoice[field] and invoice[field][0]
1152 # create the new invoice
1153 new_ids.append(self.create(cr, uid, invoice))
1157 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=''):
1160 #TODO check if we can use different period for payment and the writeoff line
1161 assert len(ids)==1, "Can only pay one invoice at a time"
1162 invoice = self.browse(cr, uid, ids[0], context=context)
1163 src_account_id = invoice.account_id.id
1164 # Take the seq as name for move
1165 types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1166 direction = types[invoice.type]
1167 #take the choosen date
1168 if 'date_p' in context and context['date_p']:
1169 date=context['date_p']
1171 date=time.strftime('%Y-%m-%d')
1173 # Take the amount in currency and the currency of the payment
1174 if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1175 amount_currency = context['amount_currency']
1176 currency_id = context['currency_id']
1178 amount_currency = False
1181 pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1182 if invoice.type in ('in_invoice', 'out_invoice'):
1183 if pay_journal['type'] == 'bank':
1184 entry_type = 'bank_pay_voucher' # Bank payment
1186 entry_type = 'pay_voucher' # Cash payment
1188 entry_type = 'cont_voucher'
1189 if invoice.type in ('in_invoice', 'in_refund'):
1190 ref = invoice.reference
1192 ref = self._convert_ref(cr, uid, invoice.number)
1193 # Pay attention to the sign for both debit/credit AND amount_currency
1195 'debit': direction * pay_amount>0 and direction * pay_amount,
1196 'credit': direction * pay_amount<0 and - direction * pay_amount,
1197 'account_id': src_account_id,
1198 'partner_id': invoice.partner_id.id,
1201 'currency_id':currency_id,
1202 'amount_currency':amount_currency and direction * amount_currency or 0.0,
1203 'company_id': invoice.company_id.id,
1206 'debit': direction * pay_amount<0 and - direction * pay_amount,
1207 'credit': direction * pay_amount>0 and direction * pay_amount,
1208 'account_id': pay_account_id,
1209 'partner_id': invoice.partner_id.id,
1212 'currency_id':currency_id,
1213 'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1214 'company_id': invoice.company_id.id,
1218 name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1222 lines = [(0, 0, l1), (0, 0, l2)]
1223 move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1224 move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1228 line = self.pool.get('account.move.line')
1229 move_ids = [move_id,]
1231 move_ids.append(invoice.move_id.id)
1232 cr.execute('SELECT id FROM account_move_line '\
1233 'WHERE move_id IN %s',
1234 ((move_id, invoice.move_id.id),))
1235 lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1236 for l in lines+invoice.payment_ids:
1237 if l.account_id.id == src_account_id:
1238 line_ids.append(l.id)
1239 total += (l.debit or 0.0) - (l.credit or 0.0)
1241 inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1242 if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1243 self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1245 code = invoice.currency_id.symbol
1246 # TODO: use currency's formatting function
1247 msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining)") % \
1248 (name, pay_amount, code, invoice.amount_total, code, total, code)
1249 self.log(cr, uid, inv_id, msg)
1250 self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1252 # Update the stored value (fields.function), so we write to trigger recompute
1253 self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1258 class account_invoice_line(osv.osv):
1260 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1262 tax_obj = self.pool.get('account.tax')
1263 cur_obj = self.pool.get('res.currency')
1264 for line in self.browse(cr, uid, ids):
1265 price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1266 taxes = tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, price, line.quantity, product=line.product_id, address_id=line.invoice_id.address_invoice_id, partner=line.invoice_id.partner_id)
1267 res[line.id] = taxes['total']
1269 cur = line.invoice_id.currency_id
1270 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1273 def _price_unit_default(self, cr, uid, context=None):
1276 if context.get('check_total', False):
1277 t = context['check_total']
1278 for l in context.get('invoice_line', {}):
1279 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1280 tax_obj = self.pool.get('account.tax')
1281 p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1282 t = t - (p * l[2].get('quantity'))
1283 taxes = l[2].get('invoice_line_tax_id')
1284 if len(taxes[0]) >= 3 and taxes[0][2]:
1285 taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1286 for tax in tax_obj.compute_all(cr, uid, taxes, p,l[2].get('quantity'), context.get('address_invoice_id', False), l[2].get('product_id', False), context.get('partner_id', False))['taxes']:
1287 t = t - tax['amount']
1291 _name = "account.invoice.line"
1292 _description = "Invoice Line"
1294 'name': fields.char('Description', size=256, required=True),
1295 'origin': fields.char('Origin', size=256, help="Reference of the document that produced this invoice."),
1296 'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1297 'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1298 'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1299 '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."),
1300 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
1301 'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
1302 digits_compute= dp.get_precision('Account'), store=True),
1303 'quantity': fields.float('Quantity', required=True),
1304 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
1305 'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1306 'note': fields.text('Notes'),
1307 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1308 'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1309 'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1314 'price_unit': _price_unit_default,
1317 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1320 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)
1321 if context.get('type', False):
1322 doc = etree.XML(res['arch'])
1323 for node in doc.xpath("//field[@name='product_id']"):
1324 if context['type'] in ('in_invoice', 'in_refund'):
1325 node.set('domain', "[('purchase_ok', '=', True)]")
1327 node.set('domain', "[('sale_ok', '=', True)]")
1328 res['arch'] = etree.tostring(doc)
1331 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, address_invoice_id=False, currency_id=False, context=None, company_id=None):
1334 company_id = company_id if company_id != None else context.get('company_id',False)
1335 context = dict(context)
1336 context.update({'company_id': company_id})
1338 raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1340 if type in ('in_invoice', 'in_refund'):
1341 return {'value': {}, 'domain':{'product_uom':[]}}
1343 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1344 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1345 fpos_obj = self.pool.get('account.fiscal.position')
1346 fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1349 context.update({'lang': part.lang})
1351 res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1353 if type in ('out_invoice','out_refund'):
1354 a = res.product_tmpl_id.property_account_income.id
1356 a = res.categ_id.property_account_income_categ.id
1358 a = res.product_tmpl_id.property_account_expense.id
1360 a = res.categ_id.property_account_expense_categ.id
1361 a = fpos_obj.map_account(cr, uid, fpos, a)
1363 result['account_id'] = a
1365 if type in ('out_invoice', 'out_refund'):
1366 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)
1368 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)
1369 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1371 if type in ('in_invoice', 'in_refund'):
1372 result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1374 result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1375 result['name'] = res.partner_ref
1378 result['uos_id'] = res.uom_id.id or uom or False
1379 result['note'] = res.description
1380 if result['uos_id']:
1381 res2 = res.uom_id.category_id.id
1383 domain = {'uos_id':[('category_id','=',res2 )]}
1385 res_final = {'value':result, 'domain':domain}
1387 if not company_id or not currency_id:
1390 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1391 currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1393 if company.currency_id.id != currency.id:
1394 if type in ('in_invoice', 'in_refund'):
1395 res_final['value']['price_unit'] = res.standard_price
1396 new_price = res_final['value']['price_unit'] * currency.rate
1397 res_final['value']['price_unit'] = new_price
1400 uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1401 if res.uom_id.category_id.id == uom.category_id.id:
1402 new_price = res_final['value']['price_unit'] * uom.factor_inv
1403 res_final['value']['price_unit'] = new_price
1406 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, address_invoice_id=False, currency_id=False, context=None, company_id=None):
1409 company_id = company_id if company_id != None else context.get('company_id',False)
1410 context = dict(context)
1411 context.update({'company_id': company_id})
1413 res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, address_invoice_id, currency_id, context=context)
1414 if 'uos_id' in res['value']:
1415 del res['value']['uos_id']
1417 res['value']['price_unit'] = 0.0
1419 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1420 prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1421 if prod.uom_id.category_id.id != prod_uom.category_id.id:
1423 'title': _('Warning!'),
1424 'message': _('You selected an Unit of Measure which is not compatible with the product.')
1426 return {'value': res['value'], 'warning': warning}
1429 def move_line_get(self, cr, uid, invoice_id, context=None):
1431 tax_obj = self.pool.get('account.tax')
1432 cur_obj = self.pool.get('res.currency')
1435 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1436 company_currency = inv.company_id.currency_id.id
1438 for line in inv.invoice_line:
1439 mres = self.move_line_get_item(cr, uid, line, context)
1443 tax_code_found= False
1444 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1445 (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1446 line.quantity, inv.address_invoice_id.id, line.product_id,
1447 inv.partner_id)['taxes']:
1449 if inv.type in ('out_invoice', 'in_invoice'):
1450 tax_code_id = tax['base_code_id']
1451 tax_amount = line.price_subtotal * tax['base_sign']
1453 tax_code_id = tax['ref_base_code_id']
1454 tax_amount = line.price_subtotal * tax['ref_base_sign']
1459 res.append(self.move_line_get_item(cr, uid, line, context))
1460 res[-1]['price'] = 0.0
1461 res[-1]['account_analytic_id'] = False
1462 elif not tax_code_id:
1464 tax_code_found = True
1466 res[-1]['tax_code_id'] = tax_code_id
1467 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1470 def move_line_get_item(self, cr, uid, line, context=None):
1473 'name': line.name[:64],
1474 'price_unit':line.price_unit,
1475 'quantity':line.quantity,
1476 'price':line.price_subtotal,
1477 'account_id':line.account_id.id,
1478 'product_id':line.product_id.id,
1479 'uos_id':line.uos_id.id,
1480 'account_analytic_id':line.account_analytic_id.id,
1481 'taxes':line.invoice_line_tax_id,
1484 # Set the tax field according to the account and the fiscal position
1486 def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1489 taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1490 fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1491 tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1493 product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1494 partner_id=partner_id, fposition_id=fposition_id)
1495 unique_tax_ids = set(tax_ids)
1496 if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1497 unique_tax_ids |= set(product_change_result['value']['invoice_line_tax_id'])
1498 return {'value':{'invoice_line_tax_id': list(unique_tax_ids)}}
1500 account_invoice_line()
1502 class account_invoice_tax(osv.osv):
1503 _name = "account.invoice.tax"
1504 _description = "Invoice Tax"
1506 def _count_factor(self, cr, uid, ids, name, args, context=None):
1508 for invoice_tax in self.browse(cr, uid, ids, context=context):
1509 res[invoice_tax.id] = {
1513 if invoice_tax.amount <> 0.0:
1514 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1515 res[invoice_tax.id]['factor_tax'] = factor_tax
1517 if invoice_tax.base <> 0.0:
1518 factor_base = invoice_tax.base_amount / invoice_tax.base
1519 res[invoice_tax.id]['factor_base'] = factor_base
1524 'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1525 'name': fields.char('Tax Description', size=64, required=True),
1526 'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1527 'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1528 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1529 'manual': fields.boolean('Manual'),
1530 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1531 'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1532 'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1533 'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1534 'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1535 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1536 'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1537 'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1540 def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1541 cur_obj = self.pool.get('res.currency')
1542 company_obj = self.pool.get('res.company')
1543 company_currency = False
1546 factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1548 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1549 if currency_id and company_currency:
1550 base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1551 return {'value': {'base_amount':base}}
1553 def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1554 cur_obj = self.pool.get('res.currency')
1555 company_obj = self.pool.get('res.company')
1556 company_currency = False
1559 factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1561 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1562 if currency_id and company_currency:
1563 amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1564 return {'value': {'tax_amount': amount}}
1572 def compute(self, cr, uid, invoice_id, context=None):
1574 tax_obj = self.pool.get('account.tax')
1575 cur_obj = self.pool.get('res.currency')
1576 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1577 cur = inv.currency_id
1578 company_currency = inv.company_id.currency_id.id
1580 for line in inv.invoice_line:
1581 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, inv.address_invoice_id.id, line.product_id, inv.partner_id)['taxes']:
1582 tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
1584 val['invoice_id'] = inv.id
1585 val['name'] = tax['name']
1586 val['amount'] = tax['amount']
1587 val['manual'] = False
1588 val['sequence'] = tax['sequence']
1589 val['base'] = tax['price_unit'] * line['quantity']
1591 if inv.type in ('out_invoice','in_invoice'):
1592 val['base_code_id'] = tax['base_code_id']
1593 val['tax_code_id'] = tax['tax_code_id']
1594 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)
1595 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)
1596 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1598 val['base_code_id'] = tax['ref_base_code_id']
1599 val['tax_code_id'] = tax['ref_tax_code_id']
1600 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)
1601 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)
1602 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1604 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1605 if not key in tax_grouped:
1606 tax_grouped[key] = val
1608 tax_grouped[key]['amount'] += val['amount']
1609 tax_grouped[key]['base'] += val['base']
1610 tax_grouped[key]['base_amount'] += val['base_amount']
1611 tax_grouped[key]['tax_amount'] += val['tax_amount']
1613 for t in tax_grouped.values():
1614 t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1615 t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1616 t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1617 t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1620 def move_line_get(self, cr, uid, invoice_id):
1622 cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1623 for t in cr.dictfetchall():
1624 if not t['amount'] \
1625 and not t['tax_code_id'] \
1626 and not t['tax_amount']:
1631 'price_unit': t['amount'],
1633 'price': t['amount'] or 0.0,
1634 'account_id': t['account_id'],
1635 'tax_code_id': t['tax_code_id'],
1636 'tax_amount': t['tax_amount']
1640 account_invoice_tax()
1643 class res_partner(osv.osv):
1644 """ Inherits partner and adds invoice information in the partner form """
1645 _inherit = 'res.partner'
1647 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1650 def copy(self, cr, uid, id, default=None, context=None):
1651 default = default or {}
1652 default.update({'invoice_ids' : []})
1653 return super(res_partner, self).copy(cr, uid, id, default, context)
1657 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: