2
0

[IMP] account_loan: Allow to increase the amount of the loan

This commit is contained in:
Enric Tobella 2023-09-15 12:28:54 +02:00
parent 7781a19940
commit 90a5bf5353
9 changed files with 364 additions and 34 deletions

View File

@ -9,6 +9,7 @@
"category": "Accounting", "category": "Accounting",
"depends": ["account"], "depends": ["account"],
"data": [ "data": [
"wizards/account_loan_increase_amount.xml",
"data/ir_sequence_data.xml", "data/ir_sequence_data.xml",
"security/ir.model.access.csv", "security/ir.model.access.csv",
"security/account_loan_security.xml", "security/account_loan_security.xml",

View File

@ -238,27 +238,31 @@ class AccountLoanLine(models.Model):
+ self.interests_amount + self.interests_amount
) )
def _move_vals(self): def _move_vals(self, journal=False, account=False):
return { return {
"loan_line_id": self.id, "loan_line_id": self.id,
"loan_id": self.loan_id.id, "loan_id": self.loan_id.id,
"date": self.date, "date": self.date,
"ref": self.name, "ref": self.name,
"journal_id": self.loan_id.journal_id.id, "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()], "line_ids": [
Command.create(vals) for vals in self._move_line_vals(account=account)
],
} }
def _move_line_vals(self): def _move_line_vals(self, account=False):
vals = [] vals = []
partner = self.loan_id.partner_id.with_company(self.loan_id.company_id) partner = self.loan_id.partner_id.with_company(self.loan_id.company_id)
vals.append( vals.append(
{ {
"account_id": partner.property_account_payable_id.id, "account_id": (account and account.id)
or partner.property_account_payable_id.id,
"partner_id": partner.id, "partner_id": partner.id,
"credit": self.payment_amount, "credit": self.payment_amount,
"debit": 0, "debit": 0,
} }
) )
if self.interests_amount:
vals.append( vals.append(
{ {
"account_id": self.loan_id.interest_expenses_account_id.id, "account_id": self.loan_id.interest_expenses_account_id.id,
@ -326,7 +330,7 @@ class AccountLoanLine(models.Model):
) )
return vals return vals
def _generate_move(self): def _generate_move(self, journal=False, account=False):
""" """
Computes and post the moves of loans Computes and post the moves of loans
:return: list of account.move generated :return: list of account.move generated
@ -338,7 +342,9 @@ class AccountLoanLine(models.Model):
lambda r: r.date < record.date and not r.move_ids lambda r: r.date < record.date and not r.move_ids
): ):
raise UserError(_("Some moves must be created first")) raise UserError(_("Some moves must be created first"))
move = self.env["account.move"].create(record._move_vals()) move = self.env["account.move"].create(
record._move_vals(journal=journal, account=account)
)
move.action_post() move.action_post()
res.append(move.id) res.append(move.id)
return res return res
@ -372,6 +378,9 @@ class AccountLoanLine(models.Model):
for line in invoice.invoice_line_ids: for line in invoice.invoice_line_ids:
line.tax_ids = line._get_computed_taxes() line.tax_ids = line._get_computed_taxes()
invoice.flush_recordset() 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: if record.loan_id.post_invoice:
invoice.action_post() invoice.action_post()
if ( if (

View File

@ -5,4 +5,5 @@ access_account_loan_line,account.loan.line,model_account_loan_line,account.group
access_account_loan_line_manager,account.loan.line,model_account_loan_line,account.group_account_manager,1,1,1,1 access_account_loan_line_manager,account.loan.line,model_account_loan_line,account.group_account_manager,1,1,1,1
access_account_loan_generate_wizard,access_account_loan_generate_wizard,model_account_loan_generate_wizard,account.group_account_manager,1,1,1,1 access_account_loan_generate_wizard,access_account_loan_generate_wizard,model_account_loan_generate_wizard,account.group_account_manager,1,1,1,1
access_account_loan_pay_amount,access_account_loan_pay_amount,model_account_loan_pay_amount,account.group_account_manager,1,1,1,1 access_account_loan_pay_amount,access_account_loan_pay_amount,model_account_loan_pay_amount,account.group_account_manager,1,1,1,1
access_account_loan_increase_amount,access_account_loan_increase_amount,model_account_loan_increase_amount,account.group_account_manager,1,1,1,1
access_account_loan_post,access_account_loan_post,model_account_loan_post,account.group_account_manager,1,1,1,1 access_account_loan_post,access_account_loan_post,model_account_loan_post,account.group_account_manager,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_account_loan_line_manager account.loan.line model_account_loan_line account.group_account_manager 1 1 1 1
6 access_account_loan_generate_wizard access_account_loan_generate_wizard model_account_loan_generate_wizard account.group_account_manager 1 1 1 1
7 access_account_loan_pay_amount access_account_loan_pay_amount model_account_loan_pay_amount account.group_account_manager 1 1 1 1
8 access_account_loan_increase_amount access_account_loan_increase_amount model_account_loan_increase_amount account.group_account_manager 1 1 1 1
9 access_account_loan_post access_account_loan_post model_account_loan_post account.group_account_manager 1 1 1 1

View File

@ -103,6 +103,52 @@ class TestLoan(TransactionCase):
self.assertEqual(line_1.principal_amount, 0) self.assertEqual(line_1.principal_amount, 0)
self.assertEqual(line_end.principal_amount, 500000) self.assertEqual(line_end.principal_amount, 500000)
def test_increase_amount_validation(self):
amount = 10000
periods = 24
loan = self.create_loan("fixed-annuity", amount, 1, periods)
self.assertTrue(loan.line_ids)
self.assertEqual(len(loan.line_ids), periods)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertAlmostEqual(
-numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
)
self.assertEqual(line.long_term_principal_amount, 0)
loan.long_term_loan_account_id = self.lt_loan_account
loan.compute_lines()
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertGreater(line.long_term_principal_amount, 0)
self.post(loan)
self.assertTrue(loan.start_date)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertTrue(line)
self.assertFalse(line.move_ids)
wzd = self.env["account.loan.generate.wizard"].create({})
action = wzd.run()
self.assertTrue(action)
self.assertFalse(wzd.run())
self.assertTrue(line.move_ids)
self.assertIn(line.move_ids.id, action["domain"][0][2])
self.assertTrue(line.move_ids)
self.assertEqual(line.move_ids.state, "posted")
with self.assertRaises(UserError):
self.env["account.loan.increase.amount"].with_context(
default_loan_id=loan.id
).create(
{
"amount": (amount - amount / periods) / 2,
"date": line.date + relativedelta(months=-1),
}
).run()
with self.assertRaises(UserError):
self.env["account.loan.increase.amount"].with_context(
default_loan_id=loan.id
).create({"amount": 0, "date": line.date}).run()
with self.assertRaises(UserError):
self.env["account.loan.increase.amount"].with_context(
default_loan_id=loan.id
).create({"amount": -100, "date": line.date}).run()
def test_pay_amount_validation(self): def test_pay_amount_validation(self):
amount = 10000 amount = 10000
periods = 24 periods = 24
@ -132,26 +178,125 @@ class TestLoan(TransactionCase):
self.assertTrue(line.move_ids) self.assertTrue(line.move_ids)
self.assertEqual(line.move_ids.state, "posted") self.assertEqual(line.move_ids.state, "posted")
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.env["account.loan.pay.amount"].create( self.env["account.loan.pay.amount"].with_context(
default_loan_id=loan.id
).create(
{ {
"loan_id": loan.id,
"amount": (amount - amount / periods) / 2, "amount": (amount - amount / periods) / 2,
"fees": 100, "fees": 100,
"date": line.date + relativedelta(months=-1), "date": line.date + relativedelta(months=-1),
} }
).run() ).run()
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.env["account.loan.pay.amount"].create( self.env["account.loan.pay.amount"].with_context(
{"loan_id": loan.id, "amount": amount, "fees": 100, "date": line.date} default_loan_id=loan.id
).run() ).create({"amount": amount, "fees": 100, "date": line.date}).run()
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.env["account.loan.pay.amount"].create( self.env["account.loan.pay.amount"].with_context(
{"loan_id": loan.id, "amount": 0, "fees": 100, "date": line.date} default_loan_id=loan.id
).run() ).create({"amount": 0, "fees": 100, "date": line.date}).run()
with self.assertRaises(UserError): with self.assertRaises(UserError):
self.env["account.loan.pay.amount"].create( self.env["account.loan.pay.amount"].with_context(
{"loan_id": loan.id, "amount": -100, "fees": 100, "date": line.date} default_loan_id=loan.id
).run() ).create({"amount": -100, "fees": 100, "date": line.date}).run()
def test_increase_amount_loan(self):
amount = 10000
periods = 24
loan = self.create_loan("fixed-annuity", amount, 1, periods)
self.assertTrue(loan.line_ids)
self.assertEqual(len(loan.line_ids), periods)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertAlmostEqual(
-numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
)
self.assertEqual(line.long_term_principal_amount, 0)
loan.long_term_loan_account_id = self.lt_loan_account
loan.compute_lines()
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertGreater(line.long_term_principal_amount, 0)
self.post(loan)
self.assertTrue(loan.start_date)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertTrue(line)
self.assertFalse(line.move_ids)
wzd = self.env["account.loan.generate.wizard"].create({})
action = wzd.run()
self.assertTrue(action)
self.assertFalse(wzd.run())
self.assertTrue(line.move_ids)
self.assertIn(line.move_ids.id, action["domain"][0][2])
self.assertTrue(line.move_ids)
self.assertEqual(line.move_ids.state, "posted")
pending_principal_amount = loan.pending_principal_amount
action = (
self.env["account.loan.increase.amount"]
.with_context(default_loan_id=loan.id)
.create(
{
"amount": 1000,
"date": line.date,
}
)
.run()
)
new_move = self.env[action["res_model"]].search(action["domain"])
new_move.ensure_one()
self.assertFalse(new_move.is_invoice())
self.assertEqual(loan, new_move.loan_id)
self.assertEqual(loan.pending_principal_amount, pending_principal_amount + 1000)
def test_increase_amount_leasing(self):
amount = 10000
periods = 24
loan = self.create_loan("fixed-annuity", amount, 1, periods)
self.assertTrue(loan.line_ids)
self.assertEqual(len(loan.line_ids), periods)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertAlmostEqual(
-numpy_financial.pmt(1 / 100 / 12, 24, 10000), line.payment_amount, 2
)
self.assertEqual(line.long_term_principal_amount, 0)
loan.is_leasing = True
loan.long_term_loan_account_id = self.lt_loan_account
loan.compute_lines()
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertGreater(line.long_term_principal_amount, 0)
self.post(loan)
self.assertTrue(loan.start_date)
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
self.assertTrue(line)
self.assertFalse(line.move_ids)
wzd = self.env["account.loan.generate.wizard"].create(
{
"date": fields.date.today() + relativedelta(days=1),
"loan_type": "leasing",
}
)
action = wzd.run()
self.assertTrue(action)
self.assertFalse(wzd.run())
self.assertTrue(line.move_ids)
self.assertIn(line.move_ids.id, action["domain"][0][2])
self.assertTrue(line.move_ids)
self.assertEqual(line.move_ids.state, "posted")
pending_principal_amount = loan.pending_principal_amount
action = (
self.env["account.loan.increase.amount"]
.with_context(default_loan_id=loan.id)
.create(
{
"amount": 1000,
"date": line.date,
}
)
.run()
)
new_move = self.env[action["res_model"]].search(action["domain"])
new_move.ensure_one()
self.assertFalse(new_move.is_invoice())
self.assertEqual(loan, new_move.loan_id)
self.assertEqual(loan.pending_principal_amount, pending_principal_amount + 1000)
def test_fixed_annuity_begin_loan(self): def test_fixed_annuity_begin_loan(self):
amount = 10000 amount = 10000

View File

@ -66,12 +66,29 @@
<button <button
name="%(account_loan_pay_amount_action)d" name="%(account_loan_pay_amount_action)d"
class="oe_stat_button" class="oe_stat_button"
icon="fa-usd" icon="fa-arrow-down"
attrs="{'invisible': [('state', '!=', 'posted')]}" attrs="{'invisible': [('state', '!=', 'posted')]}"
type="action" type="action"
string="Pay amount"
groups="account.group_account_manager" groups="account.group_account_manager"
/> >
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Deduct</span>
<span class="o_stat_text">Debt</span>
</div>
</button>
<button
name="%(account_loan_increase_amount_act_window)d"
class="oe_stat_button"
icon="fa-arrow-up"
attrs="{'invisible': [('state', '!=', 'posted')]}"
type="action"
groups="account.group_account_manager"
>
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Increase</span>
<span class="o_stat_text">Debt</span>
</div>
</button>
</div> </div>
<h1> <h1>
<field name="name" /> <field name="name" />

View File

@ -3,3 +3,4 @@
from . import account_loan_generate_entries from . import account_loan_generate_entries
from . import account_loan_pay_amount from . import account_loan_pay_amount
from . import account_loan_post from . import account_loan_post
from . import account_loan_increase_amount

View File

@ -0,0 +1,113 @@
# Copyright 2023 Dixmit
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountLoanIncreaseAmount(models.TransientModel):
_name = "account.loan.increase.amount"
_description = "Increase the debt of a loan"
@api.model
def _default_journal_id(self):
loan_id = self.env.context.get("default_loan_id")
if loan_id:
return self.env["account.loan"].browse(loan_id).journal_id.id
@api.model
def _default_account_id(self):
loan_id = self.env.context.get("default_loan_id")
if loan_id:
loan = self.env["account.loan"].browse(loan_id)
if loan.is_leasing:
return loan.leased_asset_account_id.id
else:
return loan.partner_id.with_company(
loan.company_id
).property_account_receivable_id.id
journal_id = fields.Many2one(
"account.journal", required=True, default=lambda r: r._default_journal_id()
)
account_id = fields.Many2one(
"account.account", required=True, default=lambda r: r._default_account_id()
)
loan_id = fields.Many2one(
"account.loan",
required=True,
readonly=True,
)
currency_id = fields.Many2one(
"res.currency", related="loan_id.currency_id", readonly=True
)
date = fields.Date(required=True, default=fields.Date.today())
amount = fields.Monetary(
currency_field="currency_id",
string="Amount to reduce from Principal",
)
def new_line_vals(self, sequence):
return {
"loan_id": self.loan_id.id,
"sequence": sequence,
"payment_amount": -self.amount,
"rate": 0,
"interests_amount": 0,
"date": self.date,
}
def run(self):
self.ensure_one()
if self.loan_id.is_leasing:
if self.loan_id.line_ids.filtered(
lambda r: r.date <= self.date and not r.move_ids
):
raise UserError(_("Some invoices are not created"))
if self.loan_id.line_ids.filtered(
lambda r: r.date > self.date and r.move_ids
):
raise UserError(_("Some future invoices already exists"))
else:
if self.loan_id.line_ids.filtered(
lambda r: r.date < self.date and not r.move_ids
):
raise UserError(_("Some moves are not created"))
if self.loan_id.line_ids.filtered(
lambda r: r.date > self.date and r.move_ids
):
raise UserError(_("Some future moves already exists"))
lines = self.loan_id.line_ids.filtered(lambda r: r.date > self.date).sorted(
"sequence", reverse=True
)
sequence = min(lines.mapped("sequence"))
for line in lines:
line.sequence += 1
line.flush_recordset()
old_line = lines.filtered(lambda r: r.sequence == sequence + 1)
pending = old_line.pending_principal_amount
if self.loan_id.currency_id.compare_amounts(self.amount, 0) <= 0:
raise UserError(_("Amount cannot be less than zero"))
self.loan_id.periods += 1
self.loan_id.fixed_periods = self.loan_id.periods - sequence
self.loan_id.fixed_loan_amount = pending - self.amount
new_line = self.env["account.loan.line"].create(self.new_line_vals(sequence))
new_line.long_term_pending_principal_amount = (
old_line.long_term_pending_principal_amount
)
amount = self.loan_id.loan_amount
for line in self.loan_id.line_ids.sorted("sequence"):
if line.move_ids:
amount = line.final_pending_principal_amount
else:
line.pending_principal_amount = amount
if line.sequence != sequence:
line.rate = self.loan_id.rate_period
line._check_amount()
amount -= line.payment_amount - line.interests_amount
if self.loan_id.long_term_loan_account_id:
self.loan_id._check_long_term_principal_amount()
if self.loan_id.currency_id.compare_amounts(pending, self.amount) == 0:
self.loan_id.write({"state": "cancelled"})
new_line._generate_move(journal=self.journal_id, account=self.account_id)
return new_line.view_account_values()

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Dixmit
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="account_loan_increase_amount_form_view">
<field name="name">account.loan.increase.amount.form (in account_loan)</field>
<field name="model">account.loan.increase.amount</field>
<field name="arch" type="xml">
<form>
<group>
<field name="loan_id" readonly="True" />
<field name="date" />
<field name="amount" />
<field name="currency_id" />
</group>
<footer>
<button
name="run"
string="Run"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="account_loan_increase_amount_act_window">
<field name="name">Increase Amount</field>
<field name="res_model">account.loan.increase.amount</field>
<field name="view_mode">form</field>
<field name="context">{'default_loan_id': active_id}</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -57,11 +57,14 @@ class AccountLoan(models.TransientModel):
lambda r: r.date > self.date and r.move_ids lambda r: r.date > self.date and r.move_ids
): ):
raise UserError(_("Some future invoices already exists")) raise UserError(_("Some future invoices already exists"))
else:
if self.loan_id.line_ids.filtered( if self.loan_id.line_ids.filtered(
lambda r: r.date < self.date and not r.move_ids lambda r: r.date < self.date and not r.move_ids
): ):
raise UserError(_("Some moves are not created")) raise UserError(_("Some moves are not created"))
if self.loan_id.line_ids.filtered(lambda r: r.date > self.date and r.move_ids): if self.loan_id.line_ids.filtered(
lambda r: r.date > self.date and r.move_ids
):
raise UserError(_("Some future moves already exists")) raise UserError(_("Some future moves already exists"))
lines = self.loan_id.line_ids.filtered(lambda r: r.date > self.date).sorted( lines = self.loan_id.line_ids.filtered(lambda r: r.date > self.date).sorted(
"sequence", reverse=True "sequence", reverse=True