482 lines
17 KiB
Python
482 lines
17 KiB
Python
# Copyright 2018 Creu Blanca
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
import logging
|
|
|
|
from odoo import Command, _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
try:
|
|
import numpy_financial
|
|
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",
|
|
)
|
|
company_id = fields.Many2one(
|
|
"res.company", readonly=True, related="loan_id.company_id"
|
|
)
|
|
partner_id = fields.Many2one(
|
|
"res.partner", readonly=True, related="loan_id.partner_id"
|
|
)
|
|
is_leasing = fields.Boolean(
|
|
related="loan_id.is_leasing",
|
|
readonly=True,
|
|
)
|
|
journal_id = fields.Many2one(
|
|
"account.journal",
|
|
readonly=True,
|
|
related="loan_id.journal_id",
|
|
)
|
|
short_term_loan_account_id = fields.Many2one(
|
|
"account.account",
|
|
readonly=True,
|
|
related="loan_id.short_term_loan_account_id",
|
|
)
|
|
interest_expenses_account_id = fields.Many2one(
|
|
"account.account",
|
|
readonly=True,
|
|
related="loan_id.interest_expenses_account_id",
|
|
)
|
|
loan_type = fields.Selection(
|
|
related="loan_id.loan_type",
|
|
readonly=True,
|
|
)
|
|
loan_state = fields.Selection(
|
|
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",
|
|
readonly=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")
|
|
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("move_ids")
|
|
def _compute_has_invoices(self):
|
|
for record in self:
|
|
record.has_invoices = bool(record.move_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_financial.pmt(
|
|
self.loan_id._loan_rate() / 100,
|
|
self.loan_id.periods - self.sequence + 1,
|
|
self.pending_principal_amount,
|
|
-self.loan_id.residual_amount,
|
|
)
|
|
)
|
|
if self.loan_type == "fixed-annuity-begin" and self.loan_id.round_on_end:
|
|
return self.loan_id.fixed_amount
|
|
if self.loan_type == "fixed-annuity-begin":
|
|
return self.currency_id.round(
|
|
-numpy_financial.pmt(
|
|
self.loan_id._loan_rate() / 100,
|
|
self.loan_id.periods - self.sequence + 1,
|
|
self.pending_principal_amount,
|
|
-self.loan_id.residual_amount,
|
|
when="begin",
|
|
)
|
|
)
|
|
|
|
def _check_amount(self):
|
|
"""Recompute amounts if the annuity has not been processed"""
|
|
if self.move_ids:
|
|
raise UserError(
|
|
_("Amount cannot be recomputed if moves or invoices exists " "already")
|
|
)
|
|
if (
|
|
self.sequence == self.loan_id.periods
|
|
and self.loan_id.round_on_end
|
|
and self.loan_type in ["fixed-annuity", "fixed-annuity-begin"]
|
|
):
|
|
self.interests_amount = self.currency_id.round(
|
|
self.loan_id.fixed_amount
|
|
- self.pending_principal_amount
|
|
+ self.loan_id.residual_amount
|
|
)
|
|
self.payment_amount = self.currency_id.round(self._compute_amount())
|
|
elif not self.loan_id.round_on_end:
|
|
self.interests_amount = self.currency_id.round(self._compute_interest())
|
|
self.payment_amount = self.currency_id.round(self._compute_amount())
|
|
else:
|
|
self.interests_amount = self._compute_interest()
|
|
self.payment_amount = self._compute_amount()
|
|
|
|
def _compute_interest(self):
|
|
if self.loan_type == "fixed-annuity-begin":
|
|
return -numpy_financial.ipmt(
|
|
self.loan_id._loan_rate() / 100,
|
|
2,
|
|
self.loan_id.periods - self.sequence + 1,
|
|
self.pending_principal_amount,
|
|
-self.loan_id.residual_amount,
|
|
when="begin",
|
|
)
|
|
return self.pending_principal_amount * self.loan_id._loan_rate() / 100
|
|
|
|
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, journal=False, account=False):
|
|
return {
|
|
"loan_line_id": self.id,
|
|
"loan_id": self.loan_id.id,
|
|
"date": self.date,
|
|
"ref": self.name,
|
|
"journal_id": (journal and journal.id) or self.loan_id.journal_id.id,
|
|
"line_ids": [
|
|
Command.create(vals) for vals in self._move_line_vals(account=account)
|
|
],
|
|
}
|
|
|
|
def _move_line_vals(self, account=False):
|
|
vals = []
|
|
partner = self.loan_id.partner_id.with_company(self.loan_id.company_id)
|
|
vals.append(
|
|
{
|
|
"account_id": (account and account.id)
|
|
or partner.property_account_payable_id.id,
|
|
"partner_id": partner.id,
|
|
"credit": self.payment_amount,
|
|
"debit": 0,
|
|
}
|
|
)
|
|
if self.interests_amount:
|
|
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):
|
|
return {
|
|
"loan_line_id": self.id,
|
|
"loan_id": self.loan_id.id,
|
|
"move_type": "in_invoice",
|
|
"partner_id": self.loan_id.partner_id.id,
|
|
"invoice_date": self.date,
|
|
"journal_id": self.loan_id.journal_id.id,
|
|
"company_id": self.loan_id.company_id.id,
|
|
"invoice_line_ids": [
|
|
Command.create(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
|
|
|
|
def _generate_move(self, journal=False, account=False):
|
|
"""
|
|
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(journal=journal, account=account)
|
|
)
|
|
move.action_post()
|
|
res.append(move.id)
|
|
return res
|
|
|
|
def _long_term_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": [
|
|
Command.create(vals) for vals in self._get_long_term_move_line_vals()
|
|
],
|
|
}
|
|
|
|
def _generate_invoice(self):
|
|
"""
|
|
Computes invoices of leases
|
|
: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 invoices must be created first"))
|
|
invoice = self.env["account.move"].create(record._invoice_vals())
|
|
res.append(invoice.id)
|
|
for line in invoice.invoice_line_ids:
|
|
line.tax_ids = line._get_computed_taxes()
|
|
invoice.flush_recordset()
|
|
invoice.filtered(
|
|
lambda m: m.currency_id.round(m.amount_total) < 0
|
|
).action_switch_invoice_into_refund_credit_note()
|
|
if record.loan_id.post_invoice:
|
|
invoice.action_post()
|
|
if (
|
|
record.long_term_loan_account_id
|
|
and record.long_term_principal_amount != 0
|
|
):
|
|
move = self.env["account.move"].create(
|
|
record._long_term_move_vals()
|
|
)
|
|
if record.loan_id.post_invoice:
|
|
move.action_post()
|
|
res.append(move.id)
|
|
return res
|
|
|
|
def _get_long_term_move_line_vals(self):
|
|
return [
|
|
{
|
|
"account_id": self.loan_id.short_term_loan_account_id.id,
|
|
"credit": self.long_term_principal_amount,
|
|
"debit": 0,
|
|
},
|
|
{
|
|
"account_id": self.long_term_loan_account_id.id,
|
|
"credit": 0,
|
|
"debit": self.long_term_principal_amount,
|
|
},
|
|
]
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
def view_account_moves(self):
|
|
self.ensure_one()
|
|
result = self.env["ir.actions.act_window"]._for_xml_id(
|
|
"account.action_move_line_form"
|
|
)
|
|
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.view_move_form", False)
|
|
result["views"] = [(res and res.id or False, "form")]
|
|
result["res_id"] = self.move_ids.id
|
|
return result
|
|
|
|
def view_account_invoices(self):
|
|
self.ensure_one()
|
|
result = self.env["ir.actions.act_window"]._for_xml_id(
|
|
"account.action_move_out_invoice_type"
|
|
)
|
|
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.view_move_form", False)
|
|
result["views"] = [(res and res.id or False, "form")]
|
|
result["res_id"] = self.move_ids.id
|
|
return result
|