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 _inherit = ['mail.thread']
182 _description = 'Invoice'
186 'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
187 'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
188 'type': fields.selection([
189 ('out_invoice','Customer Invoice'),
190 ('in_invoice','Supplier Invoice'),
191 ('out_refund','Customer Refund'),
192 ('in_refund','Supplier Refund'),
193 ],'Type', readonly=True, select=True, change_default=True),
195 'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
196 'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
197 'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
198 'reference_type': fields.selection(_get_reference_type, 'Reference Type',
199 required=True, readonly=True, states={'draft':[('readonly',False)]}),
200 'comment': fields.text('Additional Information'),
202 'state': fields.selection([
204 ('proforma','Pro-forma'),
205 ('proforma2','Pro-forma'),
208 ('cancel','Cancelled'),
209 ],'State', select=True, readonly=True,
210 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
211 \n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
212 \n* The \'Open\' state is used when user create invoice,a invoice number is generated.Its in open state till user does not pay invoice. \
213 \n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
214 \n* The \'Cancelled\' state is used when user cancel invoice.'),
215 'sent': fields.boolean('Sent', readonly=True, help="It indicates that the invoice has been sent."),
216 'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
217 'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
218 help="If you use payment terms, the due date will be computed automatically at the generation "\
219 "of accounting entries. If you 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."),
220 'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
221 'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
222 help="If you use payment terms, the due date will be computed automatically at the generation "\
223 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
224 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
225 'period_id': fields.many2one('account.period', 'Force Period', domain=[('state','<>','done')], help="Keep empty to use the period of the validation(invoice) date.", readonly=True, states={'draft':[('readonly',False)]}),
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', 'Salesperson', 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,
293 ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
296 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
297 journal_obj = self.pool.get('account.journal')
301 if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
302 partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
304 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
306 if view_type == 'form':
307 if partner['supplier'] and not partner['customer']:
308 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
310 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
311 if view_id and isinstance(view_id, (list, tuple)):
313 res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
315 type = context.get('journal_type', False)
316 for field in res['fields']:
317 if field == 'journal_id' and type:
318 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
319 res['fields'][field]['selection'] = journal_select
321 doc = etree.XML(res['arch'])
323 if context.get('type', False):
324 for node in doc.xpath("//field[@name='partner_bank_id']"):
325 if context['type'] == 'in_refund':
326 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
327 elif context['type'] == 'out_refund':
328 node.set('domain', "[('partner_id', '=', partner_id)]")
329 res['arch'] = etree.tostring(doc)
331 if view_type == 'search':
332 if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
333 for node in doc.xpath("//group[@name='extended filter']"):
335 res['arch'] = etree.tostring(doc)
337 if view_type == 'tree':
338 partner_string = _('Customer')
339 if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
340 partner_string = _('Supplier')
341 for node in doc.xpath("//field[@name='reference']"):
342 node.set('invisible', '0')
343 for node in doc.xpath("//field[@name='partner_id']"):
344 node.set('string', partner_string)
345 res['arch'] = etree.tostring(doc)
348 def get_log_context(self, cr, uid, context=None):
351 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
352 view_id = res and res[1] or False
353 context['view_id'] = view_id
356 def create(self, cr, uid, vals, context=None):
360 res = super(account_invoice, self).create(cr, uid, vals, context)
362 self.create_send_note(cr, uid, [res], context=context)
365 if '"journal_id" viol' in e.args[0]:
366 raise orm.except_orm(_('Configuration Error!'),
367 _('There is no Accounting Journal of type Sale/Purchase defined!'))
369 raise orm.except_orm(_('Unknown Error'), str(e))
371 def invoice_print(self, cr, uid, ids, context=None):
373 This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow
375 assert len(ids) == 1, 'This option should only be used for a single id at a time'
376 self.write(cr, uid, ids, {'sent': True}, context=context)
379 'model': 'account.invoice',
380 'form': self.read(cr, uid, ids[0], context=context)
383 'type': 'ir.actions.report.xml',
384 'report_name': 'account.invoice',
389 def action_invoice_sent(self, cr, uid, ids, context=None):
391 This function opens a window to compose an email, with the edi invoice template message loaded by default
393 mod_obj = self.pool.get('ir.model.data')
394 template = mod_obj.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')
395 template_id = template and template[1] or False
396 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
397 res_id = res and res[1] or False
398 ctx = dict(context, active_model='account.invoice', active_id=ids[0])
399 ctx.update({'mail.compose.template_id': template_id})
403 'res_model': 'mail.compose.message',
404 'views': [(res_id, 'form')],
406 'type': 'ir.actions.act_window',
412 def confirm_paid(self, cr, uid, ids, context=None):
415 self.write(cr, uid, ids, {'state':'paid'}, context=context)
416 self.confirm_paid_send_note(cr, uid, ids, context=context)
419 def unlink(self, cr, uid, ids, context=None):
422 invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
425 if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
426 unlink_ids.append(t['id'])
428 raise osv.except_osv(_('Invalid action !'), _('You can not delete an invoice which is open or paid. We suggest you to refund it instead.'))
429 osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
432 def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
433 date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
434 invoice_addr_id = False
435 partner_payment_term = False
438 fiscal_position = False
440 opt = [('uid', str(uid))]
443 opt.insert(0, ('id', partner_id))
444 res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['invoice'])
445 invoice_addr_id = res['invoice']
446 p = self.pool.get('res.partner').browse(cr, uid, partner_id)
448 if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
449 property_obj = self.pool.get('ir.property')
450 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
451 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
453 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
455 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
456 rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
457 pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
458 rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
459 pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
460 if not rec_res_id and not pay_res_id:
461 raise osv.except_osv(_('Configuration Error !'),
462 _('Can not find a chart of accounts for this company, you should create one.'))
463 account_obj = self.pool.get('account.account')
464 rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
465 pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
466 p.property_account_receivable = rec_obj_acc[0]
467 p.property_account_payable = pay_obj_acc[0]
469 if type in ('out_invoice', 'out_refund'):
470 acc_id = p.property_account_receivable.id
472 acc_id = p.property_account_payable.id
473 fiscal_position = p.property_account_position and p.property_account_position.id or False
474 partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
476 bank_id = p.bank_ids[0].id
479 'account_id': acc_id,
480 'payment_term': partner_payment_term,
481 'fiscal_position': fiscal_position
485 if type in ('in_invoice', 'in_refund'):
486 result['value']['partner_bank_id'] = bank_id
488 if payment_term != partner_payment_term:
489 if partner_payment_term:
490 to_update = self.onchange_payment_term_date_invoice(
491 cr, uid, ids, partner_payment_term, date_invoice)
492 result['value'].update(to_update['value'])
494 result['value']['date_due'] = False
496 if partner_bank_id != bank_id:
497 to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
498 result['value'].update(to_update['value'])
501 def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
504 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
505 currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
507 'currency_id': currency_id,
512 def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
514 if not payment_term_id:
517 date_invoice = time.strftime('%Y-%m-%d')
518 pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
520 pterm_list = [line[0] for line in pterm_list]
522 res = {'value':{'date_due': pterm_list[-1]}}
524 raise osv.except_osv(_('Data Insufficient !'), _('The payment term of supplier does not have a payment term line!'))
527 def onchange_invoice_line(self, cr, uid, ids, lines):
530 def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
533 def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
536 obj_journal = self.pool.get('account.journal')
537 account_obj = self.pool.get('account.account')
538 inv_line_obj = self.pool.get('account.invoice.line')
539 if company_id and part_id and type:
541 partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
542 if partner_obj.property_account_payable and partner_obj.property_account_receivable:
543 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
544 property_obj = self.pool.get('ir.property')
545 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
546 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
548 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
550 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
551 rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
552 pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
553 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
554 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
555 if not rec_res_id and not pay_res_id:
556 raise osv.except_osv(_('Configuration Error !'),
557 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
558 if type in ('out_invoice', 'out_refund'):
562 val= {'account_id': acc_id}
565 inv_obj = self.browse(cr,uid,ids)
566 for line in inv_obj[0].invoice_line:
568 if line.account_id.company_id.id != company_id:
569 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
571 raise osv.except_osv(_('Configuration Error !'),
572 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
573 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
576 for inv_line in invoice_line:
577 obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
578 if obj_l.company_id.id != company_id:
579 raise osv.except_osv(_('Configuration Error !'),
580 _('Invoice line account company does not match with invoice company.'))
583 if company_id and type:
584 if type in ('out_invoice'):
585 journal_type = 'sale'
586 elif type in ('out_refund'):
587 journal_type = 'sale_refund'
588 elif type in ('in_refund'):
589 journal_type = 'purchase_refund'
591 journal_type = 'purchase'
592 journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
594 val['journal_id'] = journal_ids[0]
595 ir_values_obj = self.pool.get('ir.values')
596 res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
597 for r in res_journal_default:
598 if r[1] == 'journal_id' and r[2] in journal_ids:
599 val['journal_id'] = r[2]
600 if not val.get('journal_id', False):
601 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)))
602 dom = {'journal_id': [('id', 'in', journal_ids)]}
604 journal_ids = obj_journal.search(cr, uid, [])
606 return {'value': val, 'domain': dom}
608 # go from canceled state to draft state
609 def action_cancel_draft(self, cr, uid, ids, *args):
610 self.write(cr, uid, ids, {'state':'draft'})
611 wf_service = netsvc.LocalService("workflow")
613 wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
614 wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
620 # return the ids of the move lines which has the same account than the invoice
622 def move_line_id_payment_get(self, cr, uid, ids, *args):
623 if not ids: return []
624 result = self.move_line_id_payment_gets(cr, uid, ids, *args)
625 return result.get(ids[0], [])
627 def move_line_id_payment_gets(self, cr, uid, ids, *args):
629 if not ids: return res
630 cr.execute('SELECT i.id, l.id '\
631 'FROM account_move_line l '\
632 'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
634 'AND l.account_id=i.account_id',
636 for r in cr.fetchall():
637 res.setdefault(r[0], [])
638 res[r[0]].append( r[1] )
641 def copy(self, cr, uid, id, default=None, context=None):
642 default = default or {}
648 'internal_number': False,
652 if 'date_invoice' not in default:
656 if 'date_due' not in default:
660 return super(account_invoice, self).copy(cr, uid, id, default, context)
662 def test_paid(self, cr, uid, ids, *args):
663 res = self.move_line_id_payment_get(cr, uid, ids)
668 cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
669 ok = ok and bool(cr.fetchone()[0])
672 def button_reset_taxes(self, cr, uid, ids, context=None):
676 ait_obj = self.pool.get('account.invoice.tax')
678 cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
679 partner = self.browse(cr, uid, id, context=ctx).partner_id
681 ctx.update({'lang': partner.lang})
682 for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
683 ait_obj.create(cr, uid, taxe)
684 # Update the stored value (fields.function), so we write to trigger recompute
685 self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
688 def button_compute(self, cr, uid, ids, context=None, set_total=False):
689 self.button_reset_taxes(cr, uid, ids, context)
690 for inv in self.browse(cr, uid, ids, context=context):
692 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
695 def _convert_ref(self, cr, uid, ref):
696 return (ref or '').replace('/','')
698 def _get_analytic_lines(self, cr, uid, id, context=None):
701 inv = self.browse(cr, uid, id)
702 cur_obj = self.pool.get('res.currency')
704 company_currency = inv.company_id.currency_id.id
705 if inv.type in ('out_invoice', 'in_refund'):
710 iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
712 if il['account_analytic_id']:
713 if inv.type in ('in_invoice', 'in_refund'):
716 ref = self._convert_ref(cr, uid, inv.number)
717 if not inv.journal_id.analytic_journal_id:
718 raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
719 il['analytic_lines'] = [(0,0, {
721 'date': inv['date_invoice'],
722 'account_id': il['account_analytic_id'],
723 'unit_amount': il['quantity'],
724 'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
725 'product_id': il['product_id'],
726 'product_uom_id': il['uos_id'],
727 'general_account_id': il['account_id'],
728 'journal_id': inv.journal_id.analytic_journal_id.id,
733 def action_date_assign(self, cr, uid, ids, *args):
734 for inv in self.browse(cr, uid, ids):
735 res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
736 if res and res['value']:
737 self.write(cr, uid, [inv.id], res['value'])
740 def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
741 """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
742 Hook method to be overridden in additional modules to verify and possibly alter the
743 move lines to be created by an invoice, for special cases.
744 :param invoice_browse: browsable record of the invoice that is generating the move lines
745 :param move_lines: list of dictionaries with the account.move.lines (as for create())
746 :return: the (possibly updated) final move_lines to create for this invoice
750 def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
752 for tax in compute_taxes.values():
753 ait_obj.create(cr, uid, tax)
756 for tax in inv.tax_line:
759 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id)
761 if not key in compute_taxes:
762 raise osv.except_osv(_('Warning !'), _('Global taxes defined, but they are not in invoice lines !'))
763 base = compute_taxes[key]['base']
764 if abs(base - tax.base) > inv.company_id.currency_id.rounding:
765 raise osv.except_osv(_('Warning !'), _('Tax base different!\nClick on compute to update the tax base.'))
766 for key in compute_taxes:
767 if not key in tax_key:
768 raise osv.except_osv(_('Warning !'), _('Taxes are missing!\nClick on compute button.'))
770 def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
773 cur_obj = self.pool.get('res.currency')
774 for i in invoice_move_lines:
775 if inv.currency_id.id != company_currency:
776 i['currency_id'] = inv.currency_id.id
777 i['amount_currency'] = i['price']
778 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
779 company_currency, i['price'],
780 context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
782 i['amount_currency'] = False
783 i['currency_id'] = False
785 if inv.type in ('out_invoice','in_refund'):
787 total_currency += i['amount_currency'] or i['price']
788 i['price'] = - i['price']
791 total_currency -= i['amount_currency'] or i['price']
792 return total, total_currency, invoice_move_lines
794 def inv_line_characteristic_hashcode(self, invoice, invoice_line):
795 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
796 will be grouped together if the journal has the 'group line' option. Of course a module
797 can add fields to invoice lines that would need to be tested too before merging lines
799 return "%s-%s-%s-%s-%s"%(
800 invoice_line['account_id'],
801 invoice_line.get('tax_code_id',"False"),
802 invoice_line.get('product_id',"False"),
803 invoice_line.get('analytic_account_id',"False"),
804 invoice_line.get('date_maturity',"False"))
806 def group_lines(self, cr, uid, iml, line, inv):
807 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
808 if inv.journal_id.group_invoice_lines:
811 tmp = self.inv_line_characteristic_hashcode(inv, l)
814 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
815 line2[tmp]['debit'] = (am > 0) and am or 0.0
816 line2[tmp]['credit'] = (am < 0) and -am or 0.0
817 line2[tmp]['tax_amount'] += l['tax_amount']
818 line2[tmp]['analytic_lines'] += l['analytic_lines']
822 for key, val in line2.items():
823 line.append((0,0,val))
826 def action_move_create(self, cr, uid, ids, context=None):
827 """Creates invoice related analytics and financial move lines"""
828 ait_obj = self.pool.get('account.invoice.tax')
829 cur_obj = self.pool.get('res.currency')
830 period_obj = self.pool.get('account.period')
831 payment_term_obj = self.pool.get('account.payment.term')
832 journal_obj = self.pool.get('account.journal')
833 move_obj = self.pool.get('account.move')
836 for inv in self.browse(cr, uid, ids, context=context):
837 if not inv.journal_id.sequence_id:
838 raise osv.except_osv(_('Error !'), _('Please define sequence on the journal related to this invoice.'))
839 if not inv.invoice_line:
840 raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
845 ctx.update({'lang': inv.partner_id.lang})
846 if not inv.date_invoice:
847 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
848 company_currency = inv.company_id.currency_id.id
849 # create the analytical lines
850 # one move line per invoice line
851 iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
852 # check if taxes are all computed
853 compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
854 self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
856 # I disabled the check_total feature
857 #if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
858 # raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
861 total_fixed = total_percent = 0
862 for line in inv.payment_term.line_ids:
863 if line.value == 'fixed':
864 total_fixed += line.value_amount
865 if line.value == 'procent':
866 total_percent += line.value_amount
867 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
868 if (total_fixed + total_percent) > 100:
869 raise osv.except_osv(_('Error !'), _("Can not create the invoice !\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. The latest line of your payment term must be of type 'balance' to avoid rounding issues."))
871 # one move line per tax line
872 iml += ait_obj.move_line_get(cr, uid, inv.id)
875 if inv.type in ('in_invoice', 'in_refund'):
877 entry_type = 'journal_pur_voucher'
878 if inv.type == 'in_refund':
879 entry_type = 'cont_voucher'
881 ref = self._convert_ref(cr, uid, inv.number)
882 entry_type = 'journal_sale_vou'
883 if inv.type == 'out_refund':
884 entry_type = 'cont_voucher'
886 diff_currency_p = inv.currency_id.id <> company_currency
887 # create one move line for the total and possibly adjust the other lines amount
890 total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
891 acc_id = inv.account_id.id
893 name = inv['name'] or '/'
896 totlines = payment_term_obj.compute(cr,
897 uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
899 res_amount_currency = total_currency
901 ctx.update({'date': inv.date_invoice})
903 if inv.currency_id.id != company_currency:
904 amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
906 amount_currency = False
908 # last line add the diff
909 res_amount_currency -= amount_currency or 0
911 if i == len(totlines):
912 amount_currency += res_amount_currency
918 'account_id': acc_id,
919 'date_maturity': t[0],
920 'amount_currency': diff_currency_p \
921 and amount_currency or False,
922 'currency_id': diff_currency_p \
923 and inv.currency_id.id or False,
931 'account_id': acc_id,
932 'date_maturity': inv.date_due or False,
933 'amount_currency': diff_currency_p \
934 and total_currency or False,
935 'currency_id': diff_currency_p \
936 and inv.currency_id.id or False,
940 date = inv.date_invoice or time.strftime('%Y-%m-%d')
941 part = inv.partner_id.id
943 line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
945 line = self.group_lines(cr, uid, iml, line, inv)
947 journal_id = inv.journal_id.id
948 journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
949 if journal.centralisation:
950 raise osv.except_osv(_('UserError'),
951 _('You cannot create an invoice on a centralised journal. Uncheck the centralised counterpart box in the related journal from the configuration menu.'))
953 line = self.finalize_invoice_move_lines(cr, uid, inv, line)
956 'ref': inv.reference and inv.reference or inv.name,
958 'journal_id': journal_id,
960 'narration':inv.comment
962 period_id = inv.period_id and inv.period_id.id or False
963 ctx.update({'company_id': inv.company_id.id})
965 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
966 period_id = period_ids and period_ids[0] or False
968 move['period_id'] = period_id
970 i[2]['period_id'] = period_id
972 move_id = move_obj.create(cr, uid, move, context=ctx)
973 new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
974 # make the invoice point to that move
975 self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
976 # Pass invoice in context in method post: used if you want to get the same
977 # account move reference when creating the same invoice after a cancelled one:
978 ctx.update({'invoice':inv})
979 move_obj.post(cr, uid, [move_id], context=ctx)
980 self._log_event(cr, uid, ids)
983 def invoice_validate(self, cr, uid, ids, context=None):
984 self.write(cr, uid, ids, {'state':'open'}, context=context)
987 def line_get_convert(self, cr, uid, x, part, date, context=None):
989 'date_maturity': x.get('date_maturity', False),
991 'name': x['name'][:64],
993 'debit': x['price']>0 and x['price'],
994 'credit': x['price']<0 and -x['price'],
995 'account_id': x['account_id'],
996 'analytic_lines': x.get('analytic_lines', []),
997 'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
998 'currency_id': x.get('currency_id', False),
999 'tax_code_id': x.get('tax_code_id', False),
1000 'tax_amount': x.get('tax_amount', False),
1001 'ref': x.get('ref', False),
1002 'quantity': x.get('quantity',1.00),
1003 'product_id': x.get('product_id', False),
1004 'product_uom_id': x.get('uos_id', False),
1005 'analytic_account_id': x.get('account_analytic_id', False),
1008 def action_number(self, cr, uid, ids, context=None):
1011 #TODO: not correct fix but required a frech values before reading it.
1012 self.write(cr, uid, ids, {})
1014 for obj_inv in self.browse(cr, uid, ids, context=context):
1016 invtype = obj_inv.type
1017 number = obj_inv.number
1018 move_id = obj_inv.move_id and obj_inv.move_id.id or False
1019 reference = obj_inv.reference or ''
1021 self.write(cr, uid, ids, {'internal_number':number})
1023 if invtype in ('in_invoice', 'in_refund'):
1025 ref = self._convert_ref(cr, uid, number)
1029 ref = self._convert_ref(cr, uid, number)
1031 cr.execute('UPDATE account_move SET ref=%s ' \
1032 'WHERE id=%s AND (ref is null OR ref = \'\')',
1034 cr.execute('UPDATE account_move_line SET ref=%s ' \
1035 'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1037 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1038 'FROM account_move_line ' \
1039 'WHERE account_move_line.move_id = %s ' \
1040 'AND account_analytic_line.move_id = account_move_line.id',
1043 for inv_id, name in self.name_get(cr, uid, [id]):
1044 ctx = context.copy()
1045 if obj_inv.type in ('out_invoice', 'out_refund'):
1046 ctx = self.get_log_context(cr, uid, context=ctx)
1047 message = _("Invoice '%s' is validated.") % name
1048 self.message_append_note(cr, uid, [inv_id], body=message, context=context)
1051 def action_cancel(self, cr, uid, ids, *args):
1052 context = {} # TODO: Use context from arguments
1053 account_move_obj = self.pool.get('account.move')
1054 invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1055 move_ids = [] # ones that we will need to remove
1058 move_ids.append(i['move_id'][0])
1059 if i['payment_ids']:
1060 account_move_line_obj = self.pool.get('account.move.line')
1061 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1062 for move_line in pay_ids:
1063 if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1064 raise osv.except_osv(_('Error !'), _('You can not cancel an invoice which is partially paid! You need to unreconcile related payment entries first!'))
1066 # First, set the invoices as cancelled and detach the move ids
1067 self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1069 # second, invalidate the move(s)
1070 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1071 # delete the move this invoice was pointing to
1072 # Note that the corresponding move_lines and move_reconciles
1073 # will be automatically deleted too
1074 account_move_obj.unlink(cr, uid, move_ids, context=context)
1075 self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1076 self.invoice_cancel_send_note(cr, uid, ids, context=context)
1081 def list_distinct_taxes(self, cr, uid, ids):
1082 invoices = self.browse(cr, uid, ids)
1084 for inv in invoices:
1085 for tax in inv.tax_line:
1086 if not tax['name'] in taxes:
1087 taxes[tax['name']] = {'name': tax['name']}
1088 return taxes.values()
1090 def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1091 #TODO: implement messages system
1094 def name_get(self, cr, uid, ids, context=None):
1098 'out_invoice': 'CI: ',
1099 'in_invoice': 'SI: ',
1100 'out_refund': 'OR: ',
1101 'in_refund': 'SR: ',
1103 return [(r['id'], (r['number']) or types[r['type']] + (r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
1105 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1112 ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1114 ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1115 return self.name_get(cr, user, ids, context)
1117 def _refund_cleanup_lines(self, cr, uid, lines):
1120 del line['invoice_id']
1121 for field in ('company_id', 'partner_id', 'account_id', 'product_id',
1122 'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
1124 line[field] = line[field][0]
1125 if 'invoice_line_tax_id' in line:
1126 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
1127 return map(lambda x: (0,0,x), lines)
1129 def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
1130 invoices = self.read(cr, uid, ids, ['name', 'type', 'number', 'reference', 'comment', 'date_due', 'partner_id', 'partner_contact', 'partner_insite', 'partner_ref', 'payment_term', 'account_id', 'currency_id', 'invoice_line', 'tax_line', 'journal_id'])
1131 obj_invoice_line = self.pool.get('account.invoice.line')
1132 obj_invoice_tax = self.pool.get('account.invoice.tax')
1133 obj_journal = self.pool.get('account.journal')
1135 for invoice in invoices:
1139 'out_invoice': 'out_refund', # Customer Invoice
1140 'in_invoice': 'in_refund', # Supplier Invoice
1141 'out_refund': 'out_invoice', # Customer Refund
1142 'in_refund': 'in_invoice', # Supplier Refund
1145 invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
1146 invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
1148 tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
1149 tax_lines = filter(lambda l: l['manual'], tax_lines)
1150 tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
1152 refund_journal_ids = [journal_id]
1153 elif invoice['type'] == 'in_invoice':
1154 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
1156 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
1159 date = time.strftime('%Y-%m-%d')
1161 'type': type_dict[invoice['type']],
1162 'date_invoice': date,
1165 'invoice_line': invoice_lines,
1166 'tax_line': tax_lines,
1167 'journal_id': refund_journal_ids
1171 'period_id': period_id,
1175 'name': description,
1177 # take the id part of the tuple returned for many2one fields
1178 for field in ('partner_id',
1179 'account_id', 'currency_id', 'payment_term', 'journal_id'):
1180 invoice[field] = invoice[field] and invoice[field][0]
1181 # create the new invoice
1182 new_ids.append(self.create(cr, uid, invoice))
1186 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=''):
1189 #TODO check if we can use different period for payment and the writeoff line
1190 assert len(ids)==1, "Can only pay one invoice at a time"
1191 invoice = self.browse(cr, uid, ids[0], context=context)
1192 src_account_id = invoice.account_id.id
1193 # Take the seq as name for move
1194 types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1195 direction = types[invoice.type]
1196 #take the choosen date
1197 if 'date_p' in context and context['date_p']:
1198 date=context['date_p']
1200 date=time.strftime('%Y-%m-%d')
1202 # Take the amount in currency and the currency of the payment
1203 if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1204 amount_currency = context['amount_currency']
1205 currency_id = context['currency_id']
1207 amount_currency = False
1210 pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1211 if invoice.type in ('in_invoice', 'out_invoice'):
1212 if pay_journal['type'] == 'bank':
1213 entry_type = 'bank_pay_voucher' # Bank payment
1215 entry_type = 'pay_voucher' # Cash payment
1217 entry_type = 'cont_voucher'
1218 if invoice.type in ('in_invoice', 'in_refund'):
1219 ref = invoice.reference
1221 ref = self._convert_ref(cr, uid, invoice.number)
1222 # Pay attention to the sign for both debit/credit AND amount_currency
1224 'debit': direction * pay_amount>0 and direction * pay_amount,
1225 'credit': direction * pay_amount<0 and - direction * pay_amount,
1226 'account_id': src_account_id,
1227 'partner_id': invoice.partner_id.id,
1230 'currency_id':currency_id,
1231 'amount_currency':amount_currency and direction * amount_currency or 0.0,
1232 'company_id': invoice.company_id.id,
1235 'debit': direction * pay_amount<0 and - direction * pay_amount,
1236 'credit': direction * pay_amount>0 and direction * pay_amount,
1237 'account_id': pay_account_id,
1238 'partner_id': invoice.partner_id.id,
1241 'currency_id':currency_id,
1242 'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1243 'company_id': invoice.company_id.id,
1247 name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1251 lines = [(0, 0, l1), (0, 0, l2)]
1252 move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1253 move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1257 line = self.pool.get('account.move.line')
1258 move_ids = [move_id,]
1260 move_ids.append(invoice.move_id.id)
1261 cr.execute('SELECT id FROM account_move_line '\
1262 'WHERE move_id IN %s',
1263 ((move_id, invoice.move_id.id),))
1264 lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1265 for l in lines+invoice.payment_ids:
1266 if l.account_id.id == src_account_id:
1267 line_ids.append(l.id)
1268 total += (l.debit or 0.0) - (l.credit or 0.0)
1270 inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1271 if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1272 self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1274 code = invoice.currency_id.symbol
1275 # TODO: use currency's formatting function
1276 msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining)") % \
1277 (name, pay_amount, code, invoice.amount_total, code, total, code)
1278 self.message_append_note(cr, uid, [inv_id], body=msg, context=context)
1279 self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1281 # Update the stored value (fields.function), so we write to trigger recompute
1282 self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1285 # -----------------------------------------
1286 # OpenChatter notifications and need_action
1287 # -----------------------------------------
1289 def _get_document_type(self, type):
1291 'out_invoice': 'Customer invoice',
1292 'in_invoice': 'Supplier invoice',
1293 'out_refund': 'Customer Refund',
1294 'in_refund': 'Supplier Refund',
1296 return type_dict.get(type, 'Invoice')
1298 def create_send_note(self, cr, uid, ids, context=None):
1299 for obj in self.browse(cr, uid, ids, context=context):
1300 self.message_append_note(cr, uid, [obj.id],body=_("%s <b>created</b>.") % (self._get_document_type(obj.type)), context=context)
1302 def confirm_paid_send_note(self, cr, uid, ids, context=None):
1303 for obj in self.browse(cr, uid, ids, context=context):
1304 self.message_append_note(cr, uid, [obj.id], body=_("%s <b>paid</b>.") % (self._get_document_type(obj.type)), context=context)
1306 def invoice_cancel_send_note(self, cr, uid, ids, context=None):
1307 for obj in self.browse(cr, uid, ids, context=context):
1308 self.message_append_note(cr, uid, [obj.id], body=_("%s <b>cancelled</b>.") % (self._get_document_type(obj.type)), context=context)
1312 class account_invoice_line(osv.osv):
1314 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1316 tax_obj = self.pool.get('account.tax')
1317 cur_obj = self.pool.get('res.currency')
1318 for line in self.browse(cr, uid, ids):
1319 price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1320 taxes = tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, price, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id)
1321 res[line.id] = taxes['total']
1323 cur = line.invoice_id.currency_id
1324 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1327 def _price_unit_default(self, cr, uid, context=None):
1330 if context.get('check_total', False):
1331 t = context['check_total']
1332 for l in context.get('invoice_line', {}):
1333 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1334 tax_obj = self.pool.get('account.tax')
1335 p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1336 t = t - (p * l[2].get('quantity'))
1337 taxes = l[2].get('invoice_line_tax_id')
1338 if len(taxes[0]) >= 3 and taxes[0][2]:
1339 taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1340 for tax in tax_obj.compute_all(cr, uid, taxes, p,l[2].get('quantity'), l[2].get('product_id', False), context.get('partner_id', False))['taxes']:
1341 t = t - tax['amount']
1345 _name = "account.invoice.line"
1346 _description = "Invoice Line"
1348 'name': fields.char('Description', size=256, required=True),
1349 'origin': fields.char('Source', size=256, help="Reference of the document that produced this invoice."),
1350 'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1351 'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1352 'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1353 '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."),
1354 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
1355 'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
1356 digits_compute= dp.get_precision('Account'), store=True),
1357 'quantity': fields.float('Quantity', required=True),
1358 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
1359 'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1360 'note': fields.text('Notes'),
1361 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1362 'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1363 'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1368 'price_unit': _price_unit_default,
1371 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1374 res = super(account_invoice_line,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
1375 if context.get('type', False):
1376 doc = etree.XML(res['arch'])
1377 for node in doc.xpath("//field[@name='product_id']"):
1378 if context['type'] in ('in_invoice', 'in_refund'):
1379 node.set('domain', "[('purchase_ok', '=', True)]")
1381 node.set('domain', "[('sale_ok', '=', True)]")
1382 res['arch'] = etree.tostring(doc)
1385 def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1388 company_id = company_id if company_id != None else context.get('company_id',False)
1389 context = dict(context)
1390 context.update({'company_id': company_id})
1392 raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1394 if type in ('in_invoice', 'in_refund'):
1395 return {'value': {}, 'domain':{'product_uom':[]}}
1397 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1398 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1399 fpos_obj = self.pool.get('account.fiscal.position')
1400 fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1403 context.update({'lang': part.lang})
1405 res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1407 if type in ('out_invoice','out_refund'):
1408 a = res.product_tmpl_id.property_account_income.id
1410 a = res.categ_id.property_account_income_categ.id
1412 a = res.product_tmpl_id.property_account_expense.id
1414 a = res.categ_id.property_account_expense_categ.id
1415 a = fpos_obj.map_account(cr, uid, fpos, a)
1417 result['account_id'] = a
1419 if type in ('out_invoice', 'out_refund'):
1420 taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1422 taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1423 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1425 if type in ('in_invoice', 'in_refund'):
1426 result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1428 result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1429 result['name'] = res.partner_ref
1432 result['uos_id'] = res.uom_id.id or uom or False
1433 result['note'] = res.description
1434 if result['uos_id']:
1435 res2 = res.uom_id.category_id.id
1437 domain = {'uos_id':[('category_id','=',res2 )]}
1439 res_final = {'value':result, 'domain':domain}
1441 if not company_id or not currency_id:
1444 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1445 currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1447 if company.currency_id.id != currency.id:
1448 if type in ('in_invoice', 'in_refund'):
1449 res_final['value']['price_unit'] = res.standard_price
1450 new_price = res_final['value']['price_unit'] * currency.rate
1451 res_final['value']['price_unit'] = new_price
1454 uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1455 if res.uom_id.category_id.id == uom.category_id.id:
1456 new_price = res_final['value']['price_unit'] * uom.factor_inv
1457 res_final['value']['price_unit'] = new_price
1460 def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1463 company_id = company_id if company_id != None else context.get('company_id',False)
1464 context = dict(context)
1465 context.update({'company_id': company_id})
1467 res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context)
1468 if 'uos_id' in res['value']:
1469 del res['value']['uos_id']
1471 res['value']['price_unit'] = 0.0
1473 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1474 prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1475 if prod.uom_id.category_id.id != prod_uom.category_id.id:
1477 'title': _('Warning!'),
1478 'message': _('You selected an Unit of Measure which is not compatible with the product.')
1480 return {'value': res['value'], 'warning': warning}
1483 def move_line_get(self, cr, uid, invoice_id, context=None):
1485 tax_obj = self.pool.get('account.tax')
1486 cur_obj = self.pool.get('res.currency')
1489 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1490 company_currency = inv.company_id.currency_id.id
1492 for line in inv.invoice_line:
1493 mres = self.move_line_get_item(cr, uid, line, context)
1497 tax_code_found= False
1498 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1499 (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1500 line.quantity, line.product_id,
1501 inv.partner_id)['taxes']:
1503 if inv.type in ('out_invoice', 'in_invoice'):
1504 tax_code_id = tax['base_code_id']
1505 tax_amount = line.price_subtotal * tax['base_sign']
1507 tax_code_id = tax['ref_base_code_id']
1508 tax_amount = line.price_subtotal * tax['ref_base_sign']
1513 res.append(self.move_line_get_item(cr, uid, line, context))
1514 res[-1]['price'] = 0.0
1515 res[-1]['account_analytic_id'] = False
1516 elif not tax_code_id:
1518 tax_code_found = True
1520 res[-1]['tax_code_id'] = tax_code_id
1521 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1524 def move_line_get_item(self, cr, uid, line, context=None):
1527 'name': line.name[:64],
1528 'price_unit':line.price_unit,
1529 'quantity':line.quantity,
1530 'price':line.price_subtotal,
1531 'account_id':line.account_id.id,
1532 'product_id':line.product_id.id,
1533 'uos_id':line.uos_id.id,
1534 'account_analytic_id':line.account_analytic_id.id,
1535 'taxes':line.invoice_line_tax_id,
1538 # Set the tax field according to the account and the fiscal position
1540 def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1543 taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1544 fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1545 tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1547 product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1548 partner_id=partner_id, fposition_id=fposition_id)
1549 unique_tax_ids = set(tax_ids)
1550 if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1551 unique_tax_ids |= set(product_change_result['value']['invoice_line_tax_id'])
1552 return {'value':{'invoice_line_tax_id': list(unique_tax_ids)}}
1554 account_invoice_line()
1556 class account_invoice_tax(osv.osv):
1557 _name = "account.invoice.tax"
1558 _description = "Invoice Tax"
1560 def _count_factor(self, cr, uid, ids, name, args, context=None):
1562 for invoice_tax in self.browse(cr, uid, ids, context=context):
1563 res[invoice_tax.id] = {
1567 if invoice_tax.amount <> 0.0:
1568 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1569 res[invoice_tax.id]['factor_tax'] = factor_tax
1571 if invoice_tax.base <> 0.0:
1572 factor_base = invoice_tax.base_amount / invoice_tax.base
1573 res[invoice_tax.id]['factor_base'] = factor_base
1578 'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1579 'name': fields.char('Tax Description', size=64, required=True),
1580 'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1581 'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1582 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1583 'manual': fields.boolean('Manual'),
1584 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1585 'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1586 'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1587 'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1588 'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1589 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1590 'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1591 'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1594 def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1595 cur_obj = self.pool.get('res.currency')
1596 company_obj = self.pool.get('res.company')
1597 company_currency = False
1600 factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1602 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1603 if currency_id and company_currency:
1604 base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1605 return {'value': {'base_amount':base}}
1607 def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1608 cur_obj = self.pool.get('res.currency')
1609 company_obj = self.pool.get('res.company')
1610 company_currency = False
1613 factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1615 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1616 if currency_id and company_currency:
1617 amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1618 return {'value': {'tax_amount': amount}}
1626 def compute(self, cr, uid, invoice_id, context=None):
1628 tax_obj = self.pool.get('account.tax')
1629 cur_obj = self.pool.get('res.currency')
1630 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1631 cur = inv.currency_id
1632 company_currency = inv.company_id.currency_id.id
1634 for line in inv.invoice_line:
1635 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, line.product_id, inv.partner_id)['taxes']:
1636 tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
1638 val['invoice_id'] = inv.id
1639 val['name'] = tax['name']
1640 val['amount'] = tax['amount']
1641 val['manual'] = False
1642 val['sequence'] = tax['sequence']
1643 val['base'] = tax['price_unit'] * line['quantity']
1645 if inv.type in ('out_invoice','in_invoice'):
1646 val['base_code_id'] = tax['base_code_id']
1647 val['tax_code_id'] = tax['tax_code_id']
1648 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)
1649 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)
1650 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1652 val['base_code_id'] = tax['ref_base_code_id']
1653 val['tax_code_id'] = tax['ref_tax_code_id']
1654 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)
1655 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)
1656 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1658 key = (val['tax_code_id'], val['base_code_id'], val['account_id'])
1659 if not key in tax_grouped:
1660 tax_grouped[key] = val
1662 tax_grouped[key]['amount'] += val['amount']
1663 tax_grouped[key]['base'] += val['base']
1664 tax_grouped[key]['base_amount'] += val['base_amount']
1665 tax_grouped[key]['tax_amount'] += val['tax_amount']
1667 for t in tax_grouped.values():
1668 t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1669 t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1670 t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1671 t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1674 def move_line_get(self, cr, uid, invoice_id):
1676 cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1677 for t in cr.dictfetchall():
1678 if not t['amount'] \
1679 and not t['tax_code_id'] \
1680 and not t['tax_amount']:
1685 'price_unit': t['amount'],
1687 'price': t['amount'] or 0.0,
1688 'account_id': t['account_id'],
1689 'tax_code_id': t['tax_code_id'],
1690 'tax_amount': t['tax_amount']
1694 account_invoice_tax()
1697 class res_partner(osv.osv):
1698 """ Inherits partner and adds invoice information in the partner form """
1699 _inherit = 'res.partner'
1701 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1704 def copy(self, cr, uid, id, default=None, context=None):
1705 default = default or {}
1706 default.update({'invoice_ids' : []})
1707 return super(res_partner, self).copy(cr, uid, id, default, context)
1711 class mail_message(osv.osv):
1712 _name = 'mail.message'
1713 _inherit = 'mail.message'
1715 def _postprocess_sent_message(self, cr, uid, message, context=None):
1716 if message.model == 'account.invoice':
1717 self.pool.get('account.invoice').write(cr, uid, [message.res_id], {'sent':True}, context=context)
1718 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1721 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: