mirror of
https://github.com/bringout/oca-financial.git
synced 2026-04-26 06:42:06 +02:00
Initial commit: OCA Financial packages (186 packages)
This commit is contained in:
commit
3e0e8473fb
8757 changed files with 947473 additions and 0 deletions
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import account_loan
|
||||
from . import account_loan_line
|
||||
from . import account_move
|
||||
from . import res_partner
|
||||
|
|
@ -0,0 +1,492 @@
|
|||
# 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)]},
|
||||
)
|
||||
long_term_journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
domain="[('company_id', '=', company_id),('type', '=', 'general')]",
|
||||
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
|
||||
|
|
@ -0,0 +1,525 @@
|
|||
# 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",
|
||||
store=True,
|
||||
)
|
||||
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):
|
||||
self.ensure_one()
|
||||
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 _add_basic_values(self, vals, account):
|
||||
self.ensure_one()
|
||||
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,
|
||||
}
|
||||
)
|
||||
return vals
|
||||
|
||||
def _add_interests_values(self, vals):
|
||||
self.ensure_one()
|
||||
vals.append(
|
||||
{
|
||||
"account_id": self.loan_id.interest_expenses_account_id.id,
|
||||
"credit": 0,
|
||||
"debit": self.interests_amount,
|
||||
}
|
||||
)
|
||||
return vals
|
||||
|
||||
def _add_short_term_account_values(self, vals):
|
||||
self.ensure_one()
|
||||
vals.append(
|
||||
{
|
||||
"account_id": self.loan_id.short_term_loan_account_id.id,
|
||||
"credit": 0,
|
||||
"debit": self.payment_amount - self.interests_amount,
|
||||
}
|
||||
)
|
||||
return vals
|
||||
|
||||
def _add_long_term_account_values(self, vals):
|
||||
self.ensure_one()
|
||||
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 _move_line_vals(self, account=False):
|
||||
self.ensure_one()
|
||||
vals = []
|
||||
vals = self._add_basic_values(vals, account)
|
||||
if self.interests_amount:
|
||||
vals = self._add_interests_values(vals)
|
||||
|
||||
vals = self._add_short_term_account_values(vals)
|
||||
vals = self._add_long_term_account_values(vals)
|
||||
|
||||
return vals
|
||||
|
||||
def _invoice_vals(self):
|
||||
self.ensure_one()
|
||||
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 _add_basic_values_invoice_line(self, vals):
|
||||
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,
|
||||
}
|
||||
)
|
||||
return vals
|
||||
|
||||
def _add_interests_values_invoice_line(self, vals):
|
||||
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 _invoice_line_vals(self):
|
||||
vals = list()
|
||||
vals = self._add_basic_values_invoice_line(vals)
|
||||
vals = self._add_interests_values_invoice_line(vals)
|
||||
return vals
|
||||
|
||||
def _auto_post_moves(self):
|
||||
"""
|
||||
Inhertiance hook to conditon posting of moves
|
||||
"""
|
||||
return True
|
||||
|
||||
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)
|
||||
)
|
||||
if record._auto_post_moves():
|
||||
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.long_term_journal_id.id
|
||||
or 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
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
loan_line_id = fields.Many2one(
|
||||
"account.loan.line",
|
||||
readonly=True,
|
||||
ondelete="restrict",
|
||||
)
|
||||
loan_id = fields.Many2one(
|
||||
"account.loan",
|
||||
readonly=True,
|
||||
store=True,
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
def action_post(self):
|
||||
res = super().action_post()
|
||||
for record in self:
|
||||
loan_line_id = record.loan_line_id
|
||||
if loan_line_id:
|
||||
record.loan_id = loan_line_id.loan_id
|
||||
record.loan_line_id._check_move_amount()
|
||||
record.loan_line_id.loan_id._compute_posted_lines()
|
||||
if record.loan_line_id.sequence == record.loan_id.periods:
|
||||
record.loan_id.close()
|
||||
return res
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2023 Dixmit
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
|
||||
_inherit = "res.partner"
|
||||
|
||||
lended_loan_ids = fields.One2many("account.loan", inverse_name="partner_id")
|
||||
lended_loan_count = fields.Integer(
|
||||
compute="_compute_lended_loan_count",
|
||||
help="How many Loans this partner lended to us ?",
|
||||
)
|
||||
|
||||
@api.depends("lended_loan_ids")
|
||||
def _compute_lended_loan_count(self):
|
||||
for record in self:
|
||||
record.lended_loan_count = len(record.lended_loan_ids)
|
||||
|
||||
def action_view_partner_lended_loans(self):
|
||||
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
"account_loan.account_loan_action"
|
||||
)
|
||||
all_child = self.with_context(active_test=False).search(
|
||||
[("id", "child_of", self.ids)]
|
||||
)
|
||||
action["domain"] = [("partner_id", "in", all_child.ids)]
|
||||
return action
|
||||
Loading…
Add table
Add a link
Reference in a new issue