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):
84 res[id] = self.test_paid(cr, uid, [id])
87 def _get_reference_type(self, cr, uid, context=None):
88 return [('none', _('Free Reference'))]
90 def _amount_residual(self, cr, uid, ids, name, args, context=None):
92 for invoice in self.browse(cr, uid, ids, context=context):
93 result[invoice.id] = 0.0
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
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):
104 for invoice in self.browse(cr, uid, ids, context=context):
107 if not invoice.move_id:
109 data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
111 for line in data_lines:
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]
122 def _get_invoice_line(self, cr, uid, ids, context=None):
124 for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
125 result[line.invoice_id.id] = True
128 def _get_invoice_tax(self, cr, uid, ids, context=None):
130 for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
131 result[tax.invoice_id.id] = True
134 def _compute_lines(self, cr, uid, ids, name, args, context=None):
136 for invoice in self.browse(cr, uid, ids, context=context):
140 for m in invoice.move_id.line_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]
149 lines = filter(lambda x: x not in src, lines)
150 result[invoice.id] = lines
153 def _get_invoice_from_line(self, cr, uid, ids, context=None):
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
164 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
167 def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
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
177 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
180 _name = "account.invoice"
181 _description = 'Invoice'
185 'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
186 'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
187 'type': fields.selection([
188 ('out_invoice','Customer Invoice'),
189 ('in_invoice','Supplier Invoice'),
190 ('out_refund','Customer Refund'),
191 ('in_refund','Supplier Refund'),
192 ],'Type', readonly=True, select=True, change_default=True),
194 'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
195 'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
196 'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
197 'reference_type': fields.selection(_get_reference_type, 'Reference Type',
198 required=True, readonly=True, states={'draft':[('readonly',False)]}),
199 'comment': fields.text('Additional Information'),
201 'state': fields.selection([
203 ('proforma','Pro-forma'),
204 ('proforma2','Pro-forma'),
207 ('cancel','Cancelled')
208 ],'State', select=True, readonly=True,
209 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
210 \n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
211 \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. \
212 \n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
213 \n* The \'Cancelled\' state is used when user cancel invoice.'),
214 'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
215 'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
216 help="If you use payment terms, the due date will be computed automatically at the generation "\
217 "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."),
218 'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
219 'address_contact_id': fields.many2one('res.partner.address', 'Contact Address', readonly=True, states={'draft':[('readonly',False)]}),
220 'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', 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)]}),
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)]}),
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',
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),
239 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
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),
246 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
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),
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',
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',
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),
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', 'Salesman', readonly=True, states={'draft':[('readonly',False)]}),
278 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
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',
288 'internal_number': False,
289 'user_id': lambda s, cr, u, c: u,
292 ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
295 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
296 journal_obj = self.pool.get('account.journal')
300 if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
301 partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
303 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
305 if view_type == 'form':
306 if partner['supplier'] and not partner['customer']:
307 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
309 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
310 if view_id and isinstance(view_id, (list, tuple)):
312 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 type = context.get('journal_type', False)
315 for field in res['fields']:
316 if field == 'journal_id' and type:
317 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
318 res['fields'][field]['selection'] = journal_select
320 doc = etree.XML(res['arch'])
322 if context.get('type', False):
323 for node in doc.xpath("//field[@name='partner_bank_id']"):
324 if context['type'] == 'in_refund':
325 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
326 elif context['type'] == 'out_refund':
327 node.set('domain', "[('partner_id', '=', partner_id)]")
328 res['arch'] = etree.tostring(doc)
330 if view_type == 'search':
331 if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
332 for node in doc.xpath("//group[@name='extended filter']"):
334 res['arch'] = etree.tostring(doc)
336 if view_type == 'tree':
337 partner_string = _('Customer')
338 if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
339 partner_string = _('Supplier')
340 for node in doc.xpath("//field[@name='reference']"):
341 node.set('invisible', '0')
342 for node in doc.xpath("//field[@name='partner_id']"):
343 node.set('string', partner_string)
344 res['arch'] = etree.tostring(doc)
347 def get_log_context(self, cr, uid, context=None):
350 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
351 view_id = res and res[1] or False
352 context['view_id'] = view_id
355 def create(self, cr, uid, vals, context=None):
359 res = super(account_invoice, self).create(cr, uid, vals, context)
360 for inv_id, name in self.name_get(cr, uid, [res], context=context):
362 if vals.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
363 ctx = self.get_log_context(cr, uid, context=ctx)
364 message = _("Invoice '%s' is waiting for validation.") % name
365 self.log(cr, uid, inv_id, message, context=ctx)
368 if '"journal_id" viol' in e.args[0]:
369 raise orm.except_orm(_('Configuration Error!'),
370 _('There is no Accounting Journal of type Sale/Purchase defined!'))
372 raise orm.except_orm(_('Unknown Error'), str(e))
374 def confirm_paid(self, cr, uid, ids, context=None):
377 self.write(cr, uid, ids, {'state':'paid'}, context=context)
378 for inv_id, name in self.name_get(cr, uid, ids, context=context):
379 message = _("Invoice '%s' is paid.") % name
380 self.log(cr, uid, inv_id, message)
383 def unlink(self, cr, uid, ids, context=None):
386 invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
389 if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
390 unlink_ids.append(t['id'])
392 raise osv.except_osv(_('Invalid action !'), _('You can not delete an invoice which is open or paid. We suggest you to refund it instead.'))
393 osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
396 def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
397 date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
398 invoice_addr_id = False
399 contact_addr_id = False
400 partner_payment_term = False
403 fiscal_position = False
405 opt = [('uid', str(uid))]
408 opt.insert(0, ('id', partner_id))
409 res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
410 contact_addr_id = res['contact']
411 invoice_addr_id = res['invoice']
412 p = self.pool.get('res.partner').browse(cr, uid, partner_id)
414 if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
415 property_obj = self.pool.get('ir.property')
416 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
417 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
419 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
421 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
422 rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
423 pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
424 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
425 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
426 if not rec_res_id and not pay_res_id:
427 raise osv.except_osv(_('Configuration Error !'),
428 _('Can not find a chart of accounts for this company, you should create one.'))
429 account_obj = self.pool.get('account.account')
430 rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
431 pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
432 p.property_account_receivable = rec_obj_acc[0]
433 p.property_account_payable = pay_obj_acc[0]
435 if type in ('out_invoice', 'out_refund'):
436 acc_id = p.property_account_receivable.id
438 acc_id = p.property_account_payable.id
439 fiscal_position = p.property_account_position and p.property_account_position.id or False
440 partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
442 bank_id = p.bank_ids[0].id
445 'address_contact_id': contact_addr_id,
446 'address_invoice_id': invoice_addr_id,
447 'account_id': acc_id,
448 'payment_term': partner_payment_term,
449 'fiscal_position': fiscal_position
453 if type in ('in_invoice', 'in_refund'):
454 result['value']['partner_bank_id'] = bank_id
456 if payment_term != partner_payment_term:
457 if partner_payment_term:
458 to_update = self.onchange_payment_term_date_invoice(
459 cr, uid, ids, partner_payment_term, date_invoice)
460 result['value'].update(to_update['value'])
462 result['value']['date_due'] = False
464 if partner_bank_id != bank_id:
465 to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
466 result['value'].update(to_update['value'])
469 def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
472 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
473 currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
475 'currency_id': currency_id,
480 def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
481 if not payment_term_id:
484 pt_obj = self.pool.get('account.payment.term')
486 date_invoice = time.strftime('%Y-%m-%d')
488 pterm_list = pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
491 pterm_list = [line[0] for line in pterm_list]
493 res = {'value':{'date_due': pterm_list[-1]}}
495 raise osv.except_osv(_('Data Insufficient !'), _('The payment term of supplier does not have a payment term line!'))
498 def onchange_invoice_line(self, cr, uid, ids, lines):
501 def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
504 def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
507 obj_journal = self.pool.get('account.journal')
508 account_obj = self.pool.get('account.account')
509 inv_line_obj = self.pool.get('account.invoice.line')
510 if company_id and part_id and type:
512 partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
513 if partner_obj.property_account_payable and partner_obj.property_account_receivable:
514 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
515 property_obj = self.pool.get('ir.property')
516 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
517 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
519 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
521 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
522 rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
523 pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
524 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
525 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
526 if not rec_res_id and not pay_res_id:
527 raise osv.except_osv(_('Configuration Error !'),
528 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
529 if type in ('out_invoice', 'out_refund'):
533 val= {'account_id': acc_id}
536 inv_obj = self.browse(cr,uid,ids)
537 for line in inv_obj[0].invoice_line:
539 if line.account_id.company_id.id != company_id:
540 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
542 raise osv.except_osv(_('Configuration Error !'),
543 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
544 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
547 for inv_line in invoice_line:
548 obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
549 if obj_l.company_id.id != company_id:
550 raise osv.except_osv(_('Configuration Error !'),
551 _('Invoice line account company does not match with invoice company.'))
554 if company_id and type:
555 if type in ('out_invoice'):
556 journal_type = 'sale'
557 elif type in ('out_refund'):
558 journal_type = 'sale_refund'
559 elif type in ('in_refund'):
560 journal_type = 'purchase_refund'
562 journal_type = 'purchase'
563 journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
565 val['journal_id'] = journal_ids[0]
566 ir_values_obj = self.pool.get('ir.values')
567 res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
568 for r in res_journal_default:
569 if r[1] == 'journal_id' and r[2] in journal_ids:
570 val['journal_id'] = r[2]
571 if not val.get('journal_id', False):
572 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)))
573 dom = {'journal_id': [('id', 'in', journal_ids)]}
575 journal_ids = obj_journal.search(cr, uid, [])
577 return {'value': val, 'domain': dom}
579 # go from canceled state to draft state
580 def action_cancel_draft(self, cr, uid, ids, *args):
581 self.write(cr, uid, ids, {'state':'draft'})
582 wf_service = netsvc.LocalService("workflow")
584 wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
585 wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
591 # return the ids of the move lines which has the same account than the invoice
593 def move_line_id_payment_get(self, cr, uid, ids, *args):
594 if not ids: return []
595 result = self.move_line_id_payment_gets(cr, uid, ids, *args)
596 return result.get(ids[0], [])
598 def move_line_id_payment_gets(self, cr, uid, ids, *args):
600 if not ids: return res
601 cr.execute('SELECT i.id, l.id '\
602 'FROM account_move_line l '\
603 'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
605 'AND l.account_id=i.account_id',
607 for r in cr.fetchall():
608 res.setdefault(r[0], [])
609 res[r[0]].append( r[1] )
612 def copy(self, cr, uid, id, default=None, context=None):
613 default = default or {}
619 'internal_number': False,
622 if 'date_invoice' not in default:
626 if 'date_due' not in default:
630 return super(account_invoice, self).copy(cr, uid, id, default, context)
632 def test_paid(self, cr, uid, ids, *args):
633 res = self.move_line_id_payment_get(cr, uid, ids)
638 cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
639 ok = ok and bool(cr.fetchone()[0])
642 def button_reset_taxes(self, cr, uid, ids, context=None):
646 ait_obj = self.pool.get('account.invoice.tax')
648 cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
649 partner = self.browse(cr, uid, id, context=ctx).partner_id
651 ctx.update({'lang': partner.lang})
652 for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
653 ait_obj.create(cr, uid, taxe)
654 # Update the stored value (fields.function), so we write to trigger recompute
655 self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
658 def button_compute(self, cr, uid, ids, context=None, set_total=False):
659 self.button_reset_taxes(cr, uid, ids, context)
660 for inv in self.browse(cr, uid, ids, context=context):
662 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
665 def _convert_ref(self, cr, uid, ref):
666 return (ref or '').replace('/','')
668 def _get_analytic_lines(self, cr, uid, id, context=None):
671 inv = self.browse(cr, uid, id)
672 cur_obj = self.pool.get('res.currency')
674 company_currency = inv.company_id.currency_id.id
675 if inv.type in ('out_invoice', 'in_refund'):
680 iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
682 if il['account_analytic_id']:
683 if inv.type in ('in_invoice', 'in_refund'):
686 ref = self._convert_ref(cr, uid, inv.number)
687 if not inv.journal_id.analytic_journal_id:
688 raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
689 il['analytic_lines'] = [(0,0, {
691 'date': inv['date_invoice'],
692 'account_id': il['account_analytic_id'],
693 'unit_amount': il['quantity'],
694 'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
695 'product_id': il['product_id'],
696 'product_uom_id': il['uos_id'],
697 'general_account_id': il['account_id'],
698 'journal_id': inv.journal_id.analytic_journal_id.id,
703 def action_date_assign(self, cr, uid, ids, *args):
704 for inv in self.browse(cr, uid, ids):
705 res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
706 if res and res['value']:
707 self.write(cr, uid, [inv.id], res['value'])
710 def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
711 """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
712 Hook method to be overridden in additional modules to verify and possibly alter the
713 move lines to be created by an invoice, for special cases.
714 :param invoice_browse: browsable record of the invoice that is generating the move lines
715 :param move_lines: list of dictionaries with the account.move.lines (as for create())
716 :return: the (possibly updated) final move_lines to create for this invoice
720 def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
722 for tax in compute_taxes.values():
723 ait_obj.create(cr, uid, tax)
726 for tax in inv.tax_line:
729 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
731 if not key in compute_taxes:
732 raise osv.except_osv(_('Warning !'), _('Global taxes defined, but they are not in invoice lines !'))
733 base = compute_taxes[key]['base']
734 if abs(base - tax.base) > inv.company_id.currency_id.rounding:
735 raise osv.except_osv(_('Warning !'), _('Tax base different!\nClick on compute to update the tax base.'))
736 for key in compute_taxes:
737 if not key in tax_key:
738 raise osv.except_osv(_('Warning !'), _('Taxes are missing!\nClick on compute button.'))
740 def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
743 cur_obj = self.pool.get('res.currency')
744 for i in invoice_move_lines:
745 if inv.currency_id.id != company_currency:
746 i['currency_id'] = inv.currency_id.id
747 i['amount_currency'] = i['price']
748 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
749 company_currency, i['price'],
750 context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
752 i['amount_currency'] = False
753 i['currency_id'] = False
755 if inv.type in ('out_invoice','in_refund'):
757 total_currency += i['amount_currency'] or i['price']
758 i['price'] = - i['price']
761 total_currency -= i['amount_currency'] or i['price']
762 return total, total_currency, invoice_move_lines
764 def inv_line_characteristic_hashcode(self, invoice, invoice_line):
765 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
766 will be grouped together if the journal has the 'group line' option. Of course a module
767 can add fields to invoice lines that would need to be tested too before merging lines
769 return "%s-%s-%s-%s-%s"%(
770 invoice_line['account_id'],
771 invoice_line.get('tax_code_id',"False"),
772 invoice_line.get('product_id',"False"),
773 invoice_line.get('analytic_account_id',"False"),
774 invoice_line.get('date_maturity',"False"))
776 def group_lines(self, cr, uid, iml, line, inv):
777 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
778 if inv.journal_id.group_invoice_lines:
781 tmp = self.inv_line_characteristic_hashcode(inv, l)
784 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
785 line2[tmp]['debit'] = (am > 0) and am or 0.0
786 line2[tmp]['credit'] = (am < 0) and -am or 0.0
787 line2[tmp]['tax_amount'] += l['tax_amount']
788 line2[tmp]['analytic_lines'] += l['analytic_lines']
792 for key, val in line2.items():
793 line.append((0,0,val))
796 def action_move_create(self, cr, uid, ids, context=None):
797 """Creates invoice related analytics and financial move lines"""
798 ait_obj = self.pool.get('account.invoice.tax')
799 cur_obj = self.pool.get('res.currency')
800 period_obj = self.pool.get('account.period')
801 payment_term_obj = self.pool.get('account.payment.term')
802 journal_obj = self.pool.get('account.journal')
803 move_obj = self.pool.get('account.move')
806 for inv in self.browse(cr, uid, ids, context=context):
807 if not inv.journal_id.sequence_id:
808 raise osv.except_osv(_('Error !'), _('Please define sequence on the journal related to this invoice.'))
809 if not inv.invoice_line:
810 raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
815 ctx.update({'lang': inv.partner_id.lang})
816 if not inv.date_invoice:
817 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
818 company_currency = inv.company_id.currency_id.id
819 # create the analytical lines
820 # one move line per invoice line
821 iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
822 # check if taxes are all computed
823 compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
824 self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
826 # I disabled the check_total feature
827 #if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
828 # raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
831 total_fixed = total_percent = 0
832 for line in inv.payment_term.line_ids:
833 if line.value == 'fixed':
834 total_fixed += line.value_amount
835 if line.value == 'procent':
836 total_percent += line.value_amount
837 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
838 if (total_fixed + total_percent) > 100:
839 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."))
841 # one move line per tax line
842 iml += ait_obj.move_line_get(cr, uid, inv.id)
845 if inv.type in ('in_invoice', 'in_refund'):
847 entry_type = 'journal_pur_voucher'
848 if inv.type == 'in_refund':
849 entry_type = 'cont_voucher'
851 ref = self._convert_ref(cr, uid, inv.number)
852 entry_type = 'journal_sale_vou'
853 if inv.type == 'out_refund':
854 entry_type = 'cont_voucher'
856 diff_currency_p = inv.currency_id.id <> company_currency
857 # create one move line for the total and possibly adjust the other lines amount
860 total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
861 acc_id = inv.account_id.id
863 name = inv['name'] or '/'
866 totlines = payment_term_obj.compute(cr,
867 uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
869 res_amount_currency = total_currency
871 ctx.update({'date': inv.date_invoice})
873 if inv.currency_id.id != company_currency:
874 amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
876 amount_currency = False
878 # last line add the diff
879 res_amount_currency -= amount_currency or 0
881 if i == len(totlines):
882 amount_currency += res_amount_currency
888 'account_id': acc_id,
889 'date_maturity': t[0],
890 'amount_currency': diff_currency_p \
891 and amount_currency or False,
892 'currency_id': diff_currency_p \
893 and inv.currency_id.id or False,
901 'account_id': acc_id,
902 'date_maturity': inv.date_due or False,
903 'amount_currency': diff_currency_p \
904 and total_currency or False,
905 'currency_id': diff_currency_p \
906 and inv.currency_id.id or False,
910 date = inv.date_invoice or time.strftime('%Y-%m-%d')
911 part = inv.partner_id.id
913 line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
915 line = self.group_lines(cr, uid, iml, line, inv)
917 journal_id = inv.journal_id.id
918 journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
919 if journal.centralisation:
920 raise osv.except_osv(_('UserError'),
921 _('You cannot create an invoice on a centralised journal. Uncheck the centralised counterpart box in the related journal from the configuration menu.'))
923 line = self.finalize_invoice_move_lines(cr, uid, inv, line)
926 'ref': inv.reference and inv.reference or inv.name,
928 'journal_id': journal_id,
930 'narration':inv.comment
932 period_id = inv.period_id and inv.period_id.id or False
933 ctx.update({'company_id': inv.company_id.id})
935 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
936 period_id = period_ids and period_ids[0] or False
938 move['period_id'] = period_id
940 i[2]['period_id'] = period_id
942 move_id = move_obj.create(cr, uid, move, context=ctx)
943 new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
944 # make the invoice point to that move
945 self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
946 # Pass invoice in context in method post: used if you want to get the same
947 # account move reference when creating the same invoice after a cancelled one:
948 ctx.update({'invoice':inv})
949 move_obj.post(cr, uid, [move_id], context=ctx)
950 self._log_event(cr, uid, ids)
953 def line_get_convert(self, cr, uid, x, part, date, context=None):
955 'date_maturity': x.get('date_maturity', False),
957 'name': x['name'][:64],
959 'debit': x['price']>0 and x['price'],
960 'credit': x['price']<0 and -x['price'],
961 'account_id': x['account_id'],
962 'analytic_lines': x.get('analytic_lines', []),
963 'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
964 'currency_id': x.get('currency_id', False),
965 'tax_code_id': x.get('tax_code_id', False),
966 'tax_amount': x.get('tax_amount', False),
967 'ref': x.get('ref', False),
968 'quantity': x.get('quantity',1.00),
969 'product_id': x.get('product_id', False),
970 'product_uom_id': x.get('uos_id', False),
971 'analytic_account_id': x.get('account_analytic_id', False),
974 def action_number(self, cr, uid, ids, context=None):
977 #TODO: not correct fix but required a frech values before reading it.
978 self.write(cr, uid, ids, {})
980 for obj_inv in self.browse(cr, uid, ids, context=context):
982 invtype = obj_inv.type
983 number = obj_inv.number
984 move_id = obj_inv.move_id and obj_inv.move_id.id or False
985 reference = obj_inv.reference or ''
987 self.write(cr, uid, ids, {'internal_number':number})
989 if invtype in ('in_invoice', 'in_refund'):
991 ref = self._convert_ref(cr, uid, number)
995 ref = self._convert_ref(cr, uid, number)
997 cr.execute('UPDATE account_move SET ref=%s ' \
998 'WHERE id=%s AND (ref is null OR ref = \'\')',
1000 cr.execute('UPDATE account_move_line SET ref=%s ' \
1001 'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1003 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1004 'FROM account_move_line ' \
1005 'WHERE account_move_line.move_id = %s ' \
1006 'AND account_analytic_line.move_id = account_move_line.id',
1009 for inv_id, name in self.name_get(cr, uid, [id]):
1010 ctx = context.copy()
1011 if obj_inv.type in ('out_invoice', 'out_refund'):
1012 ctx = self.get_log_context(cr, uid, context=ctx)
1013 message = _("Invoice '%s' is validated.") % name
1014 self.log(cr, uid, inv_id, message, context=ctx)
1017 def action_cancel(self, cr, uid, ids, *args):
1018 context = {} # TODO: Use context from arguments
1019 account_move_obj = self.pool.get('account.move')
1020 invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1021 move_ids = [] # ones that we will need to remove
1024 move_ids.append(i['move_id'][0])
1025 if i['payment_ids']:
1026 account_move_line_obj = self.pool.get('account.move.line')
1027 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1028 for move_line in pay_ids:
1029 if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1030 raise osv.except_osv(_('Error !'), _('You can not cancel an invoice which is partially paid! You need to unreconcile related payment entries first!'))
1032 # First, set the invoices as cancelled and detach the move ids
1033 self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1035 # second, invalidate the move(s)
1036 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1037 # delete the move this invoice was pointing to
1038 # Note that the corresponding move_lines and move_reconciles
1039 # will be automatically deleted too
1040 account_move_obj.unlink(cr, uid, move_ids, context=context)
1041 self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1046 def list_distinct_taxes(self, cr, uid, ids):
1047 invoices = self.browse(cr, uid, ids)
1049 for inv in invoices:
1050 for tax in inv.tax_line:
1051 if not tax['name'] in taxes:
1052 taxes[tax['name']] = {'name': tax['name']}
1053 return taxes.values()
1055 def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1056 #TODO: implement messages system
1059 def name_get(self, cr, uid, ids, context=None):
1063 'out_invoice': 'CI: ',
1064 'in_invoice': 'SI: ',
1065 'out_refund': 'OR: ',
1066 'in_refund': 'SR: ',
1068 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')]
1070 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1077 ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1079 ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1080 return self.name_get(cr, user, ids, context)
1082 def _refund_cleanup_lines(self, cr, uid, lines):
1085 del line['invoice_id']
1086 for field in ('company_id', 'partner_id', 'account_id', 'product_id',
1087 'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
1089 line[field] = line[field][0]
1090 if 'invoice_line_tax_id' in line:
1091 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
1092 return map(lambda x: (0,0,x), lines)
1094 def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
1095 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'])
1096 obj_invoice_line = self.pool.get('account.invoice.line')
1097 obj_invoice_tax = self.pool.get('account.invoice.tax')
1098 obj_journal = self.pool.get('account.journal')
1100 for invoice in invoices:
1104 'out_invoice': 'out_refund', # Customer Invoice
1105 'in_invoice': 'in_refund', # Supplier Invoice
1106 'out_refund': 'out_invoice', # Customer Refund
1107 'in_refund': 'in_invoice', # Supplier Refund
1110 invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
1111 invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
1113 tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
1114 tax_lines = filter(lambda l: l['manual'], tax_lines)
1115 tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
1117 refund_journal_ids = [journal_id]
1118 elif invoice['type'] == 'in_invoice':
1119 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
1121 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
1124 date = time.strftime('%Y-%m-%d')
1126 'type': type_dict[invoice['type']],
1127 'date_invoice': date,
1130 'invoice_line': invoice_lines,
1131 'tax_line': tax_lines,
1132 'journal_id': refund_journal_ids
1136 'period_id': period_id,
1140 'name': description,
1142 # take the id part of the tuple returned for many2one fields
1143 for field in ('address_contact_id', 'address_invoice_id', 'partner_id',
1144 'account_id', 'currency_id', 'payment_term', 'journal_id'):
1145 invoice[field] = invoice[field] and invoice[field][0]
1146 # create the new invoice
1147 new_ids.append(self.create(cr, uid, invoice))
1151 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=''):
1154 #TODO check if we can use different period for payment and the writeoff line
1155 assert len(ids)==1, "Can only pay one invoice at a time"
1156 invoice = self.browse(cr, uid, ids[0], context=context)
1157 src_account_id = invoice.account_id.id
1158 # Take the seq as name for move
1159 types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1160 direction = types[invoice.type]
1161 #take the choosen date
1162 if 'date_p' in context and context['date_p']:
1163 date=context['date_p']
1165 date=time.strftime('%Y-%m-%d')
1167 # Take the amount in currency and the currency of the payment
1168 if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1169 amount_currency = context['amount_currency']
1170 currency_id = context['currency_id']
1172 amount_currency = False
1175 pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1176 if invoice.type in ('in_invoice', 'out_invoice'):
1177 if pay_journal['type'] == 'bank':
1178 entry_type = 'bank_pay_voucher' # Bank payment
1180 entry_type = 'pay_voucher' # Cash payment
1182 entry_type = 'cont_voucher'
1183 if invoice.type in ('in_invoice', 'in_refund'):
1184 ref = invoice.reference
1186 ref = self._convert_ref(cr, uid, invoice.number)
1187 # Pay attention to the sign for both debit/credit AND amount_currency
1189 'debit': direction * pay_amount>0 and direction * pay_amount,
1190 'credit': direction * pay_amount<0 and - direction * pay_amount,
1191 'account_id': src_account_id,
1192 'partner_id': invoice.partner_id.id,
1195 'currency_id':currency_id,
1196 'amount_currency':amount_currency and direction * amount_currency or 0.0,
1197 'company_id': invoice.company_id.id,
1200 'debit': direction * pay_amount<0 and - direction * pay_amount,
1201 'credit': direction * pay_amount>0 and direction * pay_amount,
1202 'account_id': pay_account_id,
1203 'partner_id': invoice.partner_id.id,
1206 'currency_id':currency_id,
1207 'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1208 'company_id': invoice.company_id.id,
1212 name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1216 lines = [(0, 0, l1), (0, 0, l2)]
1217 move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1218 move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1222 line = self.pool.get('account.move.line')
1223 move_ids = [move_id,]
1225 move_ids.append(invoice.move_id.id)
1226 cr.execute('SELECT id FROM account_move_line '\
1227 'WHERE move_id IN %s',
1228 ((move_id, invoice.move_id.id),))
1229 lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1230 for l in lines+invoice.payment_ids:
1231 if l.account_id.id == src_account_id:
1232 line_ids.append(l.id)
1233 total += (l.debit or 0.0) - (l.credit or 0.0)
1235 inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1236 if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1237 self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1239 code = invoice.currency_id.symbol
1240 # TODO: use currency's formatting function
1241 msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining)") % \
1242 (name, pay_amount, code, invoice.amount_total, code, total, code)
1243 self.log(cr, uid, inv_id, msg)
1244 self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1246 # Update the stored value (fields.function), so we write to trigger recompute
1247 self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1252 class account_invoice_line(osv.osv):
1254 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1256 tax_obj = self.pool.get('account.tax')
1257 cur_obj = self.pool.get('res.currency')
1258 for line in self.browse(cr, uid, ids):
1259 price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1260 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)
1261 res[line.id] = taxes['total']
1263 cur = line.invoice_id.currency_id
1264 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1267 def _price_unit_default(self, cr, uid, context=None):
1270 if context.get('check_total', False):
1271 t = context['check_total']
1272 for l in context.get('invoice_line', {}):
1273 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1274 tax_obj = self.pool.get('account.tax')
1275 p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1276 t = t - (p * l[2].get('quantity'))
1277 taxes = l[2].get('invoice_line_tax_id')
1278 if len(taxes[0]) >= 3 and taxes[0][2]:
1279 taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1280 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']:
1281 t = t - tax['amount']
1285 _name = "account.invoice.line"
1286 _description = "Invoice Line"
1288 'name': fields.char('Description', size=256, required=True),
1289 'origin': fields.char('Origin', size=256, help="Reference of the document that produced this invoice."),
1290 'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1291 'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1292 'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1293 '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."),
1294 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
1295 'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
1296 digits_compute= dp.get_precision('Account'), store=True),
1297 'quantity': fields.float('Quantity', required=True),
1298 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
1299 'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1300 'note': fields.text('Notes'),
1301 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1302 'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1303 'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1308 'price_unit': _price_unit_default,
1311 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1314 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)
1315 if context.get('type', False):
1316 doc = etree.XML(res['arch'])
1317 for node in doc.xpath("//field[@name='product_id']"):
1318 if context['type'] in ('in_invoice', 'in_refund'):
1319 node.set('domain', "[('purchase_ok', '=', True)]")
1321 node.set('domain', "[('sale_ok', '=', True)]")
1322 res['arch'] = etree.tostring(doc)
1325 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):
1328 company_id = company_id if company_id != None else context.get('company_id',False)
1329 context = dict(context)
1330 context.update({'company_id': company_id})
1332 raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1334 if type in ('in_invoice', 'in_refund'):
1335 return {'value': {}, 'domain':{'product_uom':[]}}
1337 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1338 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1339 fpos_obj = self.pool.get('account.fiscal.position')
1340 fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1343 context.update({'lang': part.lang})
1345 res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1347 if type in ('out_invoice','out_refund'):
1348 a = res.product_tmpl_id.property_account_income.id
1350 a = res.categ_id.property_account_income_categ.id
1352 a = res.product_tmpl_id.property_account_expense.id
1354 a = res.categ_id.property_account_expense_categ.id
1355 a = fpos_obj.map_account(cr, uid, fpos, a)
1357 result['account_id'] = a
1359 if type in ('out_invoice', 'out_refund'):
1360 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)
1362 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)
1363 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1365 if type in ('in_invoice','in_refund') and tax_id and price_unit:
1366 tax_pool = self.pool.get('account.tax')
1367 tax_browse = tax_pool.browse(cr, uid, tax_id)
1368 if not isinstance(tax_browse, list):
1369 tax_browse = [tax_browse]
1370 taxes = tax_pool.compute_inv(cr, uid, tax_browse, price_unit, 1)
1371 tax_amount = reduce(lambda total, tax_dict: total + tax_dict.get('amount', 0.0), taxes, 0.0)
1372 price_unit = price_unit - tax_amount
1374 price_unit = price_unit / float(qty)
1376 if type in ('in_invoice', 'in_refund'):
1377 result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1379 result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1380 result['name'] = res.partner_ref
1383 result['uos_id'] = res.uom_id.id or uom or False
1384 result['note'] = res.description
1385 if result['uos_id']:
1386 res2 = res.uom_id.category_id.id
1388 domain = {'uos_id':[('category_id','=',res2 )]}
1390 res_final = {'value':result, 'domain':domain}
1392 if not company_id or not currency_id:
1395 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1396 currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1398 if company.currency_id.id != currency.id:
1399 if type in ('in_invoice', 'in_refund'):
1400 res_final['value']['price_unit'] = res.standard_price
1401 new_price = res_final['value']['price_unit'] * currency.rate
1402 res_final['value']['price_unit'] = new_price
1405 uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1406 if res.uom_id.category_id.id == uom.category_id.id:
1407 new_price = res_final['value']['price_unit'] * uom.factor_inv
1408 res_final['value']['price_unit'] = new_price
1411 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):
1414 company_id = company_id if company_id != None else context.get('company_id',False)
1415 context = dict(context)
1416 context.update({'company_id': company_id})
1418 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)
1419 if 'uos_id' in res['value']:
1420 del res['value']['uos_id']
1422 res['value']['price_unit'] = 0.0
1424 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1425 prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1426 if prod.uom_id.category_id.id != prod_uom.category_id.id:
1428 'title': _('Warning!'),
1429 'message': _('You selected an Unit of Measure which is not compatible with the product.')
1431 return {'value': res['value'], 'warning': warning}
1434 def move_line_get(self, cr, uid, invoice_id, context=None):
1436 tax_obj = self.pool.get('account.tax')
1437 cur_obj = self.pool.get('res.currency')
1440 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1441 company_currency = inv.company_id.currency_id.id
1443 for line in inv.invoice_line:
1444 mres = self.move_line_get_item(cr, uid, line, context)
1448 tax_code_found= False
1449 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1450 (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1451 line.quantity, inv.address_invoice_id.id, line.product_id,
1452 inv.partner_id)['taxes']:
1454 if inv.type in ('out_invoice', 'in_invoice'):
1455 tax_code_id = tax['base_code_id']
1456 tax_amount = line.price_subtotal * tax['base_sign']
1458 tax_code_id = tax['ref_base_code_id']
1459 tax_amount = line.price_subtotal * tax['ref_base_sign']
1464 res.append(self.move_line_get_item(cr, uid, line, context))
1465 res[-1]['price'] = 0.0
1466 res[-1]['account_analytic_id'] = False
1467 elif not tax_code_id:
1469 tax_code_found = True
1471 res[-1]['tax_code_id'] = tax_code_id
1472 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1475 def move_line_get_item(self, cr, uid, line, context=None):
1478 'name': line.name[:64],
1479 'price_unit':line.price_unit,
1480 'quantity':line.quantity,
1481 'price':line.price_subtotal,
1482 'account_id':line.account_id.id,
1483 'product_id':line.product_id.id,
1484 'uos_id':line.uos_id.id,
1485 'account_analytic_id':line.account_analytic_id.id,
1486 'taxes':line.invoice_line_tax_id,
1489 # Set the tax field according to the account and the fiscal position
1491 def onchange_account_id(self, cr, uid, ids, fposition_id, account_id):
1494 taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1495 fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1496 res = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1497 return {'value':{'invoice_line_tax_id': res}}
1499 account_invoice_line()
1501 class account_invoice_tax(osv.osv):
1502 _name = "account.invoice.tax"
1503 _description = "Invoice Tax"
1505 def _count_factor(self, cr, uid, ids, name, args, context=None):
1507 for invoice_tax in self.browse(cr, uid, ids, context=context):
1508 res[invoice_tax.id] = {
1512 if invoice_tax.amount <> 0.0:
1513 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1514 res[invoice_tax.id]['factor_tax'] = factor_tax
1516 if invoice_tax.base <> 0.0:
1517 factor_base = invoice_tax.base_amount / invoice_tax.base
1518 res[invoice_tax.id]['factor_base'] = factor_base
1523 'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1524 'name': fields.char('Tax Description', size=64, required=True),
1525 'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1526 'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1527 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1528 'manual': fields.boolean('Manual'),
1529 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1530 'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1531 'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1532 'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1533 'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1534 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1535 'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1536 'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1539 def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1540 cur_obj = self.pool.get('res.currency')
1541 company_obj = self.pool.get('res.company')
1542 company_currency = False
1545 factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1547 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1548 if currency_id and company_currency:
1549 base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1550 return {'value': {'base_amount':base}}
1552 def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1553 cur_obj = self.pool.get('res.currency')
1554 company_obj = self.pool.get('res.company')
1555 company_currency = False
1558 factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1560 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1561 if currency_id and company_currency:
1562 amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1563 return {'value': {'tax_amount': amount}}
1571 def compute(self, cr, uid, invoice_id, context=None):
1573 tax_obj = self.pool.get('account.tax')
1574 cur_obj = self.pool.get('res.currency')
1575 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1576 cur = inv.currency_id
1577 company_currency = inv.company_id.currency_id.id
1579 for line in inv.invoice_line:
1580 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']:
1581 tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
1583 val['invoice_id'] = inv.id
1584 val['name'] = tax['name']
1585 val['amount'] = tax['amount']
1586 val['manual'] = False
1587 val['sequence'] = tax['sequence']
1588 val['base'] = tax['price_unit'] * line['quantity']
1590 if inv.type in ('out_invoice','in_invoice'):
1591 val['base_code_id'] = tax['base_code_id']
1592 val['tax_code_id'] = tax['tax_code_id']
1593 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)
1594 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)
1595 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1597 val['base_code_id'] = tax['ref_base_code_id']
1598 val['tax_code_id'] = tax['ref_tax_code_id']
1599 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)
1600 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)
1601 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1603 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1604 if not key in tax_grouped:
1605 tax_grouped[key] = val
1607 tax_grouped[key]['amount'] += val['amount']
1608 tax_grouped[key]['base'] += val['base']
1609 tax_grouped[key]['base_amount'] += val['base_amount']
1610 tax_grouped[key]['tax_amount'] += val['tax_amount']
1612 for t in tax_grouped.values():
1613 t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1614 t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1615 t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1616 t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1619 def move_line_get(self, cr, uid, invoice_id):
1621 cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1622 for t in cr.dictfetchall():
1623 if not t['amount'] \
1624 and not t['tax_code_id'] \
1625 and not t['tax_amount']:
1630 'price_unit': t['amount'],
1632 'price': t['amount'] or 0.0,
1633 'account_id': t['account_id'],
1634 'tax_code_id': t['tax_code_id'],
1635 'tax_amount': t['tax_amount']
1639 account_invoice_tax()
1642 class res_partner(osv.osv):
1643 """ Inherits partner and adds invoice information in the partner form """
1644 _inherit = 'res.partner'
1646 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1649 def copy(self, cr, uid, id, default=None, context=None):
1650 default = default or {}
1651 default.update({'invoice_ids' : []})
1652 return super(res_partner, self).copy(cr, uid, id, default, context)
1656 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: