387 lines
13 KiB
Python
387 lines
13 KiB
Python
# Copyright 2018 Creu Blanca
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
try:
|
|
import numpy
|
|
except (ImportError, IOError) as err:
|
|
_logger.error(err)
|
|
|
|
|
|
class AccountLoanLine(models.Model):
|
|
_name = 'account.loan.line'
|
|
_description = 'Annuity'
|
|
_order = 'sequence asc'
|
|
|
|
name = fields.Char(compute='_compute_name')
|
|
loan_id = fields.Many2one(
|
|
'account.loan',
|
|
required=True,
|
|
readonly=True,
|
|
ondelete='cascade',
|
|
)
|
|
is_leasing = fields.Boolean(related='loan_id.is_leasing', readonly=True, )
|
|
loan_type = fields.Selection([
|
|
('fixed-annuity', 'Fixed Annuity'),
|
|
('fixed-principal', 'Fixed Principal'),
|
|
('interest', 'Only interest'),
|
|
], related='loan_id.loan_type', readonly=True,
|
|
)
|
|
loan_state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('posted', 'Posted'),
|
|
('cancelled', 'Cancelled'),
|
|
('closed', 'Closed'),
|
|
], related='loan_id.state', readonly=True, store=True)
|
|
sequence = fields.Integer(required=True, readonly=True)
|
|
date = fields.Date(
|
|
required=True,
|
|
readonly=True,
|
|
help='Date when the payment will be accounted',
|
|
)
|
|
long_term_loan_account_id = fields.Many2one(
|
|
'account.account',
|
|
readony=True,
|
|
related='loan_id.long_term_loan_account_id',
|
|
)
|
|
currency_id = fields.Many2one(
|
|
'res.currency',
|
|
related='loan_id.currency_id',
|
|
)
|
|
rate = fields.Float(
|
|
required=True,
|
|
readonly=True,
|
|
digits=(8, 6),
|
|
)
|
|
pending_principal_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
help='Pending amount of the loan before the payment',
|
|
)
|
|
long_term_pending_principal_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
help='Pending amount of the loan before the payment that will not be '
|
|
'payed in, at least, 12 months',
|
|
)
|
|
payment_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
help='Total amount that will be payed (Annuity)',
|
|
)
|
|
interests_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
help='Amount of the payment that will be assigned to interests',
|
|
)
|
|
principal_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
compute='_compute_amounts',
|
|
help='Amount of the payment that will reduce the pending loan amount',
|
|
)
|
|
long_term_principal_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
readonly=True,
|
|
help='Amount that will reduce the pending loan amount on long term',
|
|
)
|
|
final_pending_principal_amount = fields.Monetary(
|
|
currency_field='currency_id',
|
|
compute='_compute_amounts',
|
|
help='Pending amount of the loan after the payment',
|
|
)
|
|
move_ids = fields.One2many(
|
|
'account.move',
|
|
inverse_name='loan_line_id',
|
|
)
|
|
has_moves = fields.Boolean(
|
|
compute='_compute_has_moves'
|
|
)
|
|
invoice_ids = fields.One2many(
|
|
'account.invoice',
|
|
inverse_name='loan_line_id',
|
|
)
|
|
has_invoices = fields.Boolean(
|
|
compute='_compute_has_invoices'
|
|
)
|
|
_sql_constraints = [
|
|
('sequence_loan',
|
|
'unique(loan_id, sequence)',
|
|
'Sequence must be unique in a loan')
|
|
]
|
|
|
|
@api.depends('move_ids')
|
|
def _compute_has_moves(self):
|
|
for record in self:
|
|
record.has_moves = bool(record.move_ids)
|
|
|
|
@api.depends('invoice_ids')
|
|
def _compute_has_invoices(self):
|
|
for record in self:
|
|
record.has_invoices = bool(record.invoice_ids)
|
|
|
|
@api.depends('loan_id.name', 'sequence')
|
|
def _compute_name(self):
|
|
for record in self:
|
|
record.name = '%s-%d' % (record.loan_id.name, record.sequence)
|
|
|
|
@api.depends('payment_amount', 'interests_amount',
|
|
'pending_principal_amount')
|
|
def _compute_amounts(self):
|
|
for rec in self:
|
|
rec.final_pending_principal_amount = (
|
|
rec.pending_principal_amount - rec.payment_amount +
|
|
rec.interests_amount
|
|
)
|
|
rec.principal_amount = rec.payment_amount - rec.interests_amount
|
|
|
|
def compute_amount(self):
|
|
"""
|
|
Computes the payment amount
|
|
:return: Amount to be payed on the annuity
|
|
"""
|
|
if self.sequence == self.loan_id.periods:
|
|
return (self.pending_principal_amount + self.interests_amount -
|
|
self.loan_id.residual_amount)
|
|
if self.loan_type == 'fixed-principal' and self.loan_id.round_on_end:
|
|
return self.loan_id.fixed_amount + self.interests_amount
|
|
if self.loan_type == 'fixed-principal':
|
|
return (
|
|
self.pending_principal_amount -
|
|
self.loan_id.residual_amount
|
|
) / (
|
|
self.loan_id.periods - self.sequence + 1
|
|
) + self.interests_amount
|
|
if self.loan_type == 'interest':
|
|
return self.interests_amount
|
|
if self.loan_type == 'fixed-annuity' and self.loan_id.round_on_end:
|
|
return self.loan_id.fixed_amount
|
|
if self.loan_type == 'fixed-annuity':
|
|
return self.currency_id.round(- numpy.pmt(
|
|
self.rate / 100,
|
|
self.loan_id.periods - self.sequence + 1,
|
|
self.pending_principal_amount,
|
|
-self.loan_id.residual_amount
|
|
))
|
|
|
|
def check_amount(self):
|
|
"""Recompute amounts if the annuity has not been processed"""
|
|
if self.move_ids or self.invoice_ids:
|
|
raise UserError(_(
|
|
'Amount cannot be recomputed if moves or invoices exists '
|
|
'already'
|
|
))
|
|
if not self.loan_id.round_on_end:
|
|
self.interests_amount = self.currency_id.round(
|
|
self.pending_principal_amount * self.rate / 100)
|
|
self.payment_amount = self.currency_id.round(self.compute_amount())
|
|
else:
|
|
self.interests_amount = (
|
|
self.pending_principal_amount * self.rate / 100)
|
|
self.payment_amount = self.compute_amount()
|
|
|
|
@api.multi
|
|
def check_move_amount(self):
|
|
"""
|
|
Changes the amounts of the annuity once the move is posted
|
|
:return:
|
|
"""
|
|
self.ensure_one()
|
|
interests_moves = self.move_ids.mapped('line_ids').filtered(
|
|
lambda r: r.account_id == self.loan_id.interest_expenses_account_id
|
|
)
|
|
short_term_moves = self.move_ids.mapped('line_ids').filtered(
|
|
lambda r: r.account_id == self.loan_id.short_term_loan_account_id
|
|
)
|
|
long_term_moves = self.move_ids.mapped('line_ids').filtered(
|
|
lambda r: r.account_id == self.loan_id.long_term_loan_account_id
|
|
)
|
|
self.interests_amount = (
|
|
sum(interests_moves.mapped('debit')) -
|
|
sum(interests_moves.mapped('credit'))
|
|
)
|
|
self.long_term_principal_amount = (
|
|
sum(long_term_moves.mapped('debit')) -
|
|
sum(long_term_moves.mapped('credit'))
|
|
)
|
|
self.payment_amount = (
|
|
sum(short_term_moves.mapped('debit')) -
|
|
sum(short_term_moves.mapped('credit')) +
|
|
self.long_term_principal_amount +
|
|
self.interests_amount
|
|
)
|
|
|
|
def move_vals(self):
|
|
return {
|
|
'loan_line_id': self.id,
|
|
'loan_id': self.loan_id.id,
|
|
'date': self.date,
|
|
'ref': self.name,
|
|
'journal_id': self.loan_id.journal_id.id,
|
|
'line_ids': [(0, 0, vals) for vals in self.move_line_vals()]
|
|
}
|
|
|
|
def move_line_vals(self):
|
|
vals = []
|
|
partner = self.loan_id.partner_id.with_context(
|
|
force_company=self.loan_id.company_id.id)
|
|
vals.append({
|
|
'account_id': partner.property_account_payable_id.id,
|
|
'partner_id': partner.id,
|
|
'credit': self.payment_amount,
|
|
'debit': 0,
|
|
})
|
|
vals.append({
|
|
'account_id': self.loan_id.interest_expenses_account_id.id,
|
|
'credit': 0,
|
|
'debit': self.interests_amount,
|
|
})
|
|
vals.append({
|
|
'account_id': self.loan_id.short_term_loan_account_id.id,
|
|
'credit': 0,
|
|
'debit': self.payment_amount - self.interests_amount,
|
|
})
|
|
if self.long_term_loan_account_id and self.long_term_principal_amount:
|
|
vals.append({
|
|
'account_id': self.loan_id.short_term_loan_account_id.id,
|
|
'credit': self.long_term_principal_amount,
|
|
'debit': 0,
|
|
})
|
|
vals.append({
|
|
'account_id': self.long_term_loan_account_id.id,
|
|
'credit': 0,
|
|
'debit': self.long_term_principal_amount,
|
|
})
|
|
return vals
|
|
|
|
def invoice_vals(self):
|
|
partner = self.loan_id.partner_id.with_context(
|
|
force_company=self.loan_id.company_id.id)
|
|
return {
|
|
'loan_line_id': self.id,
|
|
'loan_id': self.loan_id.id,
|
|
'type': 'in_invoice',
|
|
'partner_id': self.loan_id.partner_id.id,
|
|
'date_invoice': self.date,
|
|
'account_id': partner.property_account_payable_id.id,
|
|
'journal_id': self.loan_id.journal_id.id,
|
|
'company_id': self.loan_id.company_id.id,
|
|
'invoice_line_ids': [(0, 0, vals) for vals in
|
|
self.invoice_line_vals()]
|
|
}
|
|
|
|
def invoice_line_vals(self):
|
|
vals = list()
|
|
vals.append({
|
|
'product_id': self.loan_id.product_id.id,
|
|
'name': self.loan_id.product_id.name,
|
|
'quantity': 1,
|
|
'price_unit': self.principal_amount,
|
|
'account_id': self.loan_id.short_term_loan_account_id.id,
|
|
})
|
|
vals.append({
|
|
'product_id': self.loan_id.interests_product_id.id,
|
|
'name': self.loan_id.interests_product_id.name,
|
|
'quantity': 1,
|
|
'price_unit': self.interests_amount,
|
|
'account_id': self.loan_id.interest_expenses_account_id.id,
|
|
})
|
|
return vals
|
|
|
|
@api.multi
|
|
def generate_move(self):
|
|
"""
|
|
Computes and post the moves of loans
|
|
:return: list of account.move generated
|
|
"""
|
|
res = []
|
|
for record in self:
|
|
if not record.move_ids:
|
|
if record.loan_id.line_ids.filtered(
|
|
lambda r: r.date < record.date and not r.move_ids
|
|
):
|
|
raise UserError(_('Some moves must be created first'))
|
|
move = self.env['account.move'].create(record.move_vals())
|
|
move.post()
|
|
res.append(move.id)
|
|
return res
|
|
|
|
@api.multi
|
|
def generate_invoice(self):
|
|
"""
|
|
Computes invoices of leases
|
|
:return: list of account.invoice generated
|
|
"""
|
|
res = []
|
|
for record in self:
|
|
if not record.invoice_ids:
|
|
if record.loan_id.line_ids.filtered(
|
|
lambda r: r.date < record.date and not r.invoice_ids
|
|
):
|
|
raise UserError(_('Some invoices must be created first'))
|
|
invoice = self.env['account.invoice'].create(
|
|
record.invoice_vals())
|
|
res.append(invoice.id)
|
|
for line in invoice.invoice_line_ids:
|
|
line._set_taxes()
|
|
invoice.compute_taxes()
|
|
if record.loan_id.post_invoice:
|
|
invoice.action_invoice_open()
|
|
return res
|
|
|
|
@api.multi
|
|
def view_account_values(self):
|
|
"""Shows the invoice if it is a leasing or the move if it is a loan"""
|
|
self.ensure_one()
|
|
if self.is_leasing:
|
|
return self.view_account_invoices()
|
|
return self.view_account_moves()
|
|
|
|
@api.multi
|
|
def view_process_values(self):
|
|
"""Computes the annuity and returns the result"""
|
|
self.ensure_one()
|
|
if self.is_leasing:
|
|
self.generate_invoice()
|
|
else:
|
|
self.generate_move()
|
|
return self.view_account_values()
|
|
|
|
@api.multi
|
|
def view_account_moves(self):
|
|
self.ensure_one()
|
|
action = self.env.ref('account.action_move_line_form')
|
|
result = action.read()[0]
|
|
result['context'] = {
|
|
'default_loan_line_id': self.id,
|
|
'default_loan_id': self.loan_id.id
|
|
}
|
|
result['domain'] = [('loan_line_id', '=', self.id)]
|
|
if len(self.move_ids) == 1:
|
|
res = self.env.ref('account.move.form', False)
|
|
result['views'] = [(res and res.id or False, 'form')]
|
|
result['res_id'] = self.move_ids.id
|
|
return result
|
|
|
|
@api.multi
|
|
def view_account_invoices(self):
|
|
self.ensure_one()
|
|
action = self.env.ref('account.action_invoice_tree2')
|
|
result = action.read()[0]
|
|
result['context'] = {
|
|
'default_loan_line_id': self.id,
|
|
'default_loan_id': self.loan_id.id
|
|
}
|
|
result['domain'] = [
|
|
('loan_line_id', '=', self.id),
|
|
('type', '=', 'in_invoice')
|
|
]
|
|
if len(self.invoice_ids) == 1:
|
|
res = self.env.ref('account.invoice.supplier.form', False)
|
|
result['views'] = [(res and res.id or False, 'form')]
|
|
result['res_id'] = self.invoice_ids.id
|
|
return result
|