mirror of
https://github.com/OCA/account-financial-tools.git
synced 2025-02-02 12:47:26 +02:00
131
account_loan/README.rst
Normal file
131
account_loan/README.rst
Normal file
@@ -0,0 +1,131 @@
|
||||
=======================
|
||||
Account Loan management
|
||||
=======================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:a97cd9ecd83ceef07255455f44533ac8e6d337004092ed8eb9febf1ee4e626fd
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/account-financial-tools/tree/17.0/account_loan
|
||||
:alt: OCA/account-financial-tools
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/account-financial-tools-17-0/account-financial-tools-17-0-account_loan
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=17.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module extends the functionality of accounting to support loans. It
|
||||
will create automatically moves or invoices for loans. Moreover, you can
|
||||
check the pending amount to be paid and reduce the debt.
|
||||
|
||||
It currently supports two kinds of debts:
|
||||
|
||||
- | Loans: a standard debt with banks, that only creates account moves.
|
||||
| Loan types info:
|
||||
`APR <https://en.wikipedia.org/wiki/Annual_percentage_rate>`__,
|
||||
`EAR <https://en.wikipedia.org/wiki/Effective_interest_rate>`__,
|
||||
`Real Rate <https://en.wikipedia.org/wiki/Real_interest_rate>`__.
|
||||
|
||||
- Leases: a debt with a bank where purchase invoices are necessary
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
1. Go to Invoicing / Accounting > Accounting > Loans
|
||||
2. Configure a loan selecting the company, loan type, amount, rate and
|
||||
accounts
|
||||
3. Post the loan, it will automatically create an account move with the
|
||||
expected amounts
|
||||
4. Create automatically the account moves / invoices related to loans
|
||||
and leases before a selected date
|
||||
|
||||
On a posted loan you can:
|
||||
|
||||
- Create moves or invoices (according to the configuration)
|
||||
- Modify rates when needed (only unposted lines will be modified)
|
||||
- Reduce or cancel the debt of a loan / lease
|
||||
|
||||
|Try me on Runbot|
|
||||
|
||||
.. |Try me on Runbot| image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:target: https://runbot.odoo-community.org/runbot/92/12.0
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
16.0.1.0.0
|
||||
----------
|
||||
|
||||
Due to the changes on 16, we will generate two moves on leasings, one
|
||||
for the invoice, and another one for the change from long to short term.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_loan%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
* Creu Blanca
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
- Enric Tobella <etobella@creublanca.es>
|
||||
- Bhavesh Odedra <bodedra@opensourceintegrators.com>
|
||||
- Alberto Martín Cortada <alberto.martin@guadaltech.es>
|
||||
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px
|
||||
:target: https://github.com/etobella
|
||||
:alt: etobella
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-etobella|
|
||||
|
||||
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/17.0/account_loan>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
4
account_loan/__init__.py
Normal file
4
account_loan/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
30
account_loan/__manifest__.py
Normal file
30
account_loan/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Account Loan management",
|
||||
"version": "17.0.1.0.0",
|
||||
"author": "Creu Blanca,Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/account-financial-tools",
|
||||
"license": "AGPL-3",
|
||||
"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",
|
||||
"wizards/account_loan_generate_entries_view.xml",
|
||||
"wizards/account_loan_pay_amount_view.xml",
|
||||
"wizards/account_loan_post_view.xml",
|
||||
"views/account_loan_view.xml",
|
||||
"views/account_move_view.xml",
|
||||
"views/res_partner.xml",
|
||||
"views/account_loan_lines_view.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"maintainers": ["etobella"],
|
||||
"external_dependencies": {
|
||||
"python": ["numpy>=1.15", "numpy-financial<=1.0.0"],
|
||||
"deb": ["libatlas-base-dev"],
|
||||
},
|
||||
}
|
||||
15
account_loan/data/ir_sequence_data.xml
Normal file
15
account_loan/data/ir_sequence_data.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
Copyright 2017 Eficent Business and IT Consulting Services, S.L.
|
||||
Copyright 2017 Creu Blanca
|
||||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
<odoo>
|
||||
<record id="seq_account_loan" model="ir.sequence">
|
||||
<field name="name">Account loan sequence</field>
|
||||
<field name="code">account.loan</field>
|
||||
<field name="prefix">ACL</field>
|
||||
<field name="padding">6</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
||||
1026
account_loan/i18n/account_loan.pot
Normal file
1026
account_loan/i18n/account_loan.pot
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/am.po
Normal file
1031
account_loan/i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/ar.po
Normal file
1033
account_loan/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/bg.po
Normal file
1032
account_loan/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/bs.po
Normal file
1033
account_loan/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
1047
account_loan/i18n/ca.po
Normal file
1047
account_loan/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/ca_ES.po
Normal file
1032
account_loan/i18n/ca_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/cs.po
Normal file
1032
account_loan/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/da.po
Normal file
1031
account_loan/i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
1047
account_loan/i18n/de.po
Normal file
1047
account_loan/i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/el_GR.po
Normal file
1033
account_loan/i18n/el_GR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/en_AU.po
Normal file
1032
account_loan/i18n/en_AU.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/en_GB.po
Normal file
1033
account_loan/i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
1054
account_loan/i18n/es.po
Normal file
1054
account_loan/i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
1052
account_loan/i18n/es_AR.po
Normal file
1052
account_loan/i18n/es_AR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_CL.po
Normal file
1032
account_loan/i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
1034
account_loan/i18n/es_CO.po
Normal file
1034
account_loan/i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/es_CR.po
Normal file
1033
account_loan/i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_DO.po
Normal file
1032
account_loan/i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/es_EC.po
Normal file
1033
account_loan/i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_ES.po
Normal file
1032
account_loan/i18n/es_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/es_MX.po
Normal file
1033
account_loan/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_PE.po
Normal file
1032
account_loan/i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_PY.po
Normal file
1032
account_loan/i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/es_VE.po
Normal file
1032
account_loan/i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/et.po
Normal file
1032
account_loan/i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/eu.po
Normal file
1031
account_loan/i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/fa.po
Normal file
1031
account_loan/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/fi.po
Normal file
1032
account_loan/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
1041
account_loan/i18n/fr.po
Normal file
1041
account_loan/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/fr_CA.po
Normal file
1033
account_loan/i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/fr_CH.po
Normal file
1033
account_loan/i18n/fr_CH.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/fr_FR.po
Normal file
1033
account_loan/i18n/fr_FR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/gl.po
Normal file
1032
account_loan/i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/gl_ES.po
Normal file
1032
account_loan/i18n/gl_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/gu.po
Normal file
1031
account_loan/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/he.po
Normal file
1031
account_loan/i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/hi.po
Normal file
1031
account_loan/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/hr.po
Normal file
1033
account_loan/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
1034
account_loan/i18n/hr_HR.po
Normal file
1034
account_loan/i18n/hr_HR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/hu.po
Normal file
1032
account_loan/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/id.po
Normal file
1032
account_loan/i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
1052
account_loan/i18n/it.po
Normal file
1052
account_loan/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/ja.po
Normal file
1032
account_loan/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/ko.po
Normal file
1031
account_loan/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/lo.po
Normal file
1031
account_loan/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/lt.po
Normal file
1033
account_loan/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/lt_LT.po
Normal file
1033
account_loan/i18n/lt_LT.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/lv.po
Normal file
1032
account_loan/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/mk.po
Normal file
1032
account_loan/i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/mn.po
Normal file
1032
account_loan/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/nb.po
Normal file
1033
account_loan/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/nb_NO.po
Normal file
1033
account_loan/i18n/nb_NO.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/nl.po
Normal file
1032
account_loan/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/nl_BE.po
Normal file
1033
account_loan/i18n/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/nl_NL.po
Normal file
1033
account_loan/i18n/nl_NL.po
Normal file
File diff suppressed because it is too large
Load Diff
1034
account_loan/i18n/pl.po
Normal file
1034
account_loan/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
1040
account_loan/i18n/pt.po
Normal file
1040
account_loan/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
1044
account_loan/i18n/pt_BR.po
Normal file
1044
account_loan/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/pt_PT.po
Normal file
1033
account_loan/i18n/pt_PT.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/ro.po
Normal file
1033
account_loan/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
1034
account_loan/i18n/ru.po
Normal file
1034
account_loan/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/sk.po
Normal file
1031
account_loan/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/sk_SK.po
Normal file
1033
account_loan/i18n/sk_SK.po
Normal file
File diff suppressed because it is too large
Load Diff
1034
account_loan/i18n/sl.po
Normal file
1034
account_loan/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/sr.po
Normal file
1032
account_loan/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/sr@latin.po
Normal file
1033
account_loan/i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
1050
account_loan/i18n/sv.po
Normal file
1050
account_loan/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/th.po
Normal file
1032
account_loan/i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/tr.po
Normal file
1032
account_loan/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/tr_TR.po
Normal file
1033
account_loan/i18n/tr_TR.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/uk.po
Normal file
1032
account_loan/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
1031
account_loan/i18n/vi.po
Normal file
1031
account_loan/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
1032
account_loan/i18n/vi_VN.po
Normal file
1032
account_loan/i18n/vi_VN.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/zh_CN.po
Normal file
1033
account_loan/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
1033
account_loan/i18n/zh_TW.po
Normal file
1033
account_loan/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
9
account_loan/migrations/17.0.1.0.0/noupdate_changes.xml
Normal file
9
account_loan/migrations/17.0.1.0.0/noupdate_changes.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="account_loan_multi_company_rule" model="ir.rule">
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
<record id="account_loan_line_multi_company_rule" model="ir.rule">
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
10
account_loan/migrations/17.0.1.0.0/post-migration.py
Normal file
10
account_loan/migrations/17.0.1.0.0/post-migration.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
openupgrade.load_data(
|
||||
env.cr, "account_loan", "migrations/17.0.1.0.0/noupdate_changes.xml"
|
||||
)
|
||||
7
account_loan/models/__init__.py
Normal file
7
account_loan/models/__init__.py
Normal file
@@ -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
|
||||
446
account_loan/models/account_loan.py
Normal file
446
account_loan/models/account_loan.py
Normal file
@@ -0,0 +1,446 @@
|
||||
# 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 (OSError, ImportError) 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,
|
||||
default="/",
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
required=True,
|
||||
string="Lender",
|
||||
help="Company or individual that lends the money at an interest rate.",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
required=True,
|
||||
default=_default_company,
|
||||
)
|
||||
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,
|
||||
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,
|
||||
)
|
||||
start_date = fields.Date(
|
||||
help="Start of the moves",
|
||||
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",
|
||||
)
|
||||
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",
|
||||
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,
|
||||
)
|
||||
residual_amount = fields.Monetary(
|
||||
currency_field="currency_id",
|
||||
default=0.0,
|
||||
required=True,
|
||||
help="Residual amount of the lease that must be payed on the end in "
|
||||
"order to acquire the asset",
|
||||
)
|
||||
round_on_end = fields.Boolean(
|
||||
help="When checked, the differences will be applied on the last period"
|
||||
", if it is unchecked, the annuity will be recalculated on each "
|
||||
"period.",
|
||||
)
|
||||
payment_on_first_period = fields.Boolean(
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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)]",
|
||||
)
|
||||
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,
|
||||
)
|
||||
is_leasing = fields.Boolean()
|
||||
leased_asset_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
domain="[('company_id', '=', company_id)]",
|
||||
)
|
||||
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, date=date: 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_in_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
|
||||
475
account_loan/models/account_loan_line.py
Normal file
475
account_loan/models/account_loan_line.py
Normal file
@@ -0,0 +1,475 @@
|
||||
# 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 (OSError, ImportError) 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",
|
||||
related="loan_id.company_id",
|
||||
store=True,
|
||||
)
|
||||
partner_id = fields.Many2one("res.partner", related="loan_id.partner_id")
|
||||
is_leasing = fields.Boolean(
|
||||
related="loan_id.is_leasing",
|
||||
)
|
||||
journal_id = fields.Many2one(
|
||||
"account.journal",
|
||||
related="loan_id.journal_id",
|
||||
)
|
||||
short_term_loan_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
related="loan_id.short_term_loan_account_id",
|
||||
)
|
||||
interest_expenses_account_id = fields.Many2one(
|
||||
"account.account",
|
||||
related="loan_id.interest_expenses_account_id",
|
||||
)
|
||||
loan_type = fields.Selection(
|
||||
related="loan_id.loan_type",
|
||||
)
|
||||
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",
|
||||
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, record=record: 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, record=record: 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_move_type()
|
||||
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
|
||||
32
account_loan/models/account_move.py
Normal file
32
account_loan/models/account_move.py
Normal file
@@ -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
|
||||
30
account_loan/models/res_partner.py
Normal file
30
account_loan/models/res_partner.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
3
account_loan/pyproject.toml
Normal file
3
account_loan/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
3
account_loan/readme/CONTRIBUTORS.md
Normal file
3
account_loan/readme/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,3 @@
|
||||
- Enric Tobella \<<etobella@creublanca.es>\>
|
||||
- Bhavesh Odedra \<<bodedra@opensourceintegrators.com>\>
|
||||
- Alberto Martín Cortada \<<alberto.martin@guadaltech.es>\>
|
||||
13
account_loan/readme/DESCRIPTION.md
Normal file
13
account_loan/readme/DESCRIPTION.md
Normal file
@@ -0,0 +1,13 @@
|
||||
This module extends the functionality of accounting to support loans. It
|
||||
will create automatically moves or invoices for loans. Moreover, you can
|
||||
check the pending amount to be paid and reduce the debt.
|
||||
|
||||
It currently supports two kinds of debts:
|
||||
|
||||
- Loans: a standard debt with banks, that only creates account moves.
|
||||
Loan types info:
|
||||
[APR](https://en.wikipedia.org/wiki/Annual_percentage_rate),
|
||||
[EAR](https://en.wikipedia.org/wiki/Effective_interest_rate), [Real
|
||||
Rate](https://en.wikipedia.org/wiki/Real_interest_rate).
|
||||
|
||||
- Leases: a debt with a bank where purchase invoices are necessary
|
||||
4
account_loan/readme/HISTORY.md
Normal file
4
account_loan/readme/HISTORY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## 16.0.1.0.0
|
||||
|
||||
Due to the changes on 16, we will generate two moves on leasings, one
|
||||
for the invoice, and another one for the change from long to short term.
|
||||
17
account_loan/readme/USAGE.md
Normal file
17
account_loan/readme/USAGE.md
Normal file
@@ -0,0 +1,17 @@
|
||||
To use this module, you need to:
|
||||
|
||||
1. Go to Invoicing / Accounting \> Accounting \> Loans
|
||||
2. Configure a loan selecting the company, loan type, amount, rate and
|
||||
accounts
|
||||
3. Post the loan, it will automatically create an account move with the
|
||||
expected amounts
|
||||
4. Create automatically the account moves / invoices related to loans
|
||||
and leases before a selected date
|
||||
|
||||
On a posted loan you can:
|
||||
|
||||
- Create moves or invoices (according to the configuration)
|
||||
- Modify rates when needed (only unposted lines will be modified)
|
||||
- Reduce or cancel the debt of a loan / lease
|
||||
|
||||
[](https://runbot.odoo-community.org/runbot/92/12.0)
|
||||
16
account_loan/security/account_loan_security.xml
Normal file
16
account_loan/security/account_loan_security.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="account_loan_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account loan multi-company</field>
|
||||
<field ref="model_account_loan" name="model_id" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_line_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Account loan line multi-company</field>
|
||||
<field ref="model_account_loan_line" name="model_id" />
|
||||
<field eval="True" name="global" />
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
9
account_loan/security/ir.model.access.csv
Normal file
9
account_loan/security/ir.model.access.csv
Normal file
@@ -0,0 +1,9 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_loan,account.loan,model_account_loan,account.group_account_user,1,0,0,0
|
||||
access_account_loan_manager,account.loan,model_account_loan,account.group_account_manager,1,1,1,1
|
||||
access_account_loan_line,account.loan.line,model_account_loan_line,account.group_account_user,1,0,0,0
|
||||
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
|
||||
|
BIN
account_loan/static/description/icon.png
Normal file
BIN
account_loan/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
475
account_loan/static/description/index.html
Normal file
475
account_loan/static/description/index.html
Normal file
@@ -0,0 +1,475 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Account Loan management</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="account-loan-management">
|
||||
<h1 class="title">Account Loan management</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:a97cd9ecd83ceef07255455f44533ac8e6d337004092ed8eb9febf1ee4e626fd
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-financial-tools/tree/17.0/account_loan"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-financial-tools-17-0/account-financial-tools-17-0-account_loan"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module extends the functionality of accounting to support loans. It
|
||||
will create automatically moves or invoices for loans. Moreover, you can
|
||||
check the pending amount to be paid and reduce the debt.</p>
|
||||
<p>It currently supports two kinds of debts:</p>
|
||||
<ul>
|
||||
<li><div class="first line-block">
|
||||
<div class="line">Loans: a standard debt with banks, that only creates account moves.</div>
|
||||
<div class="line">Loan types info:
|
||||
<a class="reference external" href="https://en.wikipedia.org/wiki/Annual_percentage_rate">APR</a>,
|
||||
<a class="reference external" href="https://en.wikipedia.org/wiki/Effective_interest_rate">EAR</a>,
|
||||
<a class="reference external" href="https://en.wikipedia.org/wiki/Real_interest_rate">Real Rate</a>.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><p class="first">Leases: a debt with a bank where purchase invoices are necessary</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-3">16.0.1.0.0</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
|
||||
<p>To use this module, you need to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Go to Invoicing / Accounting > Accounting > Loans</li>
|
||||
<li>Configure a loan selecting the company, loan type, amount, rate and
|
||||
accounts</li>
|
||||
<li>Post the loan, it will automatically create an account move with the
|
||||
expected amounts</li>
|
||||
<li>Create automatically the account moves / invoices related to loans
|
||||
and leases before a selected date</li>
|
||||
</ol>
|
||||
<p>On a posted loan you can:</p>
|
||||
<ul class="simple">
|
||||
<li>Create moves or invoices (according to the configuration)</li>
|
||||
<li>Modify rates when needed (only unposted lines will be modified)</li>
|
||||
<li>Reduce or cancel the debt of a loan / lease</li>
|
||||
</ul>
|
||||
<p><a class="reference external image-reference" href="https://runbot.odoo-community.org/runbot/92/12.0"><img alt="Try me on Runbot" src="https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas" /></a></p>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">16.0.1.0.0</a></h2>
|
||||
<p>Due to the changes on 16, we will generate two moves on leasings, one
|
||||
for the invoice, and another one for the change from long to short term.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_loan%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Creu Blanca</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li>
|
||||
<li>Bhavesh Odedra <<a class="reference external" href="mailto:bodedra@opensourceintegrators.com">bodedra@opensourceintegrators.com</a>></li>
|
||||
<li>Alberto Martín Cortada <<a class="reference external" href="mailto:alberto.martin@guadaltech.es">alberto.martin@guadaltech.es</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
|
||||
<p><a class="reference external image-reference" href="https://github.com/etobella"><img alt="etobella" src="https://github.com/etobella.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/17.0/account_loan">OCA/account-financial-tools</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
3
account_loan/tests/__init__.py
Normal file
3
account_loan/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_loan
|
||||
631
account_loan/tests/test_loan.py
Normal file
631
account_loan/tests/test_loan.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# Copyright 2018 Creu Blanca
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import logging
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.base.tests.common import BaseCommon
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import numpy_financial
|
||||
except (OSError, ImportError) as err:
|
||||
_logger.error(err)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestLoan(BaseCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company = cls.env.ref("base.main_company")
|
||||
cls.company_02 = cls.env["res.company"].create({"name": "Auxiliar company"})
|
||||
cls.journal = cls.env["account.journal"].create(
|
||||
{
|
||||
"company_id": cls.company.id,
|
||||
"type": "purchase",
|
||||
"name": "Debts",
|
||||
"code": "DBT",
|
||||
}
|
||||
)
|
||||
cls.loan_account = cls.create_account(
|
||||
"DEP",
|
||||
"depreciation",
|
||||
"liability_current",
|
||||
)
|
||||
cls.payable_account = cls.create_account("PAY", "payable", "liability_payable")
|
||||
cls.asset_account = cls.create_account("ASSET", "asset", "liability_payable")
|
||||
cls.interests_account = cls.create_account("FEE", "Fees", "expense")
|
||||
cls.lt_loan_account = cls.create_account(
|
||||
"LTD",
|
||||
"Long term depreciation",
|
||||
"liability_non_current",
|
||||
)
|
||||
cls.partner = cls.env["res.partner"].create({"name": "Bank"})
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{"name": "Payment", "type": "service"}
|
||||
)
|
||||
cls.interests_product = cls.env["product.product"].create(
|
||||
{"name": "Bank fee", "type": "service"}
|
||||
)
|
||||
|
||||
def test_onchange(self):
|
||||
loan = self.env["account.loan"].create(
|
||||
{
|
||||
"name": "LOAN",
|
||||
"company_id": self.company.id,
|
||||
"journal_id": self.journal.id,
|
||||
"loan_type": "fixed-annuity",
|
||||
"loan_amount": 100,
|
||||
"rate": 1,
|
||||
"periods": 2,
|
||||
"short_term_loan_account_id": self.loan_account.id,
|
||||
"interest_expenses_account_id": self.interests_account.id,
|
||||
"product_id": self.product.id,
|
||||
"interests_product_id": self.interests_product.id,
|
||||
"partner_id": self.partner.id,
|
||||
}
|
||||
)
|
||||
loan_form = Form(loan)
|
||||
loan_form.is_leasing = True
|
||||
self.assertNotEqual(loan.journal_id, loan_form.journal_id)
|
||||
loan_form.company_id = self.company_02
|
||||
self.assertFalse(loan_form.interest_expenses_account_id)
|
||||
|
||||
def test_partner_loans(self):
|
||||
self.assertFalse(self.partner.lended_loan_count)
|
||||
loan = self.create_loan("fixed-annuity", 500000, 1, 60)
|
||||
self.assertEqual(1, self.partner.lended_loan_count)
|
||||
action = self.partner.action_view_partner_lended_loans()
|
||||
self.assertEqual(loan, self.env[action["res_model"]].search(action["domain"]))
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_round_on_end(self):
|
||||
loan = self.create_loan("fixed-annuity", 500000, 1, 60)
|
||||
loan.round_on_end = True
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
for line in loan.line_ids:
|
||||
self.assertAlmostEqual(line_1.payment_amount, line.payment_amount, 2)
|
||||
loan.loan_type = "fixed-principal"
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
|
||||
self.assertNotAlmostEqual(line_1.payment_amount, line_end.payment_amount, 2)
|
||||
loan.loan_type = "interest"
|
||||
loan.compute_lines()
|
||||
line_1 = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line_end = loan.line_ids.filtered(lambda r: r.sequence == 60)
|
||||
self.assertEqual(line_1.principal_amount, 0)
|
||||
self.assertEqual(line_end.principal_amount, 500000)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
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()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_pay_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.pay.amount"].with_context(
|
||||
default_loan_id=loan.id
|
||||
).create(
|
||||
{
|
||||
"amount": (amount - amount / periods) / 2,
|
||||
"fees": 100,
|
||||
"date": line.date + relativedelta(months=-1),
|
||||
}
|
||||
).run()
|
||||
with self.assertRaises(UserError):
|
||||
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"].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"].with_context(
|
||||
default_loan_id=loan.id
|
||||
).create({"amount": -100, "fees": 100, "date": line.date}).run()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
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)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
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)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_fixed_annuity_begin_loan(self):
|
||||
amount = 10000
|
||||
periods = 24
|
||||
loan = self.create_loan("fixed-annuity-begin", 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, when="begin"),
|
||||
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")
|
||||
loan.rate = 2
|
||||
loan.compute_lines()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertAlmostEqual(
|
||||
-numpy_financial.pmt(1 / 100 / 12, periods, amount, when="begin"),
|
||||
line.payment_amount,
|
||||
2,
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 2)
|
||||
self.assertAlmostEqual(
|
||||
-numpy_financial.pmt(
|
||||
2 / 100 / 12, periods - 1, line.pending_principal_amount, when="begin"
|
||||
),
|
||||
line.payment_amount,
|
||||
2,
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 3)
|
||||
with self.assertRaises(UserError):
|
||||
line.view_process_values()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_fixed_annuity_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")
|
||||
loan.rate = 2
|
||||
loan.compute_lines()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertAlmostEqual(
|
||||
-numpy_financial.pmt(1 / 100 / 12, periods, amount), line.payment_amount, 2
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 2)
|
||||
self.assertAlmostEqual(
|
||||
-numpy_financial.pmt(
|
||||
2 / 100 / 12, periods - 1, line.pending_principal_amount
|
||||
),
|
||||
line.payment_amount,
|
||||
2,
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 3)
|
||||
with self.assertRaises(UserError):
|
||||
line.view_process_values()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_fixed_principal_loan_leasing(self):
|
||||
amount = 24000
|
||||
periods = 24
|
||||
loan = self.create_loan("fixed-principal", amount, 1, periods)
|
||||
self.partner.property_account_payable_id = self.payable_account
|
||||
self.assertEqual(loan.journal_type, "general")
|
||||
loan.is_leasing = True
|
||||
loan.post_invoice = False
|
||||
self.assertEqual(loan.journal_type, "purchase")
|
||||
loan.long_term_loan_account_id = self.lt_loan_account
|
||||
loan.rate_type = "real"
|
||||
loan.compute_lines()
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertEqual(amount / periods, line.principal_amount)
|
||||
self.assertEqual(amount / periods, line.long_term_principal_amount)
|
||||
self.post(loan)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertTrue(line)
|
||||
self.assertFalse(line.has_invoices)
|
||||
self.assertFalse(line.has_moves)
|
||||
action = (
|
||||
self.env["account.loan.generate.wizard"]
|
||||
.create(
|
||||
{
|
||||
"date": fields.date.today() + relativedelta(days=1),
|
||||
"loan_type": "leasing",
|
||||
}
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.assertTrue(line.has_invoices)
|
||||
self.assertTrue(line.has_moves)
|
||||
self.assertEqual(
|
||||
line.move_ids, self.env[action["res_model"]].search(action["domain"])
|
||||
)
|
||||
loan.invalidate_recordset()
|
||||
with self.assertRaises(UserError):
|
||||
self.env["account.loan.pay.amount"].create(
|
||||
{
|
||||
"loan_id": loan.id,
|
||||
"amount": (amount - amount / periods) / 2,
|
||||
"fees": 100,
|
||||
"date": loan.line_ids.filtered(lambda r: r.sequence == 2).date,
|
||||
}
|
||||
).run()
|
||||
with self.assertRaises(UserError):
|
||||
self.env["account.loan.pay.amount"].create(
|
||||
{
|
||||
"loan_id": loan.id,
|
||||
"amount": (amount - amount / periods) / 2,
|
||||
"fees": 100,
|
||||
"date": loan.line_ids.filtered(lambda r: r.sequence == 1).date
|
||||
+ relativedelta(months=-1),
|
||||
}
|
||||
).run()
|
||||
self.assertTrue(line.move_ids)
|
||||
self.assertTrue(line.move_ids.filtered(lambda r: r.is_invoice()))
|
||||
self.assertTrue(line.move_ids.filtered(lambda r: not r.is_invoice()))
|
||||
self.assertTrue(all([m.state == "draft" for m in line.move_ids]))
|
||||
self.assertTrue(line.has_moves)
|
||||
line.move_ids.action_post()
|
||||
self.assertTrue(all([m.state == "posted" for m in line.move_ids]))
|
||||
for move in line.move_ids:
|
||||
self.assertIn(
|
||||
move,
|
||||
self.env["account.move"].search(loan.view_account_moves()["domain"]),
|
||||
)
|
||||
for move in line.move_ids.filtered(lambda r: r.is_invoice()):
|
||||
self.assertIn(
|
||||
move,
|
||||
self.env["account.move"].search(loan.view_account_invoices()["domain"]),
|
||||
)
|
||||
with self.assertRaises(UserError):
|
||||
self.env["account.loan.pay.amount"].create(
|
||||
{
|
||||
"loan_id": loan.id,
|
||||
"amount": (amount - amount / periods) / 2,
|
||||
"fees": 100,
|
||||
"date": loan.line_ids.filtered(
|
||||
lambda r: r.sequence == periods
|
||||
).date,
|
||||
}
|
||||
).run()
|
||||
self.env["account.loan.pay.amount"].create(
|
||||
{
|
||||
"loan_id": loan.id,
|
||||
"amount": (amount - amount / periods) / 2,
|
||||
"date": line.date,
|
||||
"fees": 100,
|
||||
}
|
||||
).run()
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 2)
|
||||
self.assertEqual(loan.periods, periods + 1)
|
||||
self.assertAlmostEqual(
|
||||
line.principal_amount, (amount - amount / periods) / 2, 2
|
||||
)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 3)
|
||||
self.assertEqual(amount / periods / 2, line.principal_amount)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 4)
|
||||
with self.assertRaises(UserError):
|
||||
line.view_process_values()
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_fixed_principal_loan_auto_post_leasing(self):
|
||||
amount = 24000
|
||||
periods = 24
|
||||
loan = self.create_loan("fixed-principal", amount, 1, periods)
|
||||
self.partner.property_account_payable_id = self.payable_account
|
||||
self.assertEqual(loan.journal_type, "general")
|
||||
loan.is_leasing = True
|
||||
self.assertEqual(loan.journal_type, "purchase")
|
||||
loan.long_term_loan_account_id = self.lt_loan_account
|
||||
loan.rate_type = "real"
|
||||
loan.compute_lines()
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertEqual(amount / periods, line.principal_amount)
|
||||
self.assertEqual(amount / periods, line.long_term_principal_amount)
|
||||
self.post(loan)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
self.assertTrue(line)
|
||||
self.assertFalse(line.has_invoices)
|
||||
self.assertFalse(line.has_moves)
|
||||
self.env["account.loan.generate.wizard"].create(
|
||||
{"date": fields.date.today(), "loan_type": "leasing"}
|
||||
).run()
|
||||
self.assertTrue(line.has_invoices)
|
||||
self.assertTrue(line.has_moves)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_interests_on_end_loan(self):
|
||||
amount = 10000
|
||||
periods = 10
|
||||
loan = self.create_loan("interest", amount, 1, periods)
|
||||
loan.payment_on_first_period = False
|
||||
loan.start_date = fields.Date.today()
|
||||
loan.rate_type = "ear"
|
||||
loan.compute_lines()
|
||||
self.assertTrue(loan.line_ids)
|
||||
self.assertEqual(len(loan.line_ids), periods)
|
||||
self.assertEqual(0, loan.line_ids[0].principal_amount)
|
||||
self.assertEqual(
|
||||
amount,
|
||||
loan.line_ids.filtered(lambda r: r.sequence == periods).principal_amount,
|
||||
)
|
||||
self.post(loan)
|
||||
self.assertEqual(loan.payment_amount, 0)
|
||||
self.assertEqual(loan.interests_amount, 0)
|
||||
self.assertEqual(loan.pending_principal_amount, amount)
|
||||
self.assertFalse(loan.line_ids.filtered(lambda r: r.date <= loan.start_date))
|
||||
for line in loan.line_ids:
|
||||
self.assertEqual(loan.state, "posted")
|
||||
line.view_process_values()
|
||||
self.assertTrue(line.move_ids)
|
||||
self.assertEqual(line.move_ids.state, "posted")
|
||||
self.assertEqual(loan.state, "closed")
|
||||
loan.invalidate_recordset()
|
||||
self.assertEqual(loan.payment_amount - loan.interests_amount, amount)
|
||||
self.assertEqual(loan.pending_principal_amount, 0)
|
||||
|
||||
@mute_logger("odoo.models.unlink")
|
||||
def test_cancel_loan(self):
|
||||
amount = 10000
|
||||
periods = 10
|
||||
loan = self.create_loan("fixed-annuity", amount, 1, periods)
|
||||
self.post(loan)
|
||||
line = loan.line_ids.filtered(lambda r: r.sequence == 1)
|
||||
line.view_process_values()
|
||||
self.assertTrue(line.move_ids)
|
||||
self.assertEqual(line.move_ids.state, "posted")
|
||||
pay = self.env["account.loan.pay.amount"].create(
|
||||
{"loan_id": loan.id, "amount": 0, "fees": 100, "date": line.date}
|
||||
)
|
||||
pay.cancel_loan = True
|
||||
pay._onchange_cancel_loan()
|
||||
self.assertEqual(pay.amount, line.final_pending_principal_amount)
|
||||
pay.run()
|
||||
self.assertEqual(loan.state, "cancelled")
|
||||
|
||||
def post(self, loan):
|
||||
self.assertFalse(loan.move_ids)
|
||||
post = (
|
||||
self.env["account.loan.post"]
|
||||
.with_context(default_loan_id=loan.id)
|
||||
.create({})
|
||||
)
|
||||
post.run()
|
||||
self.assertTrue(loan.move_ids)
|
||||
with self.assertRaises(UserError):
|
||||
post.run()
|
||||
|
||||
@classmethod
|
||||
def create_account(cls, code, name, account_type):
|
||||
return cls.env["account.account"].create(
|
||||
{
|
||||
"company_id": cls.company.id,
|
||||
"name": name,
|
||||
"code": code,
|
||||
"account_type": account_type,
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
|
||||
def create_loan(self, type_loan, amount, rate, periods):
|
||||
loan = self.env["account.loan"].create(
|
||||
{
|
||||
"journal_id": self.journal.id,
|
||||
"rate_type": "napr",
|
||||
"loan_type": type_loan,
|
||||
"loan_amount": amount,
|
||||
"payment_on_first_period": True,
|
||||
"rate": rate,
|
||||
"periods": periods,
|
||||
"leased_asset_account_id": self.asset_account.id,
|
||||
"short_term_loan_account_id": self.loan_account.id,
|
||||
"interest_expenses_account_id": self.interests_account.id,
|
||||
"product_id": self.product.id,
|
||||
"interests_product_id": self.interests_product.id,
|
||||
"partner_id": self.partner.id,
|
||||
}
|
||||
)
|
||||
loan.compute_lines()
|
||||
return loan
|
||||
140
account_loan/views/account_loan_lines_view.xml
Normal file
140
account_loan/views/account_loan_lines_view.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="account_loan_line_tree" model="ir.ui.view">
|
||||
<field name="name">account.loan.line.tree</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="0">
|
||||
<field name="sequence" />
|
||||
<field name="date" />
|
||||
<field name="rate" />
|
||||
<field name="pending_principal_amount" />
|
||||
<field name="payment_amount" sum="Total payments" />
|
||||
<field name="principal_amount" />
|
||||
<field name="interests_amount" sum="Total interests" />
|
||||
<field
|
||||
name="long_term_pending_principal_amount"
|
||||
column_invisible="not parent.long_term_loan_account_id"
|
||||
/>
|
||||
<field
|
||||
name="long_term_principal_amount"
|
||||
column_invisible="not parent.long_term_loan_account_id"
|
||||
/>
|
||||
<field name="long_term_loan_account_id" column_invisible="True" />
|
||||
<field name="loan_state" column_invisible="True" />
|
||||
<field name="is_leasing" column_invisible="True" />
|
||||
<field name="has_invoices" column_invisible="True" />
|
||||
<field name="has_moves" column_invisible="True" />
|
||||
<field name="currency_id" column_invisible="True" />
|
||||
<button
|
||||
name="view_account_values"
|
||||
string="Values"
|
||||
type="object"
|
||||
icon="fa-eye"
|
||||
invisible="not has_moves and not has_invoices"
|
||||
/>
|
||||
<button
|
||||
name="view_process_values"
|
||||
string="Process"
|
||||
type="object"
|
||||
icon="fa-cogs"
|
||||
invisible="has_moves or has_invoices or loan_state != 'posted'"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_loan_line_form" model="ir.ui.view">
|
||||
<field name="name">account.loan.line.form</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="sequence" />
|
||||
<field name="rate" />
|
||||
<field name="date" />
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="pending_principal_amount" />
|
||||
<field name="payment_amount" />
|
||||
<field name="principal_amount" />
|
||||
<field name="interests_amount" />
|
||||
<field name="final_pending_principal_amount" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="long_term_pending_principal_amount" />
|
||||
<field name="long_term_principal_amount" />
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_loan_lines_view" model="ir.ui.view">
|
||||
<field name="name">account.loan.lines.view</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="0" import="0">
|
||||
<field name="sequence" />
|
||||
<field name="loan_id" />
|
||||
<field name="date" />
|
||||
<field name="rate" />
|
||||
<field name="pending_principal_amount" />
|
||||
<field name="payment_amount" />
|
||||
<field name="principal_amount" />
|
||||
<field name="interests_amount" />
|
||||
<field name="long_term_pending_principal_amount" />
|
||||
<field name="long_term_principal_amount" />
|
||||
<field
|
||||
name="company_id"
|
||||
optional="hide"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="partner_id" optional="hide" />
|
||||
<field name="journal_id" optional="hide" />
|
||||
<field name="short_term_loan_account_id" optional="hide" />
|
||||
<field name="long_term_loan_account_id" optional="hide" />
|
||||
<field name="interest_expenses_account_id" optional="hide" />
|
||||
<groupby name="loan_id">
|
||||
<button name="edit" type="edit" icon="fa-edit" title="Edit" />
|
||||
</groupby>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_account_loan_lines_search" model="ir.ui.view">
|
||||
<field name="name">Loan Items</field>
|
||||
<field name="model">account.loan.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="loan_id" />
|
||||
<field name="partner_id" />
|
||||
<group expand='0' string='Group by...'>
|
||||
<filter
|
||||
string='Loan'
|
||||
name="group_by_loan_id"
|
||||
context="{'group_by': 'loan_id'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_lines_action" model="ir.actions.act_window">
|
||||
<field name="name">Loan Items</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.loan.line</field>
|
||||
<field name="view_mode">tree,pivot,graph</field>
|
||||
<field name="view_id" ref="account_loan_lines_view" />
|
||||
<field name="context">{'search_default_group_by_loan_id': 1}</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="account_loan_lines_menu"
|
||||
parent="loan_menu"
|
||||
sequence="20"
|
||||
name="Loan Items"
|
||||
action="account_loan_lines_action"
|
||||
/>
|
||||
</odoo>
|
||||
226
account_loan/views/account_loan_view.xml
Normal file
226
account_loan/views/account_loan_view.xml
Normal file
@@ -0,0 +1,226 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2011 Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
Copyright 2016 Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
<record id="account_loan_search" model="ir.ui.view">
|
||||
<field name="name">account.loan.tree</field>
|
||||
<field name="model">account.loan</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="partner_id" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_loan_tree" model="ir.ui.view">
|
||||
<field name="name">account.loan.tree</field>
|
||||
<field name="model">account.loan</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="partner_id" optional="hide" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="is_leasing" />
|
||||
<field name="state" />
|
||||
<field name="rate" optional="hide" />
|
||||
<field name="loan_amount" optional="hide" />
|
||||
<field name="pending_principal_amount" optional="hide" />
|
||||
<field name="periods" optional="hide" />
|
||||
<field name="start_date" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_loan_form" model="ir.ui.view">
|
||||
<field name="name">account.loan.form</field>
|
||||
<field name="model">account.loan</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="compute_lines" type="object" string="Compute items" />
|
||||
<button
|
||||
name="%(account_loan_post_action)d"
|
||||
invisible="state != 'draft'"
|
||||
type="action"
|
||||
string="Post"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button
|
||||
name="view_account_moves"
|
||||
class="oe_stat_button"
|
||||
icon="fa-bars"
|
||||
invisible="state == 'draft'"
|
||||
type="object"
|
||||
string="Moves"
|
||||
/>
|
||||
<button
|
||||
name="view_account_invoices"
|
||||
class="oe_stat_button"
|
||||
icon="fa-pencil-square-o"
|
||||
invisible="state == 'draft' or not is_leasing"
|
||||
type="object"
|
||||
string="Invoices"
|
||||
/>
|
||||
<button
|
||||
name="%(account_loan_pay_amount_action)d"
|
||||
class="oe_stat_button"
|
||||
icon="fa-arrow-down"
|
||||
invisible="state != 'posted'"
|
||||
type="action"
|
||||
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"
|
||||
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>
|
||||
<h1>
|
||||
<field name="name" readonly="state != 'draft'" />
|
||||
</h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="company_id" invisible="1" />
|
||||
<field
|
||||
name="company_id"
|
||||
options="{'no_create': True}"
|
||||
readonly="state != 'draft'"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field name="loan_type" readonly="state != 'draft'" />
|
||||
<field name="loan_amount" readonly="state != 'draft'" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="rate_type" readonly="state != 'draft'" />
|
||||
<field name="rate" />
|
||||
<field name="rate_period" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id" readonly="state != 'draft'" />
|
||||
<field name="start_date" readonly="state != 'draft'" />
|
||||
<field name="periods" readonly="state != 'draft'" />
|
||||
<field name="method_period" readonly="state != 'draft'" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_leasing" readonly="state != 'draft'" />
|
||||
<field name="round_on_end" readonly="state != 'draft'" />
|
||||
<field
|
||||
name="payment_on_first_period"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="state == 'draft'">
|
||||
<group>
|
||||
<field name="pending_principal_amount" />
|
||||
<field name="payment_amount" />
|
||||
<field name="interests_amount" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Items" id="items">
|
||||
<field
|
||||
name="line_ids"
|
||||
context="{'tree_view_ref': 'account_loan.account_loan_line_tree'}"
|
||||
/>
|
||||
</page>
|
||||
<page string="Accounts" id="accounting">
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="journal_id"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
<field
|
||||
name="short_term_loan_account_id"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
<field name="journal_type" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="long_term_loan_account_id"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
<field
|
||||
name="interest_expenses_account_id"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
<field name="currency_id" invisible="1" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Leasing" id="leasing" invisible="not is_leasing">
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="leased_asset_account_id"
|
||||
required="is_leasing"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
<field
|
||||
name="residual_amount"
|
||||
readonly="state != 'draft'"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="product_id" required="is_leasing" />
|
||||
<field
|
||||
name="interests_product_id"
|
||||
required="is_leasing"
|
||||
/>
|
||||
<field name="post_invoice" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="activity_ids" widget="mail_activity" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account_loan_action" model="ir.actions.act_window">
|
||||
<field name="name">Loans</field>
|
||||
<field name="res_model">account.loan</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="loan_menu"
|
||||
name="Loans"
|
||||
parent="account.menu_finance_entries"
|
||||
sequence="75"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="account_loan_menu"
|
||||
parent="loan_menu"
|
||||
sequence="10"
|
||||
name="Loans"
|
||||
action="account_loan_action"
|
||||
/>
|
||||
</odoo>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user