diff --git a/account_loan/__manifest__.py b/account_loan/__manifest__.py index 8ad68efe..b2fa61f0 100644 --- a/account_loan/__manifest__.py +++ b/account_loan/__manifest__.py @@ -9,6 +9,7 @@ "category": "Accounting", "depends": ["account"], "data": [ + "wizards/account_loan_increase_amount.xml", "data/ir_sequence_data.xml", "security/ir.model.access.csv", "security/account_loan_security.xml", diff --git a/account_loan/models/account_loan_line.py b/account_loan/models/account_loan_line.py index 47313f62..f5457714 100644 --- a/account_loan/models/account_loan_line.py +++ b/account_loan/models/account_loan_line.py @@ -238,34 +238,38 @@ class AccountLoanLine(models.Model): + self.interests_amount ) - def _move_vals(self): + 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": self.loan_id.journal_id.id, - "line_ids": [Command.create(vals) for vals in self._move_line_vals()], + "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): + 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": partner.property_account_payable_id.id, + "account_id": (account and account.id) + or 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, - } - ) + 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, @@ -326,7 +330,7 @@ class AccountLoanLine(models.Model): ) return vals - def _generate_move(self): + def _generate_move(self, journal=False, account=False): """ Computes and post the moves of loans :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 ): 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() res.append(move.id) return res @@ -372,6 +378,9 @@ class AccountLoanLine(models.Model): 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 ( diff --git a/account_loan/security/ir.model.access.csv b/account_loan/security/ir.model.access.csv index 4046d304..1245d713 100644 --- a/account_loan/security/ir.model.access.csv +++ b/account_loan/security/ir.model.access.csv @@ -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_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_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 diff --git a/account_loan/tests/test_loan.py b/account_loan/tests/test_loan.py index d4f453ff..45e183d4 100644 --- a/account_loan/tests/test_loan.py +++ b/account_loan/tests/test_loan.py @@ -103,6 +103,52 @@ class TestLoan(TransactionCase): self.assertEqual(line_1.principal_amount, 0) 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): amount = 10000 periods = 24 @@ -132,26 +178,125 @@ class TestLoan(TransactionCase): self.assertTrue(line.move_ids) self.assertEqual(line.move_ids.state, "posted") 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, "fees": 100, "date": line.date + relativedelta(months=-1), } ).run() with self.assertRaises(UserError): - self.env["account.loan.pay.amount"].create( - {"loan_id": loan.id, "amount": amount, "fees": 100, "date": line.date} - ).run() + self.env["account.loan.pay.amount"].with_context( + default_loan_id=loan.id + ).create({"amount": amount, "fees": 100, "date": line.date}).run() with self.assertRaises(UserError): - self.env["account.loan.pay.amount"].create( - {"loan_id": loan.id, "amount": 0, "fees": 100, "date": line.date} - ).run() + self.env["account.loan.pay.amount"].with_context( + default_loan_id=loan.id + ).create({"amount": 0, "fees": 100, "date": line.date}).run() with self.assertRaises(UserError): - self.env["account.loan.pay.amount"].create( - {"loan_id": loan.id, "amount": -100, "fees": 100, "date": line.date} - ).run() + self.env["account.loan.pay.amount"].with_context( + default_loan_id=loan.id + ).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): amount = 10000 diff --git a/account_loan/views/account_loan_view.xml b/account_loan/views/account_loan_view.xml index 184bed31..9aea0489 100644 --- a/account_loan/views/account_loan_view.xml +++ b/account_loan/views/account_loan_view.xml @@ -66,12 +66,29 @@ +

diff --git a/account_loan/wizards/__init__.py b/account_loan/wizards/__init__.py index a80275e9..0c72565f 100644 --- a/account_loan/wizards/__init__.py +++ b/account_loan/wizards/__init__.py @@ -3,3 +3,4 @@ from . import account_loan_generate_entries from . import account_loan_pay_amount from . import account_loan_post +from . import account_loan_increase_amount diff --git a/account_loan/wizards/account_loan_increase_amount.py b/account_loan/wizards/account_loan_increase_amount.py new file mode 100644 index 00000000..9545a705 --- /dev/null +++ b/account_loan/wizards/account_loan_increase_amount.py @@ -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() diff --git a/account_loan/wizards/account_loan_increase_amount.xml b/account_loan/wizards/account_loan_increase_amount.xml new file mode 100644 index 00000000..abff87fe --- /dev/null +++ b/account_loan/wizards/account_loan_increase_amount.xml @@ -0,0 +1,40 @@ + + + + + + account.loan.increase.amount.form (in account_loan) + account.loan.increase.amount + +
+ + + + + + +
+
+
+
+
+ + + Increase Amount + account.loan.increase.amount + form + {'default_loan_id': active_id} + new + + + +
diff --git a/account_loan/wizards/account_loan_pay_amount.py b/account_loan/wizards/account_loan_pay_amount.py index af637477..7f95858b 100644 --- a/account_loan/wizards/account_loan_pay_amount.py +++ b/account_loan/wizards/account_loan_pay_amount.py @@ -57,12 +57,15 @@ class AccountLoan(models.TransientModel): lambda r: r.date > self.date and r.move_ids ): raise UserError(_("Some future invoices already exists")) - 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")) + 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 )