2
0
account-financial-tools/account_loan/models/account_loan.py
2023-09-28 16:47:31 +02:00

487 lines
16 KiB
Python

# Copyright 2018 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
import math
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
try:
import numpy_financial
except (ImportError, IOError) as err:
_logger.debug(err)
class AccountLoan(models.Model):
_name = "account.loan"
_description = "Loan"
_inherit = ["mail.thread", "mail.activity.mixin"]
def _default_company(self):
return self.env.company
name = fields.Char(
copy=False,
required=True,
readonly=True,
default="/",
states={"draft": [("readonly", False)]},
)
partner_id = fields.Many2one(
"res.partner",
required=True,
string="Lender",
help="Company or individual that lends the money at an interest rate.",
readonly=True,
states={"draft": [("readonly", False)]},
)
company_id = fields.Many2one(
"res.company",
required=True,
default=_default_company,
readonly=True,
states={"draft": [("readonly", False)]},
)
state = fields.Selection(
[
("draft", "Draft"),
("posted", "Posted"),
("cancelled", "Cancelled"),
("closed", "Closed"),
],
required=True,
copy=False,
default="draft",
)
line_ids = fields.One2many(
"account.loan.line",
readonly=True,
inverse_name="loan_id",
copy=False,
)
periods = fields.Integer(
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Number of periods that the loan will last",
)
method_period = fields.Integer(
string="Period Length",
default=1,
help="State here the time between 2 depreciations, in months",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
start_date = fields.Date(
help="Start of the moves",
readonly=True,
states={"draft": [("readonly", False)]},
copy=False,
)
rate = fields.Float(
required=True,
default=0.0,
digits=(8, 6),
help="Currently applied rate",
tracking=True,
)
rate_period = fields.Float(
compute="_compute_rate_period",
digits=(8, 6),
help="Real rate that will be applied on each period",
)
rate_type = fields.Selection(
[("napr", "Nominal APR"), ("ear", "EAR"), ("real", "Real rate")],
required=True,
help="Method of computation of the applied rate",
default="napr",
readonly=True,
states={"draft": [("readonly", False)]},
)
loan_type = fields.Selection(
[
("fixed-annuity", "Fixed Annuity"),
("fixed-annuity-begin", "Fixed Annuity Begin"),
("fixed-principal", "Fixed Principal"),
("interest", "Only interest"),
],
required=True,
help="Method of computation of the period annuity",
readonly=True,
states={"draft": [("readonly", False)]},
default="fixed-annuity",
)
fixed_amount = fields.Monetary(
currency_field="currency_id",
compute="_compute_fixed_amount",
)
fixed_loan_amount = fields.Monetary(
currency_field="currency_id",
readonly=True,
copy=False,
default=0,
)
fixed_periods = fields.Integer(
readonly=True,
copy=False,
default=0,
)
loan_amount = fields.Monetary(
currency_field="currency_id",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
residual_amount = fields.Monetary(
currency_field="currency_id",
default=0.0,
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Residual amount of the lease that must be payed on the end in "
"order to acquire the asset",
)
round_on_end = fields.Boolean(
default=False,
help="When checked, the differences will be applied on the last period"
", if it is unchecked, the annuity will be recalculated on each "
"period.",
readonly=True,
states={"draft": [("readonly", False)]},
)
payment_on_first_period = fields.Boolean(
default=False,
readonly=True,
states={"draft": [("readonly", False)]},
help="When checked, the first payment will be on start date",
)
currency_id = fields.Many2one(
"res.currency",
compute="_compute_currency",
readonly=True,
)
journal_type = fields.Char(compute="_compute_journal_type")
journal_id = fields.Many2one(
"account.journal",
domain="[('company_id', '=', company_id),('type', '=', journal_type)]",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
short_term_loan_account_id = fields.Many2one(
"account.account",
domain="[('company_id', '=', company_id)]",
string="Short term account",
help="Account that will contain the pending amount on short term",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
long_term_loan_account_id = fields.Many2one(
"account.account",
string="Long term account",
help="Account that will contain the pending amount on Long term",
domain="[('company_id', '=', company_id)]",
readonly=True,
states={"draft": [("readonly", False)]},
)
interest_expenses_account_id = fields.Many2one(
"account.account",
domain="[('company_id', '=', company_id)]",
string="Interests account",
help="Account where the interests will be assigned to",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
)
is_leasing = fields.Boolean(
default=False,
readonly=True,
states={"draft": [("readonly", False)]},
)
leased_asset_account_id = fields.Many2one(
"account.account",
domain="[('company_id', '=', company_id)]",
readonly=True,
states={"draft": [("readonly", False)]},
)
product_id = fields.Many2one(
"product.product",
string="Loan product",
help="Product where the amount of the loan will be assigned when the "
"invoice is created",
)
interests_product_id = fields.Many2one(
"product.product",
string="Interest product",
help="Product where the amount of interests will be assigned when the "
"invoice is created",
)
move_ids = fields.One2many("account.move", copy=False, inverse_name="loan_id")
pending_principal_amount = fields.Monetary(
currency_field="currency_id",
compute="_compute_total_amounts",
)
payment_amount = fields.Monetary(
currency_field="currency_id",
string="Total payed amount",
compute="_compute_total_amounts",
)
interests_amount = fields.Monetary(
currency_field="currency_id",
string="Total interests payed",
compute="_compute_total_amounts",
)
post_invoice = fields.Boolean(
default=True, help="Invoices will be posted automatically"
)
_sql_constraints = [
("name_uniq", "unique(name, company_id)", "Loan name must be unique"),
]
@api.depends("line_ids", "currency_id", "loan_amount")
def _compute_total_amounts(self):
for record in self:
lines = record.line_ids.filtered(lambda r: r.move_ids)
record.payment_amount = sum(lines.mapped("payment_amount")) or 0.0
record.interests_amount = sum(lines.mapped("interests_amount")) or 0.0
record.pending_principal_amount = (
record.loan_amount - record.payment_amount + record.interests_amount
)
@api.depends("rate_period", "fixed_loan_amount", "fixed_periods", "currency_id")
def _compute_fixed_amount(self):
"""
Computes the fixed amount in order to be used if round_on_end is
checked. On fix-annuity interests are included and on fixed-principal
and interests it isn't.
:return:
"""
for record in self:
if record.loan_type == "fixed-annuity":
record.fixed_amount = -record.currency_id.round(
numpy_financial.pmt(
record._loan_rate() / 100,
record.fixed_periods,
record.fixed_loan_amount,
-record.residual_amount,
)
)
elif record.loan_type == "fixed-annuity-begin":
record.fixed_amount = -record.currency_id.round(
numpy_financial.pmt(
record._loan_rate() / 100,
record.fixed_periods,
record.fixed_loan_amount,
-record.residual_amount,
when="begin",
)
)
elif record.loan_type == "fixed-principal":
record.fixed_amount = record.currency_id.round(
(record.fixed_loan_amount - record.residual_amount)
/ record.fixed_periods
)
else:
record.fixed_amount = 0.0
@api.model
def _compute_rate(self, rate, rate_type, method_period):
"""
Returns the real rate
:param rate: Rate
:param rate_type: Computation rate
:param method_period: Number of months between payments
:return:
"""
if rate_type == "napr":
return rate / 12 * method_period
if rate_type == "ear":
return math.pow(1 + rate, method_period / 12) - 1
return rate
@api.depends("rate", "method_period", "rate_type")
def _compute_rate_period(self):
for record in self:
record.rate_period = record._loan_rate()
def _loan_rate(self):
return self._compute_rate(self.rate, self.rate_type, self.method_period)
@api.depends("journal_id", "company_id")
def _compute_currency(self):
for rec in self:
rec.currency_id = rec.journal_id.currency_id or rec.company_id.currency_id
@api.depends("is_leasing")
def _compute_journal_type(self):
for record in self:
if record.is_leasing:
record.journal_type = "purchase"
else:
record.journal_type = "general"
@api.onchange("is_leasing")
def _onchange_is_leasing(self):
self.journal_id = self.env["account.journal"].search(
[
("company_id", "=", self.company_id.id),
("type", "=", "purchase" if self.is_leasing else "general"),
],
limit=1,
)
self.residual_amount = 0.0
@api.onchange("company_id")
def _onchange_company(self):
self._onchange_is_leasing()
self.interest_expenses_account_id = (
self.short_term_loan_account_id
) = self.long_term_loan_account_id = False
def _get_default_name(self, vals):
return self.env["ir.sequence"].next_by_code("account.loan") or "/"
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("name", "/") == "/":
vals["name"] = self._get_default_name(vals)
return super().create(vals_list)
def post(self):
self.ensure_one()
if not self.start_date:
self.start_date = fields.Date.today()
self._compute_draft_lines()
self.write({"state": "posted"})
def close(self):
self.write({"state": "closed"})
def compute_lines(self):
self.ensure_one()
if self.state == "draft":
return self._compute_draft_lines()
return self._compute_posted_lines()
def _compute_posted_lines(self):
"""
Recompute the amounts of not finished lines. Useful if rate is changed
"""
amount = self.loan_amount
for line in self.line_ids.sorted("sequence"):
if line.move_ids:
amount = line.final_pending_principal_amount
else:
line.rate = self.rate_period
line.pending_principal_amount = amount
line._check_amount()
amount -= line.payment_amount - line.interests_amount
if self.long_term_loan_account_id:
self._check_long_term_principal_amount()
def _check_long_term_principal_amount(self):
"""
Recomputes the long term pending principal of unfinished lines.
"""
lines = self.line_ids.filtered(lambda r: not r.move_ids)
amount = 0
if not lines:
return
final_sequence = min(lines.mapped("sequence"))
for line in lines.sorted("sequence", reverse=True):
date = line.date + relativedelta(months=12)
if self.state == "draft" or line.sequence != final_sequence:
line.long_term_pending_principal_amount = sum(
self.line_ids.filtered(lambda r: r.date >= date).mapped(
"principal_amount"
)
)
line.long_term_principal_amount = (
line.long_term_pending_principal_amount - amount
)
amount = line.long_term_pending_principal_amount
def _new_line_vals(self, sequence, date, amount):
return {
"loan_id": self.id,
"sequence": sequence,
"date": date,
"pending_principal_amount": amount,
"rate": self.rate_period,
}
def _compute_draft_lines(self):
self.ensure_one()
self.fixed_periods = self.periods
self.fixed_loan_amount = self.loan_amount
self.line_ids.unlink()
amount = self.loan_amount
if self.start_date:
date = self.start_date
else:
date = datetime.today().date()
delta = relativedelta(months=self.method_period)
if not self.payment_on_first_period:
date += delta
for i in range(1, self.periods + 1):
line = self.env["account.loan.line"].create(
self._new_line_vals(i, date, amount)
)
line._check_amount()
date += delta
amount -= line.payment_amount - line.interests_amount
if self.long_term_loan_account_id:
self._check_long_term_principal_amount()
def view_account_moves(self):
self.ensure_one()
result = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_move_line_form"
)
result["domain"] = [("loan_id", "=", self.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["domain"] = [("loan_id", "=", self.id), ("move_type", "=", "in_invoice")]
return result
@api.model
def _generate_loan_entries(self, date):
"""
Generate the moves of unfinished loans before date
:param date:
:return:
"""
res = []
for record in self.search(
[("state", "=", "posted"), ("is_leasing", "=", False)]
):
lines = record.line_ids.filtered(
lambda r: r.date <= date and not r.move_ids
)
res += lines._generate_move()
return res
@api.model
def _generate_leasing_entries(self, date):
res = []
for record in self.search(
[("state", "=", "posted"), ("is_leasing", "=", True)]
):
res += record.line_ids.filtered(
lambda r: r.date <= date and not r.move_ids
)._generate_invoice()
return res