1726 lines
87 KiB
Python
1726 lines
87 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
import time
|
|
from lxml import etree
|
|
import decimal_precision as dp
|
|
|
|
import netsvc
|
|
import pooler
|
|
from osv import fields, osv, orm
|
|
from tools.translate import _
|
|
|
|
class account_invoice(osv.osv):
|
|
def _amount_all(self, cr, uid, ids, name, args, context=None):
|
|
res = {}
|
|
for invoice in self.browse(cr, uid, ids, context=context):
|
|
res[invoice.id] = {
|
|
'amount_untaxed': 0.0,
|
|
'amount_tax': 0.0,
|
|
'amount_total': 0.0
|
|
}
|
|
for line in invoice.invoice_line:
|
|
res[invoice.id]['amount_untaxed'] += line.price_subtotal
|
|
for line in invoice.tax_line:
|
|
res[invoice.id]['amount_tax'] += line.amount
|
|
res[invoice.id]['amount_total'] = res[invoice.id]['amount_tax'] + res[invoice.id]['amount_untaxed']
|
|
return res
|
|
|
|
def _get_journal(self, cr, uid, context=None):
|
|
if context is None:
|
|
context = {}
|
|
type_inv = context.get('type', 'out_invoice')
|
|
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
|
company_id = context.get('company_id', user.company_id.id)
|
|
type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'}
|
|
journal_obj = self.pool.get('account.journal')
|
|
res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')),
|
|
('company_id', '=', company_id)],
|
|
limit=1)
|
|
return res and res[0] or False
|
|
|
|
def _get_currency(self, cr, uid, context=None):
|
|
res = False
|
|
journal_id = self._get_journal(cr, uid, context=context)
|
|
if journal_id:
|
|
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
|
|
res = journal.currency and journal.currency.id or journal.company_id.currency_id.id
|
|
return res
|
|
|
|
def _get_journal_analytic(self, cr, uid, type_inv, context=None):
|
|
type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
|
|
tt = type2journal.get(type_inv, 'sale')
|
|
result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
|
|
if not result:
|
|
raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,))
|
|
return result[0]
|
|
|
|
def _get_type(self, cr, uid, context=None):
|
|
if context is None:
|
|
context = {}
|
|
return context.get('type', 'out_invoice')
|
|
|
|
def _reconciled(self, cr, uid, ids, name, args, context=None):
|
|
res = {}
|
|
for id in ids:
|
|
res[id] = self.test_paid(cr, uid, [id])
|
|
return res
|
|
|
|
def _get_reference_type(self, cr, uid, context=None):
|
|
return [('none', _('Free Reference'))]
|
|
|
|
def _amount_residual(self, cr, uid, ids, name, args, context=None):
|
|
result = {}
|
|
for invoice in self.browse(cr, uid, ids, context=context):
|
|
result[invoice.id] = 0.0
|
|
if invoice.move_id:
|
|
for m in invoice.move_id.line_id:
|
|
if m.account_id.type in ('receivable','payable'):
|
|
result[invoice.id] += m.amount_residual_currency
|
|
return result
|
|
|
|
# Give Journal Items related to the payment reconciled to this invoice
|
|
# Return ids of partial and total payments related to the selected invoices
|
|
def _get_lines(self, cr, uid, ids, name, arg, context=None):
|
|
res = {}
|
|
for invoice in self.browse(cr, uid, ids, context=context):
|
|
id = invoice.id
|
|
res[id] = []
|
|
if not invoice.move_id:
|
|
continue
|
|
data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
|
|
partial_ids = []
|
|
for line in data_lines:
|
|
ids_line = []
|
|
if line.reconcile_id:
|
|
ids_line = line.reconcile_id.line_id
|
|
elif line.reconcile_partial_id:
|
|
ids_line = line.reconcile_partial_id.line_partial_ids
|
|
l = map(lambda x: x.id, ids_line)
|
|
partial_ids.append(line.id)
|
|
res[id] =[x for x in l if x <> line.id and x not in partial_ids]
|
|
return res
|
|
|
|
def _get_invoice_line(self, cr, uid, ids, context=None):
|
|
result = {}
|
|
for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
|
|
result[line.invoice_id.id] = True
|
|
return result.keys()
|
|
|
|
def _get_invoice_tax(self, cr, uid, ids, context=None):
|
|
result = {}
|
|
for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
|
|
result[tax.invoice_id.id] = True
|
|
return result.keys()
|
|
|
|
def _compute_lines(self, cr, uid, ids, name, args, context=None):
|
|
result = {}
|
|
for invoice in self.browse(cr, uid, ids, context=context):
|
|
src = []
|
|
lines = []
|
|
if invoice.move_id:
|
|
for m in invoice.move_id.line_id:
|
|
temp_lines = []
|
|
if m.reconcile_id:
|
|
temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
|
|
elif m.reconcile_partial_id:
|
|
temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
|
|
lines += [x for x in temp_lines if x not in lines]
|
|
src.append(m.id)
|
|
|
|
lines = filter(lambda x: x not in src, lines)
|
|
result[invoice.id] = lines
|
|
return result
|
|
|
|
def _get_invoice_from_line(self, cr, uid, ids, context=None):
|
|
move = {}
|
|
for line in self.pool.get('account.move.line').browse(cr, uid, ids, context=context):
|
|
if line.reconcile_partial_id:
|
|
for line2 in line.reconcile_partial_id.line_partial_ids:
|
|
move[line2.move_id.id] = True
|
|
if line.reconcile_id:
|
|
for line2 in line.reconcile_id.line_id:
|
|
move[line2.move_id.id] = True
|
|
invoice_ids = []
|
|
if move:
|
|
invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
|
|
return invoice_ids
|
|
|
|
def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
|
|
move = {}
|
|
for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
|
|
for line in r.line_partial_ids:
|
|
move[line.move_id.id] = True
|
|
for line in r.line_id:
|
|
move[line.move_id.id] = True
|
|
|
|
invoice_ids = []
|
|
if move:
|
|
invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
|
|
return invoice_ids
|
|
|
|
_name = "account.invoice"
|
|
_inherit = ['mail.thread']
|
|
_description = 'Invoice'
|
|
_order = "id desc"
|
|
|
|
_columns = {
|
|
'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
|
|
'type': fields.selection([
|
|
('out_invoice','Customer Invoice'),
|
|
('in_invoice','Supplier Invoice'),
|
|
('out_refund','Customer Refund'),
|
|
('in_refund','Supplier Refund'),
|
|
],'Type', readonly=True, select=True, change_default=True),
|
|
|
|
'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
|
|
'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
|
|
'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
|
|
'reference_type': fields.selection(_get_reference_type, 'Payment Reference',
|
|
required=True, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'comment': fields.text('Additional Information'),
|
|
|
|
'state': fields.selection([
|
|
('draft','Draft'),
|
|
('proforma','Pro-forma'),
|
|
('proforma2','Pro-forma'),
|
|
('open','Open'),
|
|
('paid','Paid'),
|
|
('cancel','Cancelled'),
|
|
],'State', select=True, readonly=True,
|
|
help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
|
|
\n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
|
|
\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. \
|
|
\n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
|
|
\n* The \'Cancelled\' state is used when user cancel invoice.'),
|
|
'sent': fields.boolean('Sent', readonly=True, help="It indicates that the invoice has been sent."),
|
|
'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
|
|
'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
|
|
help="If you use payment terms, the due date will be computed automatically at the generation "\
|
|
"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."),
|
|
'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
|
|
'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
|
|
help="If you use payment terms, the due date will be computed automatically at the generation "\
|
|
"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."),
|
|
'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)]}),
|
|
|
|
'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
|
|
'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
|
|
'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
|
|
|
|
'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
|
|
'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed',
|
|
store={
|
|
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
|
|
'account.invoice.tax': (_get_invoice_tax, None, 20),
|
|
'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
|
|
},
|
|
multi='all'),
|
|
'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
|
|
store={
|
|
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
|
|
'account.invoice.tax': (_get_invoice_tax, None, 20),
|
|
'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
|
|
},
|
|
multi='all'),
|
|
'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
|
|
store={
|
|
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
|
|
'account.invoice.tax': (_get_invoice_tax, None, 20),
|
|
'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
|
|
},
|
|
multi='all'),
|
|
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
|
|
'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
|
|
store={
|
|
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
|
|
'account.move.line': (_get_invoice_from_line, None, 50),
|
|
'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
|
|
}, 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."),
|
|
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
|
|
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)]}),
|
|
'move_lines':fields.function(_get_lines, type='many2many', relation='account.move.line', string='Entry Lines'),
|
|
'residual': fields.function(_amount_residual, digits_compute=dp.get_precision('Account'), string='Balance',
|
|
store={
|
|
'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line','move_id'], 50),
|
|
'account.invoice.tax': (_get_invoice_tax, None, 50),
|
|
'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 50),
|
|
'account.move.line': (_get_invoice_from_line, None, 50),
|
|
'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
|
|
},
|
|
help="Remaining amount due."),
|
|
'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
|
|
'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
|
|
'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, states={'draft':[('readonly',False)]}),
|
|
'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
|
|
}
|
|
_defaults = {
|
|
'type': _get_type,
|
|
'state': 'draft',
|
|
'journal_id': _get_journal,
|
|
'currency_id': _get_currency,
|
|
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.invoice', context=c),
|
|
'reference_type': 'none',
|
|
'check_total': 0.0,
|
|
'internal_number': False,
|
|
'user_id': lambda s, cr, u, c: u,
|
|
'sent': False,
|
|
}
|
|
_sql_constraints = [
|
|
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
|
|
]
|
|
|
|
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
|
|
journal_obj = self.pool.get('account.journal')
|
|
if context is None:
|
|
context = {}
|
|
|
|
if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
|
|
partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
|
|
if not view_type:
|
|
view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
|
|
view_type = 'tree'
|
|
if view_type == 'form':
|
|
if partner['supplier'] and not partner['customer']:
|
|
view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
|
|
elif partner['customer'] and not partner['supplier']:
|
|
view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
|
|
if view_id and isinstance(view_id, (list, tuple)):
|
|
view_id = view_id[0]
|
|
res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
|
|
|
|
type = context.get('journal_type', False)
|
|
for field in res['fields']:
|
|
if field == 'journal_id' and type:
|
|
journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
|
|
res['fields'][field]['selection'] = journal_select
|
|
|
|
doc = etree.XML(res['arch'])
|
|
|
|
if context.get('type', False):
|
|
for node in doc.xpath("//field[@name='partner_bank_id']"):
|
|
if context['type'] == 'in_refund':
|
|
node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
|
|
elif context['type'] == 'out_refund':
|
|
node.set('domain', "[('partner_id', '=', partner_id)]")
|
|
res['arch'] = etree.tostring(doc)
|
|
|
|
if view_type == 'search':
|
|
if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
|
|
for node in doc.xpath("//group[@name='extended filter']"):
|
|
doc.remove(node)
|
|
res['arch'] = etree.tostring(doc)
|
|
|
|
if view_type == 'tree':
|
|
partner_string = _('Customer')
|
|
if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
|
|
partner_string = _('Supplier')
|
|
for node in doc.xpath("//field[@name='reference']"):
|
|
node.set('invisible', '0')
|
|
for node in doc.xpath("//field[@name='partner_id']"):
|
|
node.set('string', partner_string)
|
|
res['arch'] = etree.tostring(doc)
|
|
return res
|
|
|
|
def get_log_context(self, cr, uid, context=None):
|
|
if context is None:
|
|
context = {}
|
|
res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
|
|
view_id = res and res[1] or False
|
|
context['view_id'] = view_id
|
|
return context
|
|
|
|
def create(self, cr, uid, vals, context=None):
|
|
if context is None:
|
|
context = {}
|
|
try:
|
|
res = super(account_invoice, self).create(cr, uid, vals, context)
|
|
if res:
|
|
self.create_send_note(cr, uid, [res], context=context)
|
|
return res
|
|
except Exception, e:
|
|
if '"journal_id" viol' in e.args[0]:
|
|
raise orm.except_orm(_('Configuration Error!'),
|
|
_('There is no Sale/Purchase Journal(s) defined.'))
|
|
else:
|
|
raise orm.except_orm(_('Unknown Error!'), str(e))
|
|
|
|
def invoice_print(self, cr, uid, ids, context=None):
|
|
'''
|
|
This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow
|
|
'''
|
|
assert len(ids) == 1, 'This option should only be used for a single id at a time.'
|
|
self.write(cr, uid, ids, {'sent': True}, context=context)
|
|
datas = {
|
|
'ids': ids,
|
|
'model': 'account.invoice',
|
|
'form': self.read(cr, uid, ids[0], context=context)
|
|
}
|
|
return {
|
|
'type': 'ir.actions.report.xml',
|
|
'report_name': 'account.invoice',
|
|
'datas': datas,
|
|
'nodestroy' : True
|
|
}
|
|
|
|
def action_invoice_sent(self, cr, uid, ids, context=None):
|
|
'''
|
|
This function opens a window to compose an email, with the edi invoice template message loaded by default
|
|
'''
|
|
mod_obj = self.pool.get('ir.model.data')
|
|
template = mod_obj.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')
|
|
template_id = template and template[1] or False
|
|
res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
|
|
res_id = res and res[1] or False
|
|
ctx = dict(context, active_model='account.invoice', active_id=ids[0])
|
|
ctx.update({'mail.compose.template_id': template_id})
|
|
return {
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.compose.message',
|
|
'views': [(res_id, 'form')],
|
|
'view_id': res_id,
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'new',
|
|
'context': ctx,
|
|
'nodestroy': True,
|
|
}
|
|
|
|
def confirm_paid(self, cr, uid, ids, context=None):
|
|
if context is None:
|
|
context = {}
|
|
self.write(cr, uid, ids, {'state':'paid'}, context=context)
|
|
self.confirm_paid_send_note(cr, uid, ids, context=context)
|
|
return True
|
|
|
|
def unlink(self, cr, uid, ids, context=None):
|
|
if context is None:
|
|
context = {}
|
|
invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
|
|
unlink_ids = []
|
|
for t in invoices:
|
|
if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
|
|
unlink_ids.append(t['id'])
|
|
else:
|
|
raise osv.except_osv(_('Invalid Action!'), _('You cannot delete an invoice which is open or paid. You should refund it instead.'))
|
|
osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
|
|
return True
|
|
|
|
def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
|
|
date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
|
|
invoice_addr_id = False
|
|
partner_payment_term = False
|
|
acc_id = False
|
|
bank_id = False
|
|
fiscal_position = False
|
|
|
|
opt = [('uid', str(uid))]
|
|
if partner_id:
|
|
|
|
opt.insert(0, ('id', partner_id))
|
|
res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['invoice'])
|
|
invoice_addr_id = res['invoice']
|
|
p = self.pool.get('res.partner').browse(cr, uid, partner_id)
|
|
if company_id:
|
|
if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
|
|
property_obj = self.pool.get('ir.property')
|
|
rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
|
|
pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
|
|
if not rec_pro_id:
|
|
rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
|
|
if not pay_pro_id:
|
|
pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
|
|
rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
|
|
pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
|
|
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
|
|
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
|
|
if not rec_res_id and not pay_res_id:
|
|
raise osv.except_osv(_('Configuration Error!'),
|
|
_('Cannot find a chart of accounts for this company, you should create one.'))
|
|
account_obj = self.pool.get('account.account')
|
|
rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
|
|
pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
|
|
p.property_account_receivable = rec_obj_acc[0]
|
|
p.property_account_payable = pay_obj_acc[0]
|
|
|
|
if type in ('out_invoice', 'out_refund'):
|
|
acc_id = p.property_account_receivable.id
|
|
else:
|
|
acc_id = p.property_account_payable.id
|
|
fiscal_position = p.property_account_position and p.property_account_position.id or False
|
|
partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
|
|
if p.bank_ids:
|
|
bank_id = p.bank_ids[0].id
|
|
|
|
result = {'value': {
|
|
'account_id': acc_id,
|
|
'payment_term': partner_payment_term,
|
|
'fiscal_position': fiscal_position
|
|
}
|
|
}
|
|
|
|
if type in ('in_invoice', 'in_refund'):
|
|
result['value']['partner_bank_id'] = bank_id
|
|
|
|
if payment_term != partner_payment_term:
|
|
if partner_payment_term:
|
|
to_update = self.onchange_payment_term_date_invoice(
|
|
cr, uid, ids, partner_payment_term, date_invoice)
|
|
result['value'].update(to_update['value'])
|
|
else:
|
|
result['value']['date_due'] = False
|
|
|
|
if partner_bank_id != bank_id:
|
|
to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
|
|
result['value'].update(to_update['value'])
|
|
return result
|
|
|
|
def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
|
|
result = {}
|
|
if journal_id:
|
|
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
|
|
currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
|
|
result = {'value': {
|
|
'currency_id': currency_id,
|
|
}
|
|
}
|
|
return result
|
|
|
|
def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
|
|
res = {}
|
|
if not payment_term_id:
|
|
return res
|
|
if not date_invoice:
|
|
date_invoice = time.strftime('%Y-%m-%d')
|
|
pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
|
|
if pterm_list:
|
|
pterm_list = [line[0] for line in pterm_list]
|
|
pterm_list.sort()
|
|
res = {'value':{'date_due': pterm_list[-1]}}
|
|
else:
|
|
raise osv.except_osv(_('Insufficient Data!'), _('The payment term of supplier does not have a payment term line.'))
|
|
return res
|
|
|
|
def onchange_invoice_line(self, cr, uid, ids, lines):
|
|
return {}
|
|
|
|
def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
|
|
return {'value': {}}
|
|
|
|
def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
|
|
val = {}
|
|
dom = {}
|
|
obj_journal = self.pool.get('account.journal')
|
|
account_obj = self.pool.get('account.account')
|
|
inv_line_obj = self.pool.get('account.invoice.line')
|
|
if company_id and part_id and type:
|
|
acc_id = False
|
|
partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
|
|
if partner_obj.property_account_payable and partner_obj.property_account_receivable:
|
|
if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
|
|
property_obj = self.pool.get('ir.property')
|
|
rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
|
|
pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
|
|
if not rec_pro_id:
|
|
rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
|
|
if not pay_pro_id:
|
|
pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
|
|
rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
|
|
pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
|
|
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
|
|
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
|
|
if not rec_res_id and not pay_res_id:
|
|
raise osv.except_osv(_('Configuration Error!'),
|
|
_('Cannot find a chart of account, you should create one from Settings\Configuration\Accounting menu.'))
|
|
if type in ('out_invoice', 'out_refund'):
|
|
acc_id = rec_res_id
|
|
else:
|
|
acc_id = pay_res_id
|
|
val= {'account_id': acc_id}
|
|
if ids:
|
|
if company_id:
|
|
inv_obj = self.browse(cr,uid,ids)
|
|
for line in inv_obj[0].invoice_line:
|
|
if line.account_id:
|
|
if line.account_id.company_id.id != company_id:
|
|
result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
|
|
if not result_id:
|
|
raise osv.except_osv(_('Configuration Error!'),
|
|
_('Cannot find a chart of account, you should create one from Settings\Configuration\Accounting menu.'))
|
|
inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
|
|
else:
|
|
if invoice_line:
|
|
for inv_line in invoice_line:
|
|
obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
|
|
if obj_l.company_id.id != company_id:
|
|
raise osv.except_osv(_('Configuration Error!'),
|
|
_('Invoice line account\'s company and invoice\'s compnay does not match.'))
|
|
else:
|
|
continue
|
|
if company_id and type:
|
|
if type in ('out_invoice'):
|
|
journal_type = 'sale'
|
|
elif type in ('out_refund'):
|
|
journal_type = 'sale_refund'
|
|
elif type in ('in_refund'):
|
|
journal_type = 'purchase_refund'
|
|
else:
|
|
journal_type = 'purchase'
|
|
journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
|
|
if journal_ids:
|
|
val['journal_id'] = journal_ids[0]
|
|
ir_values_obj = self.pool.get('ir.values')
|
|
res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
|
|
for r in res_journal_default:
|
|
if r[1] == 'journal_id' and r[2] in journal_ids:
|
|
val['journal_id'] = r[2]
|
|
if not val.get('journal_id', False):
|
|
raise osv.except_osv(_('Configuration Error!'), (_('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % (journal_type)))
|
|
dom = {'journal_id': [('id', 'in', journal_ids)]}
|
|
else:
|
|
journal_ids = obj_journal.search(cr, uid, [])
|
|
|
|
return {'value': val, 'domain': dom}
|
|
|
|
# go from canceled state to draft state
|
|
def action_cancel_draft(self, cr, uid, ids, *args):
|
|
self.write(cr, uid, ids, {'state':'draft'})
|
|
wf_service = netsvc.LocalService("workflow")
|
|
for inv_id in ids:
|
|
wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
|
|
wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
|
|
return True
|
|
|
|
# Workflow stuff
|
|
#################
|
|
|
|
# return the ids of the move lines which has the same account than the invoice
|
|
# whose id is in ids
|
|
def move_line_id_payment_get(self, cr, uid, ids, *args):
|
|
if not ids: return []
|
|
result = self.move_line_id_payment_gets(cr, uid, ids, *args)
|
|
return result.get(ids[0], [])
|
|
|
|
def move_line_id_payment_gets(self, cr, uid, ids, *args):
|
|
res = {}
|
|
if not ids: return res
|
|
cr.execute('SELECT i.id, l.id '\
|
|
'FROM account_move_line l '\
|
|
'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
|
|
'WHERE i.id IN %s '\
|
|
'AND l.account_id=i.account_id',
|
|
(tuple(ids),))
|
|
for r in cr.fetchall():
|
|
res.setdefault(r[0], [])
|
|
res[r[0]].append( r[1] )
|
|
return res
|
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
default = default or {}
|
|
default.update({
|
|
'state':'draft',
|
|
'number':False,
|
|
'move_id':False,
|
|
'move_name':False,
|
|
'internal_number': False,
|
|
'period_id': False,
|
|
'sent': False,
|
|
})
|
|
if 'date_invoice' not in default:
|
|
default.update({
|
|
'date_invoice':False
|
|
})
|
|
if 'date_due' not in default:
|
|
default.update({
|
|
'date_due':False
|
|
})
|
|
return super(account_invoice, self).copy(cr, uid, id, default, context)
|
|
|
|
def test_paid(self, cr, uid, ids, *args):
|
|
res = self.move_line_id_payment_get(cr, uid, ids)
|
|
if not res:
|
|
return False
|
|
ok = True
|
|
for id in res:
|
|
cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
|
|
ok = ok and bool(cr.fetchone()[0])
|
|
return ok
|
|
|
|
def button_reset_taxes(self, cr, uid, ids, context=None):
|
|
if context is None:
|
|
context = {}
|
|
ctx = context.copy()
|
|
ait_obj = self.pool.get('account.invoice.tax')
|
|
for id in ids:
|
|
cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
|
|
partner = self.browse(cr, uid, id, context=ctx).partner_id
|
|
if partner.lang:
|
|
ctx.update({'lang': partner.lang})
|
|
for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
|
|
ait_obj.create(cr, uid, taxe)
|
|
# Update the stored value (fields.function), so we write to trigger recompute
|
|
self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
|
|
return True
|
|
|
|
def button_compute(self, cr, uid, ids, context=None, set_total=False):
|
|
self.button_reset_taxes(cr, uid, ids, context)
|
|
for inv in self.browse(cr, uid, ids, context=context):
|
|
if set_total:
|
|
self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
|
|
return True
|
|
|
|
def _convert_ref(self, cr, uid, ref):
|
|
return (ref or '').replace('/','')
|
|
|
|
def _get_analytic_lines(self, cr, uid, id, context=None):
|
|
if context is None:
|
|
context = {}
|
|
inv = self.browse(cr, uid, id)
|
|
cur_obj = self.pool.get('res.currency')
|
|
|
|
company_currency = inv.company_id.currency_id.id
|
|
if inv.type in ('out_invoice', 'in_refund'):
|
|
sign = 1
|
|
else:
|
|
sign = -1
|
|
|
|
iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
|
|
for il in iml:
|
|
if il['account_analytic_id']:
|
|
if inv.type in ('in_invoice', 'in_refund'):
|
|
ref = inv.reference
|
|
else:
|
|
ref = self._convert_ref(cr, uid, inv.number)
|
|
if not inv.journal_id.analytic_journal_id:
|
|
raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
|
|
il['analytic_lines'] = [(0,0, {
|
|
'name': il['name'],
|
|
'date': inv['date_invoice'],
|
|
'account_id': il['account_analytic_id'],
|
|
'unit_amount': il['quantity'],
|
|
'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
|
|
'product_id': il['product_id'],
|
|
'product_uom_id': il['uos_id'],
|
|
'general_account_id': il['account_id'],
|
|
'journal_id': inv.journal_id.analytic_journal_id.id,
|
|
'ref': ref,
|
|
})]
|
|
return iml
|
|
|
|
def action_date_assign(self, cr, uid, ids, *args):
|
|
for inv in self.browse(cr, uid, ids):
|
|
res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
|
|
if res and res['value']:
|
|
self.write(cr, uid, [inv.id], res['value'])
|
|
return True
|
|
|
|
def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
|
|
"""finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
|
|
Hook method to be overridden in additional modules to verify and possibly alter the
|
|
move lines to be created by an invoice, for special cases.
|
|
:param invoice_browse: browsable record of the invoice that is generating the move lines
|
|
:param move_lines: list of dictionaries with the account.move.lines (as for create())
|
|
:return: the (possibly updated) final move_lines to create for this invoice
|
|
"""
|
|
return move_lines
|
|
|
|
def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
|
|
if not inv.tax_line:
|
|
for tax in compute_taxes.values():
|
|
ait_obj.create(cr, uid, tax)
|
|
else:
|
|
tax_key = []
|
|
for tax in inv.tax_line:
|
|
if tax.manual:
|
|
continue
|
|
key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id, tax.account_analytic_id.id)
|
|
tax_key.append(key)
|
|
if not key in compute_taxes:
|
|
raise osv.except_osv(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !'))
|
|
base = compute_taxes[key]['base']
|
|
if abs(base - tax.base) > inv.company_id.currency_id.rounding:
|
|
raise osv.except_osv(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.'))
|
|
for key in compute_taxes:
|
|
if not key in tax_key:
|
|
raise osv.except_osv(_('Warning!'), _('Taxes are missing!\nClick on compute button.'))
|
|
|
|
def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
|
|
total = 0
|
|
total_currency = 0
|
|
cur_obj = self.pool.get('res.currency')
|
|
for i in invoice_move_lines:
|
|
if inv.currency_id.id != company_currency:
|
|
i['currency_id'] = inv.currency_id.id
|
|
i['amount_currency'] = i['price']
|
|
i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
|
|
company_currency, i['price'],
|
|
context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
|
|
else:
|
|
i['amount_currency'] = False
|
|
i['currency_id'] = False
|
|
i['ref'] = ref
|
|
if inv.type in ('out_invoice','in_refund'):
|
|
total += i['price']
|
|
total_currency += i['amount_currency'] or i['price']
|
|
i['price'] = - i['price']
|
|
else:
|
|
total -= i['price']
|
|
total_currency -= i['amount_currency'] or i['price']
|
|
return total, total_currency, invoice_move_lines
|
|
|
|
def inv_line_characteristic_hashcode(self, invoice, invoice_line):
|
|
"""Overridable hashcode generation for invoice lines. Lines having the same hashcode
|
|
will be grouped together if the journal has the 'group line' option. Of course a module
|
|
can add fields to invoice lines that would need to be tested too before merging lines
|
|
or not."""
|
|
return "%s-%s-%s-%s-%s"%(
|
|
invoice_line['account_id'],
|
|
invoice_line.get('tax_code_id',"False"),
|
|
invoice_line.get('product_id',"False"),
|
|
invoice_line.get('analytic_account_id',"False"),
|
|
invoice_line.get('date_maturity',"False"))
|
|
|
|
def group_lines(self, cr, uid, iml, line, inv):
|
|
"""Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
|
|
if inv.journal_id.group_invoice_lines:
|
|
line2 = {}
|
|
for x, y, l in line:
|
|
tmp = self.inv_line_characteristic_hashcode(inv, l)
|
|
|
|
if tmp in line2:
|
|
am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
|
|
line2[tmp]['debit'] = (am > 0) and am or 0.0
|
|
line2[tmp]['credit'] = (am < 0) and -am or 0.0
|
|
line2[tmp]['tax_amount'] += l['tax_amount']
|
|
line2[tmp]['analytic_lines'] += l['analytic_lines']
|
|
else:
|
|
line2[tmp] = l
|
|
line = []
|
|
for key, val in line2.items():
|
|
line.append((0,0,val))
|
|
return line
|
|
|
|
def action_move_create(self, cr, uid, ids, context=None):
|
|
"""Creates invoice related analytics and financial move lines"""
|
|
ait_obj = self.pool.get('account.invoice.tax')
|
|
cur_obj = self.pool.get('res.currency')
|
|
period_obj = self.pool.get('account.period')
|
|
payment_term_obj = self.pool.get('account.payment.term')
|
|
journal_obj = self.pool.get('account.journal')
|
|
move_obj = self.pool.get('account.move')
|
|
if context is None:
|
|
context = {}
|
|
for inv in self.browse(cr, uid, ids, context=context):
|
|
if not inv.journal_id.sequence_id:
|
|
raise osv.except_osv(_('Error!'), _('Please define sequence on the journal related to this invoice.'))
|
|
if not inv.invoice_line:
|
|
raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
|
|
if inv.move_id:
|
|
continue
|
|
|
|
ctx = context.copy()
|
|
ctx.update({'lang': inv.partner_id.lang})
|
|
if not inv.date_invoice:
|
|
self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
|
|
company_currency = inv.company_id.currency_id.id
|
|
# create the analytical lines
|
|
# one move line per invoice line
|
|
iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
|
|
# check if taxes are all computed
|
|
compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
|
|
self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
|
|
|
|
# I disabled the check_total feature
|
|
#if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
|
|
# raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
|
|
|
|
if inv.payment_term:
|
|
total_fixed = total_percent = 0
|
|
for line in inv.payment_term.line_ids:
|
|
if line.value == 'fixed':
|
|
total_fixed += line.value_amount
|
|
if line.value == 'procent':
|
|
total_percent += line.value_amount
|
|
total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
|
|
if (total_fixed + total_percent) > 100:
|
|
raise osv.except_osv(_('Error!'), _("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'."))
|
|
|
|
# one move line per tax line
|
|
iml += ait_obj.move_line_get(cr, uid, inv.id)
|
|
|
|
entry_type = ''
|
|
if inv.type in ('in_invoice', 'in_refund'):
|
|
ref = inv.reference
|
|
entry_type = 'journal_pur_voucher'
|
|
if inv.type == 'in_refund':
|
|
entry_type = 'cont_voucher'
|
|
else:
|
|
ref = self._convert_ref(cr, uid, inv.number)
|
|
entry_type = 'journal_sale_vou'
|
|
if inv.type == 'out_refund':
|
|
entry_type = 'cont_voucher'
|
|
|
|
diff_currency_p = inv.currency_id.id <> company_currency
|
|
# create one move line for the total and possibly adjust the other lines amount
|
|
total = 0
|
|
total_currency = 0
|
|
total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
|
|
acc_id = inv.account_id.id
|
|
|
|
name = inv['name'] or '/'
|
|
totlines = False
|
|
if inv.payment_term:
|
|
totlines = payment_term_obj.compute(cr,
|
|
uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
|
|
if totlines:
|
|
res_amount_currency = total_currency
|
|
i = 0
|
|
ctx.update({'date': inv.date_invoice})
|
|
for t in totlines:
|
|
if inv.currency_id.id != company_currency:
|
|
amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
|
|
else:
|
|
amount_currency = False
|
|
|
|
# last line add the diff
|
|
res_amount_currency -= amount_currency or 0
|
|
i += 1
|
|
if i == len(totlines):
|
|
amount_currency += res_amount_currency
|
|
|
|
iml.append({
|
|
'type': 'dest',
|
|
'name': name,
|
|
'price': t[1],
|
|
'account_id': acc_id,
|
|
'date_maturity': t[0],
|
|
'amount_currency': diff_currency_p \
|
|
and amount_currency or False,
|
|
'currency_id': diff_currency_p \
|
|
and inv.currency_id.id or False,
|
|
'ref': ref,
|
|
})
|
|
else:
|
|
iml.append({
|
|
'type': 'dest',
|
|
'name': name,
|
|
'price': total,
|
|
'account_id': acc_id,
|
|
'date_maturity': inv.date_due or False,
|
|
'amount_currency': diff_currency_p \
|
|
and total_currency or False,
|
|
'currency_id': diff_currency_p \
|
|
and inv.currency_id.id or False,
|
|
'ref': ref
|
|
})
|
|
|
|
date = inv.date_invoice or time.strftime('%Y-%m-%d')
|
|
part = inv.partner_id.id
|
|
|
|
line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
|
|
|
|
line = self.group_lines(cr, uid, iml, line, inv)
|
|
|
|
journal_id = inv.journal_id.id
|
|
journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
|
|
if journal.centralisation:
|
|
raise osv.except_osv(_('User Error!'),
|
|
_('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.'))
|
|
|
|
line = self.finalize_invoice_move_lines(cr, uid, inv, line)
|
|
|
|
move = {
|
|
'ref': inv.reference and inv.reference or inv.name,
|
|
'line_id': line,
|
|
'journal_id': journal_id,
|
|
'date': date,
|
|
'narration':inv.comment
|
|
}
|
|
period_id = inv.period_id and inv.period_id.id or False
|
|
ctx.update({'company_id': inv.company_id.id})
|
|
if not period_id:
|
|
period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
|
|
period_id = period_ids and period_ids[0] or False
|
|
if period_id:
|
|
move['period_id'] = period_id
|
|
for i in line:
|
|
i[2]['period_id'] = period_id
|
|
|
|
move_id = move_obj.create(cr, uid, move, context=ctx)
|
|
new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
|
|
# make the invoice point to that move
|
|
self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
|
|
# Pass invoice in context in method post: used if you want to get the same
|
|
# account move reference when creating the same invoice after a cancelled one:
|
|
ctx.update({'invoice':inv})
|
|
move_obj.post(cr, uid, [move_id], context=ctx)
|
|
self._log_event(cr, uid, ids)
|
|
return True
|
|
|
|
def invoice_validate(self, cr, uid, ids, context=None):
|
|
self.write(cr, uid, ids, {'state':'open'}, context=context)
|
|
return True
|
|
|
|
def line_get_convert(self, cr, uid, x, part, date, context=None):
|
|
return {
|
|
'date_maturity': x.get('date_maturity', False),
|
|
'partner_id': part,
|
|
'name': x['name'][:64],
|
|
'date': date,
|
|
'debit': x['price']>0 and x['price'],
|
|
'credit': x['price']<0 and -x['price'],
|
|
'account_id': x['account_id'],
|
|
'analytic_lines': x.get('analytic_lines', []),
|
|
'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
|
|
'currency_id': x.get('currency_id', False),
|
|
'tax_code_id': x.get('tax_code_id', False),
|
|
'tax_amount': x.get('tax_amount', False),
|
|
'ref': x.get('ref', False),
|
|
'quantity': x.get('quantity',1.00),
|
|
'product_id': x.get('product_id', False),
|
|
'product_uom_id': x.get('uos_id', False),
|
|
'analytic_account_id': x.get('account_analytic_id', False),
|
|
}
|
|
|
|
def action_number(self, cr, uid, ids, context=None):
|
|
if context is None:
|
|
context = {}
|
|
#TODO: not correct fix but required a frech values before reading it.
|
|
self.write(cr, uid, ids, {})
|
|
|
|
for obj_inv in self.browse(cr, uid, ids, context=context):
|
|
id = obj_inv.id
|
|
invtype = obj_inv.type
|
|
number = obj_inv.number
|
|
move_id = obj_inv.move_id and obj_inv.move_id.id or False
|
|
reference = obj_inv.reference or ''
|
|
|
|
self.write(cr, uid, ids, {'internal_number':number})
|
|
|
|
if invtype in ('in_invoice', 'in_refund'):
|
|
if not reference:
|
|
ref = self._convert_ref(cr, uid, number)
|
|
else:
|
|
ref = reference
|
|
else:
|
|
ref = self._convert_ref(cr, uid, number)
|
|
|
|
cr.execute('UPDATE account_move SET ref=%s ' \
|
|
'WHERE id=%s AND (ref is null OR ref = \'\')',
|
|
(ref, move_id))
|
|
cr.execute('UPDATE account_move_line SET ref=%s ' \
|
|
'WHERE move_id=%s AND (ref is null OR ref = \'\')',
|
|
(ref, move_id))
|
|
cr.execute('UPDATE account_analytic_line SET ref=%s ' \
|
|
'FROM account_move_line ' \
|
|
'WHERE account_move_line.move_id = %s ' \
|
|
'AND account_analytic_line.move_id = account_move_line.id',
|
|
(ref, move_id))
|
|
|
|
for inv_id, name in self.name_get(cr, uid, [id]):
|
|
ctx = context.copy()
|
|
if obj_inv.type in ('out_invoice', 'out_refund'):
|
|
ctx = self.get_log_context(cr, uid, context=ctx)
|
|
message = _("Invoice '%s' is validated.") % name
|
|
self.message_append_note(cr, uid, [inv_id], body=message, context=context)
|
|
return True
|
|
|
|
def action_cancel(self, cr, uid, ids, *args):
|
|
context = {} # TODO: Use context from arguments
|
|
account_move_obj = self.pool.get('account.move')
|
|
invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
|
|
move_ids = [] # ones that we will need to remove
|
|
for i in invoices:
|
|
if i['move_id']:
|
|
move_ids.append(i['move_id'][0])
|
|
if i['payment_ids']:
|
|
account_move_line_obj = self.pool.get('account.move.line')
|
|
pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
|
|
for move_line in pay_ids:
|
|
if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
|
|
raise osv.except_osv(_('Error!'), _('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.'))
|
|
|
|
# First, set the invoices as cancelled and detach the move ids
|
|
self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
|
|
if move_ids:
|
|
# second, invalidate the move(s)
|
|
account_move_obj.button_cancel(cr, uid, move_ids, context=context)
|
|
# delete the move this invoice was pointing to
|
|
# Note that the corresponding move_lines and move_reconciles
|
|
# will be automatically deleted too
|
|
account_move_obj.unlink(cr, uid, move_ids, context=context)
|
|
self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
|
|
self.invoice_cancel_send_note(cr, uid, ids, context=context)
|
|
return True
|
|
|
|
###################
|
|
|
|
def list_distinct_taxes(self, cr, uid, ids):
|
|
invoices = self.browse(cr, uid, ids)
|
|
taxes = {}
|
|
for inv in invoices:
|
|
for tax in inv.tax_line:
|
|
if not tax['name'] in taxes:
|
|
taxes[tax['name']] = {'name': tax['name']}
|
|
return taxes.values()
|
|
|
|
def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
|
|
#TODO: implement messages system
|
|
return True
|
|
|
|
def name_get(self, cr, uid, ids, context=None):
|
|
if not ids:
|
|
return []
|
|
types = {
|
|
'out_invoice': 'CI: ',
|
|
'in_invoice': 'SI: ',
|
|
'out_refund': 'OR: ',
|
|
'in_refund': 'SR: ',
|
|
}
|
|
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')]
|
|
|
|
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
|
|
if not args:
|
|
args = []
|
|
if context is None:
|
|
context = {}
|
|
ids = []
|
|
if name:
|
|
ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
|
|
if not ids:
|
|
ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
|
|
return self.name_get(cr, user, ids, context)
|
|
|
|
def _refund_cleanup_lines(self, cr, uid, lines):
|
|
for line in lines:
|
|
del line['id']
|
|
del line['invoice_id']
|
|
for field in ('company_id', 'partner_id', 'account_id', 'product_id',
|
|
'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
|
|
if line.get(field):
|
|
line[field] = line[field][0]
|
|
if 'invoice_line_tax_id' in line:
|
|
line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
|
|
return map(lambda x: (0,0,x), lines)
|
|
|
|
def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
|
|
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'])
|
|
obj_invoice_line = self.pool.get('account.invoice.line')
|
|
obj_invoice_tax = self.pool.get('account.invoice.tax')
|
|
obj_journal = self.pool.get('account.journal')
|
|
new_ids = []
|
|
for invoice in invoices:
|
|
del invoice['id']
|
|
|
|
type_dict = {
|
|
'out_invoice': 'out_refund', # Customer Invoice
|
|
'in_invoice': 'in_refund', # Supplier Invoice
|
|
'out_refund': 'out_invoice', # Customer Refund
|
|
'in_refund': 'in_invoice', # Supplier Refund
|
|
}
|
|
|
|
invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
|
|
invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
|
|
|
|
tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
|
|
tax_lines = filter(lambda l: l['manual'], tax_lines)
|
|
tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
|
|
if journal_id:
|
|
refund_journal_ids = [journal_id]
|
|
elif invoice['type'] == 'in_invoice':
|
|
refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
|
|
else:
|
|
refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
|
|
|
|
if not date:
|
|
date = time.strftime('%Y-%m-%d')
|
|
invoice.update({
|
|
'type': type_dict[invoice['type']],
|
|
'date_invoice': date,
|
|
'state': 'draft',
|
|
'number': False,
|
|
'invoice_line': invoice_lines,
|
|
'tax_line': tax_lines,
|
|
'journal_id': refund_journal_ids
|
|
})
|
|
if period_id:
|
|
invoice.update({
|
|
'period_id': period_id,
|
|
})
|
|
if description:
|
|
invoice.update({
|
|
'name': description,
|
|
})
|
|
# take the id part of the tuple returned for many2one fields
|
|
for field in ('partner_id',
|
|
'account_id', 'currency_id', 'payment_term', 'journal_id'):
|
|
invoice[field] = invoice[field] and invoice[field][0]
|
|
# create the new invoice
|
|
new_ids.append(self.create(cr, uid, invoice))
|
|
|
|
return new_ids
|
|
|
|
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=''):
|
|
if context is None:
|
|
context = {}
|
|
#TODO check if we can use different period for payment and the writeoff line
|
|
assert len(ids)==1, "Can only pay one invoice at a time."
|
|
invoice = self.browse(cr, uid, ids[0], context=context)
|
|
src_account_id = invoice.account_id.id
|
|
# Take the seq as name for move
|
|
types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
|
|
direction = types[invoice.type]
|
|
#take the choosen date
|
|
if 'date_p' in context and context['date_p']:
|
|
date=context['date_p']
|
|
else:
|
|
date=time.strftime('%Y-%m-%d')
|
|
|
|
# Take the amount in currency and the currency of the payment
|
|
if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
|
|
amount_currency = context['amount_currency']
|
|
currency_id = context['currency_id']
|
|
else:
|
|
amount_currency = False
|
|
currency_id = False
|
|
|
|
pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
|
|
if invoice.type in ('in_invoice', 'out_invoice'):
|
|
if pay_journal['type'] == 'bank':
|
|
entry_type = 'bank_pay_voucher' # Bank payment
|
|
else:
|
|
entry_type = 'pay_voucher' # Cash payment
|
|
else:
|
|
entry_type = 'cont_voucher'
|
|
if invoice.type in ('in_invoice', 'in_refund'):
|
|
ref = invoice.reference
|
|
else:
|
|
ref = self._convert_ref(cr, uid, invoice.number)
|
|
# Pay attention to the sign for both debit/credit AND amount_currency
|
|
l1 = {
|
|
'debit': direction * pay_amount>0 and direction * pay_amount,
|
|
'credit': direction * pay_amount<0 and - direction * pay_amount,
|
|
'account_id': src_account_id,
|
|
'partner_id': invoice.partner_id.id,
|
|
'ref':ref,
|
|
'date': date,
|
|
'currency_id':currency_id,
|
|
'amount_currency':amount_currency and direction * amount_currency or 0.0,
|
|
'company_id': invoice.company_id.id,
|
|
}
|
|
l2 = {
|
|
'debit': direction * pay_amount<0 and - direction * pay_amount,
|
|
'credit': direction * pay_amount>0 and direction * pay_amount,
|
|
'account_id': pay_account_id,
|
|
'partner_id': invoice.partner_id.id,
|
|
'ref':ref,
|
|
'date': date,
|
|
'currency_id':currency_id,
|
|
'amount_currency':amount_currency and - direction * amount_currency or 0.0,
|
|
'company_id': invoice.company_id.id,
|
|
}
|
|
|
|
if not name:
|
|
name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
|
|
l1['name'] = name
|
|
l2['name'] = name
|
|
|
|
lines = [(0, 0, l1), (0, 0, l2)]
|
|
move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
|
|
move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
|
|
|
|
line_ids = []
|
|
total = 0.0
|
|
line = self.pool.get('account.move.line')
|
|
move_ids = [move_id,]
|
|
if invoice.move_id:
|
|
move_ids.append(invoice.move_id.id)
|
|
cr.execute('SELECT id FROM account_move_line '\
|
|
'WHERE move_id IN %s',
|
|
((move_id, invoice.move_id.id),))
|
|
lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
|
|
for l in lines+invoice.payment_ids:
|
|
if l.account_id.id == src_account_id:
|
|
line_ids.append(l.id)
|
|
total += (l.debit or 0.0) - (l.credit or 0.0)
|
|
|
|
inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
|
|
if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
|
|
self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
|
|
else:
|
|
code = invoice.currency_id.symbol
|
|
# TODO: use currency's formatting function
|
|
msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining).") % \
|
|
(name, pay_amount, code, invoice.amount_total, code, total, code)
|
|
self.message_append_note(cr, uid, [inv_id], body=msg, context=context)
|
|
self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
|
|
|
|
# Update the stored value (fields.function), so we write to trigger recompute
|
|
self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
|
|
return True
|
|
|
|
# -----------------------------------------
|
|
# OpenChatter notifications and need_action
|
|
# -----------------------------------------
|
|
|
|
def _get_document_type(self, type):
|
|
type_dict = {
|
|
'out_invoice': 'Customer invoice',
|
|
'in_invoice': 'Supplier invoice',
|
|
'out_refund': 'Customer Refund',
|
|
'in_refund': 'Supplier Refund',
|
|
}
|
|
return type_dict.get(type, 'Invoice')
|
|
|
|
def create_send_note(self, cr, uid, ids, context=None):
|
|
for obj in self.browse(cr, uid, ids, context=context):
|
|
self.message_append_note(cr, uid, [obj.id],body=_("%s <b>created</b>.") % (self._get_document_type(obj.type)), context=context)
|
|
|
|
def confirm_paid_send_note(self, cr, uid, ids, context=None):
|
|
for obj in self.browse(cr, uid, ids, context=context):
|
|
self.message_append_note(cr, uid, [obj.id], body=_("%s <b>paid</b>.") % (self._get_document_type(obj.type)), context=context)
|
|
|
|
def invoice_cancel_send_note(self, cr, uid, ids, context=None):
|
|
for obj in self.browse(cr, uid, ids, context=context):
|
|
self.message_append_note(cr, uid, [obj.id], body=_("%s <b>cancelled</b>.") % (self._get_document_type(obj.type)), context=context)
|
|
|
|
account_invoice()
|
|
|
|
class account_invoice_line(osv.osv):
|
|
|
|
def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
|
|
res = {}
|
|
tax_obj = self.pool.get('account.tax')
|
|
cur_obj = self.pool.get('res.currency')
|
|
for line in self.browse(cr, uid, ids):
|
|
price = line.price_unit * (1-(line.discount or 0.0)/100.0)
|
|
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)
|
|
res[line.id] = taxes['total']
|
|
if line.invoice_id:
|
|
cur = line.invoice_id.currency_id
|
|
res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
|
|
return res
|
|
|
|
def _price_unit_default(self, cr, uid, context=None):
|
|
if context is None:
|
|
context = {}
|
|
if context.get('check_total', False):
|
|
t = context['check_total']
|
|
for l in context.get('invoice_line', {}):
|
|
if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
|
|
tax_obj = self.pool.get('account.tax')
|
|
p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
|
|
t = t - (p * l[2].get('quantity'))
|
|
taxes = l[2].get('invoice_line_tax_id')
|
|
if len(taxes[0]) >= 3 and taxes[0][2]:
|
|
taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
|
|
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']:
|
|
t = t - tax['amount']
|
|
return t
|
|
return 0
|
|
|
|
_name = "account.invoice.line"
|
|
_description = "Invoice Line"
|
|
_columns = {
|
|
'name': fields.text('Description', required=True),
|
|
'origin': fields.char('Source', size=256, help="Reference of the document that produced this invoice."),
|
|
'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
|
|
'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
|
|
'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
|
|
'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."),
|
|
'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')),
|
|
'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
|
|
digits_compute= dp.get_precision('Account'), store=True),
|
|
'quantity': fields.float('Quantity', digits_compute= dp.get_precision('Product Unit of Measure'), required=True),
|
|
'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount')),
|
|
'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
|
|
'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
|
|
'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
|
|
'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
|
|
}
|
|
_defaults = {
|
|
'quantity': 1,
|
|
'discount': 0.0,
|
|
'price_unit': _price_unit_default,
|
|
}
|
|
|
|
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
|
|
if context is None:
|
|
context = {}
|
|
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)
|
|
if context.get('type', False):
|
|
doc = etree.XML(res['arch'])
|
|
for node in doc.xpath("//field[@name='product_id']"):
|
|
if context['type'] in ('in_invoice', 'in_refund'):
|
|
node.set('domain', "[('purchase_ok', '=', True)]")
|
|
else:
|
|
node.set('domain', "[('sale_ok', '=', True)]")
|
|
res['arch'] = etree.tostring(doc)
|
|
return res
|
|
|
|
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):
|
|
if context is None:
|
|
context = {}
|
|
company_id = company_id if company_id != None else context.get('company_id',False)
|
|
context = dict(context)
|
|
context.update({'company_id': company_id})
|
|
if not partner_id:
|
|
raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
|
|
if not product:
|
|
if type in ('in_invoice', 'in_refund'):
|
|
return {'value': {}, 'domain':{'product_uom':[]}}
|
|
else:
|
|
return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
|
|
part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
|
|
fpos_obj = self.pool.get('account.fiscal.position')
|
|
fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
|
|
|
|
if part.lang:
|
|
context.update({'lang': part.lang})
|
|
result = {}
|
|
res = self.pool.get('product.product').browse(cr, uid, product, context=context)
|
|
|
|
if type in ('out_invoice','out_refund'):
|
|
a = res.product_tmpl_id.property_account_income.id
|
|
if not a:
|
|
a = res.categ_id.property_account_income_categ.id
|
|
else:
|
|
a = res.product_tmpl_id.property_account_expense.id
|
|
if not a:
|
|
a = res.categ_id.property_account_expense_categ.id
|
|
a = fpos_obj.map_account(cr, uid, fpos, a)
|
|
if a:
|
|
result['account_id'] = a
|
|
|
|
if type in ('out_invoice', 'out_refund'):
|
|
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)
|
|
else:
|
|
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)
|
|
tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
|
|
|
|
if type in ('in_invoice', 'in_refund'):
|
|
result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
|
|
else:
|
|
result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
|
|
result['name'] = res.partner_ref
|
|
|
|
domain = {}
|
|
result['uos_id'] = res.uom_id.id or uom or False
|
|
if res.description:
|
|
result['name'] += '\n'+res.description
|
|
if result['uos_id']:
|
|
res2 = res.uom_id.category_id.id
|
|
if res2:
|
|
domain = {'uos_id':[('category_id','=',res2 )]}
|
|
|
|
res_final = {'value':result, 'domain':domain}
|
|
|
|
if not company_id or not currency_id:
|
|
return res_final
|
|
|
|
company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
|
|
currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
|
|
|
|
if company.currency_id.id != currency.id:
|
|
if type in ('in_invoice', 'in_refund'):
|
|
res_final['value']['price_unit'] = res.standard_price
|
|
new_price = res_final['value']['price_unit'] * currency.rate
|
|
res_final['value']['price_unit'] = new_price
|
|
|
|
if uom:
|
|
uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
|
|
if res.uom_id.category_id.id == uom.category_id.id:
|
|
new_price = res_final['value']['price_unit'] * uom.factor_inv
|
|
res_final['value']['price_unit'] = new_price
|
|
return res_final
|
|
|
|
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):
|
|
if context is None:
|
|
context = {}
|
|
company_id = company_id if company_id != None else context.get('company_id',False)
|
|
context = dict(context)
|
|
context.update({'company_id': company_id})
|
|
warning = {}
|
|
res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context)
|
|
if 'uos_id' in res['value']:
|
|
del res['value']['uos_id']
|
|
if not uom:
|
|
res['value']['price_unit'] = 0.0
|
|
if product and uom:
|
|
prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
|
|
prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
|
|
if prod.uom_id.category_id.id != prod_uom.category_id.id:
|
|
warning = {
|
|
'title': _('Warning!'),
|
|
'message': _('The selected unit of measure is not compatible with the unit of measure of the product.')
|
|
}
|
|
return {'value': res['value'], 'warning': warning}
|
|
return res
|
|
|
|
def move_line_get(self, cr, uid, invoice_id, context=None):
|
|
res = []
|
|
tax_obj = self.pool.get('account.tax')
|
|
cur_obj = self.pool.get('res.currency')
|
|
if context is None:
|
|
context = {}
|
|
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
|
|
company_currency = inv.company_id.currency_id.id
|
|
|
|
for line in inv.invoice_line:
|
|
mres = self.move_line_get_item(cr, uid, line, context)
|
|
if not mres:
|
|
continue
|
|
res.append(mres)
|
|
tax_code_found= False
|
|
for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
|
|
(line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
|
|
line.quantity, line.product_id,
|
|
inv.partner_id)['taxes']:
|
|
|
|
if inv.type in ('out_invoice', 'in_invoice'):
|
|
tax_code_id = tax['base_code_id']
|
|
tax_amount = line.price_subtotal * tax['base_sign']
|
|
else:
|
|
tax_code_id = tax['ref_base_code_id']
|
|
tax_amount = line.price_subtotal * tax['ref_base_sign']
|
|
|
|
if tax_code_found:
|
|
if not tax_code_id:
|
|
continue
|
|
res.append(self.move_line_get_item(cr, uid, line, context))
|
|
res[-1]['price'] = 0.0
|
|
res[-1]['account_analytic_id'] = False
|
|
elif not tax_code_id:
|
|
continue
|
|
tax_code_found = True
|
|
|
|
res[-1]['tax_code_id'] = tax_code_id
|
|
res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
|
|
return res
|
|
|
|
def move_line_get_item(self, cr, uid, line, context=None):
|
|
return {
|
|
'type':'src',
|
|
'name': line.name.split('\n')[0][:64],
|
|
'price_unit':line.price_unit,
|
|
'quantity':line.quantity,
|
|
'price':line.price_subtotal,
|
|
'account_id':line.account_id.id,
|
|
'product_id':line.product_id.id,
|
|
'uos_id':line.uos_id.id,
|
|
'account_analytic_id':line.account_analytic_id.id,
|
|
'taxes':line.invoice_line_tax_id,
|
|
}
|
|
#
|
|
# Set the tax field according to the account and the fiscal position
|
|
#
|
|
def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
|
|
if not account_id:
|
|
return {}
|
|
taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
|
|
fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
|
|
tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
|
|
|
|
product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
|
|
partner_id=partner_id, fposition_id=fposition_id)
|
|
unique_tax_ids = set(tax_ids)
|
|
if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
|
|
unique_tax_ids |= set(product_change_result['value']['invoice_line_tax_id'])
|
|
return {'value':{'invoice_line_tax_id': list(unique_tax_ids)}}
|
|
|
|
account_invoice_line()
|
|
|
|
class account_invoice_tax(osv.osv):
|
|
_name = "account.invoice.tax"
|
|
_description = "Invoice Tax"
|
|
|
|
def _count_factor(self, cr, uid, ids, name, args, context=None):
|
|
res = {}
|
|
for invoice_tax in self.browse(cr, uid, ids, context=context):
|
|
res[invoice_tax.id] = {
|
|
'factor_base': 1.0,
|
|
'factor_tax': 1.0,
|
|
}
|
|
if invoice_tax.amount <> 0.0:
|
|
factor_tax = invoice_tax.tax_amount / invoice_tax.amount
|
|
res[invoice_tax.id]['factor_tax'] = factor_tax
|
|
|
|
if invoice_tax.base <> 0.0:
|
|
factor_base = invoice_tax.base_amount / invoice_tax.base
|
|
res[invoice_tax.id]['factor_base'] = factor_base
|
|
|
|
return res
|
|
|
|
_columns = {
|
|
'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
|
|
'name': fields.char('Tax Description', size=64, required=True),
|
|
'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
|
|
'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
|
|
'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
|
|
'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
|
|
'manual': fields.boolean('Manual'),
|
|
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
|
|
'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
|
|
'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
|
|
'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
|
|
'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
|
|
'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
|
|
'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
|
|
'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
|
|
}
|
|
|
|
def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
|
|
cur_obj = self.pool.get('res.currency')
|
|
company_obj = self.pool.get('res.company')
|
|
company_currency = False
|
|
factor = 1
|
|
if ids:
|
|
factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
|
|
if company_id:
|
|
company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
|
|
if currency_id and company_currency:
|
|
base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
|
|
return {'value': {'base_amount':base}}
|
|
|
|
def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
|
|
cur_obj = self.pool.get('res.currency')
|
|
company_obj = self.pool.get('res.company')
|
|
company_currency = False
|
|
factor = 1
|
|
if ids:
|
|
factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
|
|
if company_id:
|
|
company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
|
|
if currency_id and company_currency:
|
|
amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
|
|
return {'value': {'tax_amount': amount}}
|
|
|
|
_order = 'sequence'
|
|
_defaults = {
|
|
'manual': 1,
|
|
'base_amount': 0.0,
|
|
'tax_amount': 0.0,
|
|
}
|
|
def compute(self, cr, uid, invoice_id, context=None):
|
|
tax_grouped = {}
|
|
tax_obj = self.pool.get('account.tax')
|
|
cur_obj = self.pool.get('res.currency')
|
|
inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
|
|
cur = inv.currency_id
|
|
company_currency = inv.company_id.currency_id.id
|
|
|
|
for line in inv.invoice_line:
|
|
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']:
|
|
tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
|
|
val={}
|
|
val['invoice_id'] = inv.id
|
|
val['name'] = tax['name']
|
|
val['amount'] = tax['amount']
|
|
val['manual'] = False
|
|
val['sequence'] = tax['sequence']
|
|
val['base'] = tax['price_unit'] * line['quantity']
|
|
|
|
if inv.type in ('out_invoice','in_invoice'):
|
|
val['base_code_id'] = tax['base_code_id']
|
|
val['tax_code_id'] = tax['tax_code_id']
|
|
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)
|
|
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)
|
|
val['account_id'] = tax['account_collected_id'] or line.account_id.id
|
|
val['account_analytic_id'] = tax['account_analytic_collected_id']
|
|
else:
|
|
val['base_code_id'] = tax['ref_base_code_id']
|
|
val['tax_code_id'] = tax['ref_tax_code_id']
|
|
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)
|
|
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)
|
|
val['account_id'] = tax['account_paid_id'] or line.account_id.id
|
|
val['account_analytic_id'] = tax['account_analytic_paid_id']
|
|
|
|
key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
|
|
if not key in tax_grouped:
|
|
tax_grouped[key] = val
|
|
else:
|
|
tax_grouped[key]['amount'] += val['amount']
|
|
tax_grouped[key]['base'] += val['base']
|
|
tax_grouped[key]['base_amount'] += val['base_amount']
|
|
tax_grouped[key]['tax_amount'] += val['tax_amount']
|
|
|
|
for t in tax_grouped.values():
|
|
t['base'] = cur_obj.round(cr, uid, cur, t['base'])
|
|
t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
|
|
t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
|
|
t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
|
|
return tax_grouped
|
|
|
|
def move_line_get(self, cr, uid, invoice_id):
|
|
res = []
|
|
cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
|
|
for t in cr.dictfetchall():
|
|
if not t['amount'] \
|
|
and not t['tax_code_id'] \
|
|
and not t['tax_amount']:
|
|
continue
|
|
res.append({
|
|
'type':'tax',
|
|
'name':t['name'],
|
|
'price_unit': t['amount'],
|
|
'quantity': 1,
|
|
'price': t['amount'] or 0.0,
|
|
'account_id': t['account_id'],
|
|
'tax_code_id': t['tax_code_id'],
|
|
'tax_amount': t['tax_amount'],
|
|
'account_analytic_id': t['account_analytic_id'],
|
|
})
|
|
return res
|
|
|
|
account_invoice_tax()
|
|
|
|
|
|
class res_partner(osv.osv):
|
|
""" Inherits partner and adds invoice information in the partner form """
|
|
_inherit = 'res.partner'
|
|
_columns = {
|
|
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
|
|
}
|
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
default = default or {}
|
|
default.update({'invoice_ids' : []})
|
|
return super(res_partner, self).copy(cr, uid, id, default, context)
|
|
|
|
res_partner()
|
|
|
|
class mail_message(osv.osv):
|
|
_name = 'mail.message'
|
|
_inherit = 'mail.message'
|
|
|
|
def _postprocess_sent_message(self, cr, uid, message, context=None):
|
|
if message.model == 'account.invoice':
|
|
self.pool.get('account.invoice').write(cr, uid, [message.res_id], {'sent':True}, context=context)
|
|
return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
|
|
|
|
mail_message()
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|