make new modul
This commit is contained in:
parent
56ccd84d63
commit
7c8cdfcd0c
Binary file not shown.
|
|
@ -0,0 +1,64 @@
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
||||||
|
|
||||||
|
def _account_loans_post_init(env):
|
||||||
|
# If in demo, import the demo amortization schedule in the loan
|
||||||
|
if env.ref('base.module_account_loans').demo:
|
||||||
|
_account_loans_import_loan_demo(
|
||||||
|
env,
|
||||||
|
env.ref('account_loans.account_loans_loan_demo1'),
|
||||||
|
env.ref('account_loans.account_loans_loan_demo_file_csv')
|
||||||
|
)
|
||||||
|
|
||||||
|
_account_loans_import_loan_demo(
|
||||||
|
env,
|
||||||
|
env.ref('account_loans.account_loans_loan_demo2'),
|
||||||
|
env.ref('account_loans.account_loans_loan_demo_file_xlsx')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _account_loans_add_date_column(csv_attachment):
|
||||||
|
# Modify the CSV such that one part of the loan lines are in the past (so their related
|
||||||
|
# generated entries are posted), and the other part in the future (so in draft)
|
||||||
|
data = io.StringIO()
|
||||||
|
writer = csv.writer(data, delimiter=',')
|
||||||
|
reader = csv.reader(io.StringIO(csv_attachment.raw.decode()), quotechar='"', delimiter=',')
|
||||||
|
current_date = fields.Date.today() - relativedelta(years=1)
|
||||||
|
for i, row in enumerate(reader):
|
||||||
|
if i == 0:
|
||||||
|
row.insert(0, 'Date')
|
||||||
|
else:
|
||||||
|
row.insert(0, current_date.strftime('%Y-%m-%d'))
|
||||||
|
current_date += relativedelta(months=1)
|
||||||
|
writer.writerow(row)
|
||||||
|
data.seek(0) # move offset back to beginning
|
||||||
|
generated_file = data.read()
|
||||||
|
data.close()
|
||||||
|
csv_attachment.raw = generated_file.encode()
|
||||||
|
return csv_attachment
|
||||||
|
|
||||||
|
|
||||||
|
def _account_loans_import_loan_demo(env, loan, attachment):
|
||||||
|
if attachment.mimetype == 'text/csv':
|
||||||
|
attachment = _account_loans_add_date_column(attachment)
|
||||||
|
action = loan.action_upload_amortization_schedule(attachment.id)
|
||||||
|
import_wizard = env['base_import.import'].browse(action.get('params', {}).get('context', {}).get('wizard_id'))
|
||||||
|
result = import_wizard.parse_preview({
|
||||||
|
'quoting': '"',
|
||||||
|
'separator': ',',
|
||||||
|
'date_format': '%Y-%m-%d',
|
||||||
|
'has_headers': True,
|
||||||
|
})
|
||||||
|
import_wizard.with_context(default_loan_id=loan.id).execute_import(
|
||||||
|
['date', 'principal', 'interest'],
|
||||||
|
[],
|
||||||
|
result["options"],
|
||||||
|
)
|
||||||
|
loan.action_file_uploaded()
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
'name': 'Loans Management',
|
||||||
|
'description': """
|
||||||
|
Loans management
|
||||||
|
=================
|
||||||
|
Keeps track of loans, and creates corresponding journal entries.
|
||||||
|
""",
|
||||||
|
'category': 'Accounting/Accounting',
|
||||||
|
'sequence': 32,
|
||||||
|
'depends': ['odex30_account_asset', 'base_import'],
|
||||||
|
'data': [
|
||||||
|
'security/account_loans_security.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
|
||||||
|
'wizard/account_loan_close_wizard.xml',
|
||||||
|
'wizard/account_loan_compute_wizard.xml',
|
||||||
|
|
||||||
|
'views/account_asset_views.xml',
|
||||||
|
'views/account_asset_group_views.xml',
|
||||||
|
'views/account_loan_views.xml',
|
||||||
|
'views/account_move_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'demo/account_loans_demo.xml',
|
||||||
|
],
|
||||||
|
'license': 'OEEL-1',
|
||||||
|
'auto_install': True,
|
||||||
|
'post_init_hook': '_account_loans_post_init',
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'account_loans/static/src/**/*',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="account_loans_loan_demo_file_csv" model="ir.attachment">
|
||||||
|
<field name="name">loan_amortization_demo.csv</field>
|
||||||
|
<field name="datas" type="base64" file="account_loans/demo/files/loan_amortization_demo.csv"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loans_loan_demo_file_xlsx" model="ir.attachment">
|
||||||
|
<field name="name">loan_amortization_demo.xlsx</field>
|
||||||
|
<field name="datas" type="base64" file="account_loans/demo/files/loan_amortization_demo.xlsx"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loans_journal_loan" model="account.journal">
|
||||||
|
<field name="name">Journal Loan Demo</field>
|
||||||
|
<field name="type">general</field>
|
||||||
|
<field name="code">LOAN</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loans_loan_demo1" model="account.loan">
|
||||||
|
<!-- Used in the __init__.py -> _account_loans_post_init-->
|
||||||
|
<field name="name">Loan Demo 1</field>
|
||||||
|
<field name="asset_group_id" ref="odex30_account_asset.account_asset_group_demo"/>
|
||||||
|
<field name="journal_id" ref="account_loans.account_loans_journal_loan"/>
|
||||||
|
<field name="long_term_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'liability_non_current'),
|
||||||
|
]"/>
|
||||||
|
<field name="short_term_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'liability_current'),
|
||||||
|
]"/>
|
||||||
|
<field name="expense_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'expense'),
|
||||||
|
('id', '!=', obj().env.user.company_id.account_journal_early_pay_discount_loss_account_id.id),
|
||||||
|
]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loans_loan_demo2" model="account.loan">
|
||||||
|
<!-- Used in the __init__.py -> _account_loans_post_init-->
|
||||||
|
<field name="name">Loan Demo 2</field>
|
||||||
|
<field name="asset_group_id" ref="odex30_account_asset.account_asset_group_demo"/>
|
||||||
|
<field name="journal_id" ref="account_loans.account_loans_journal_loan"/>
|
||||||
|
<field name="long_term_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'liability_non_current'),
|
||||||
|
]"/>
|
||||||
|
<field name="short_term_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'liability_current'),
|
||||||
|
]"/>
|
||||||
|
<field name="expense_account_id" model="account.account" search="[
|
||||||
|
('company_ids', '=', ref('base.main_company')),
|
||||||
|
('account_type', '=', 'expense'),
|
||||||
|
('id', '!=', obj().env.user.company_id.account_journal_early_pay_discount_loss_account_id.id),
|
||||||
|
]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
Principal,Interest
|
||||||
|
304.60,250.00
|
||||||
|
308.42,246.17
|
||||||
|
312.30,242.30
|
||||||
|
316.22,238.38
|
||||||
|
320.19,234.40
|
||||||
|
324.22,230.38
|
||||||
|
328.29,226.31
|
||||||
|
332.41,222.18
|
||||||
|
336.59,218.01
|
||||||
|
340.82,213.78
|
||||||
|
345.10,209.50
|
||||||
|
349.44,205.16
|
||||||
|
353.83,200.77
|
||||||
|
358.27,196.33
|
||||||
|
362.77,191.83
|
||||||
|
367.33,187.27
|
||||||
|
371.94,182.65
|
||||||
|
376.62,177.98
|
||||||
|
381.35,173.25
|
||||||
|
386.14,168.46
|
||||||
|
390.99,163.61
|
||||||
|
395.90,158.70
|
||||||
|
400.87,153.72
|
||||||
|
405.91,148.69
|
||||||
|
411.01,143.59
|
||||||
|
416.17,138.42
|
||||||
|
421.40,133.20
|
||||||
|
426.70,127.90
|
||||||
|
432.06,122.54
|
||||||
|
437.48,117.11
|
||||||
|
442.98,111.62
|
||||||
|
448.54,106.05
|
||||||
|
454.18,100.42
|
||||||
|
459.89,94.71
|
||||||
|
465.66,88.94
|
||||||
|
471.51,83.09
|
||||||
|
477.44,77.16
|
||||||
|
483.43,71.16
|
||||||
|
489.51,65.09
|
||||||
|
495.66,58.94
|
||||||
|
501.88,52.71
|
||||||
|
508.19,46.41
|
||||||
|
514.57,40.03
|
||||||
|
521.04,33.56
|
||||||
|
527.58,27.02
|
||||||
|
534.21,20.39
|
||||||
|
540.92,13.68
|
||||||
|
547.72,6.88
|
||||||
|
Binary file not shown.
|
|
@ -0,0 +1,961 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * account_loans
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-03-26 20:45+0000\n"
|
||||||
|
"PO-Revision-Date: 2025-03-26 20:45+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "1st amortization schedule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30a/360
|
||||||
|
msgid "30A/360"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30e/360
|
||||||
|
msgid "30E/360"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30e/360_isda
|
||||||
|
msgid "30E/360 ISDA"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30u/360
|
||||||
|
msgid "30U/360"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"duration == 1\">months</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"duration != 1\">month</span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"loan_term == 1\">years</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"loan_term != 1\">year</span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "<span class=\"oe_inline\">%</span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/360
|
||||||
|
msgid "A/360"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/365f
|
||||||
|
msgid "A/365F"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/a_afb
|
||||||
|
msgid "A/A AFB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/a_isda
|
||||||
|
msgid "A/A ISDA"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_needaction
|
||||||
|
msgid "Action Needed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__active
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__active
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
msgid "All draft entries after the"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Amortization schedule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__amount_borrowed
|
||||||
|
msgid "Amount Borrowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__amount_borrowed_difference
|
||||||
|
msgid "Amount Borrowed Difference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Apply"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_asset_group
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__asset_group_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_asset_group_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Asset Group"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_asset
|
||||||
|
msgid "Asset/Revenue Recognition"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__payment_end_of_month__at_anniversary
|
||||||
|
msgid "At Anniversary"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_attachment_count
|
||||||
|
msgid "Attachment Count"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__cancelled
|
||||||
|
msgid "Cancelled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__date
|
||||||
|
msgid "Close Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loan_close_wizard
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_close_wizard
|
||||||
|
msgid "Close Loan Wizard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__closed
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Closed Loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_close_wizard.py:0
|
||||||
|
msgid "Closed on the %(date)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__company_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__company_id
|
||||||
|
msgid "Company"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__compounding_method
|
||||||
|
msgid "Compounding Method"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-javascript
|
||||||
|
#: code:addons/account_loans/static/src/components/loans/file_upload.xml:0
|
||||||
|
msgid "Compute"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Compute New Loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Confirm"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__count_linked_assets
|
||||||
|
msgid "Count Linked Assets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset__count_linked_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset_group__count_linked_loans
|
||||||
|
msgid "Count Linked Loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__currency_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__currency_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__currency_id
|
||||||
|
msgid "Currency"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Current"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Discard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__draft
|
||||||
|
msgid "Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Draft & Running Loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Due"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__duration
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Duration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__duration_difference
|
||||||
|
msgid "Duration Difference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__end_date
|
||||||
|
msgid "End Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__payment_end_of_month__end_of_month
|
||||||
|
msgid "End of Month"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan_line__generated_move_ids
|
||||||
|
msgid "Entries that we generated from this loan line"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__expense_account_id
|
||||||
|
msgid "Expense Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__first_payment_date
|
||||||
|
msgid "First Payment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_follower_ids
|
||||||
|
msgid "Followers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_partner_ids
|
||||||
|
msgid "Followers (Partners)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__generated_move_ids
|
||||||
|
msgid "Generated Entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__generating_loan_line_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__generating_loan_line_id
|
||||||
|
msgid "Generating Loan Line"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Group By"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__has_message
|
||||||
|
msgid "Has Message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_needaction
|
||||||
|
msgid "If checked, new messages require your attention."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_error
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_sms_error
|
||||||
|
msgid "If checked, some messages have a delivery error."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__interest
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__interest
|
||||||
|
msgid "Interest"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__interest_difference
|
||||||
|
msgid "Interest Difference"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__interest_rate
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Interest Rate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Interest Rate must be between 0 and 100"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Interests"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_is_follower
|
||||||
|
msgid "Is Follower"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__is_loan_payment_move
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__is_loan_payment_move
|
||||||
|
msgid "Is Loan Payment Move"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__is_payment_move_posted
|
||||||
|
msgid "Is Payment Move Posted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__is_wrong_date
|
||||||
|
msgid "Is Wrong Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_journal
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__journal_id
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:account.journal,name:account_loans.account_loans_journal_loan
|
||||||
|
msgid "Journal Loan Demo"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_bank_statement_line__generating_loan_line_id
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_move__generating_loan_line_id
|
||||||
|
msgid "Line of the loan that generated this entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__linked_assets_ids
|
||||||
|
msgid "Linked Assets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_asset.py:0
|
||||||
|
msgid "Linked loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__loan_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_amount
|
||||||
|
msgid "Loan Amount"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Loan Amount must be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loan_compute_wizard
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_compute_wizard
|
||||||
|
msgid "Loan Compute Wizard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
msgid "Loan Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Loan Entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_line
|
||||||
|
msgid "Loan Line"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__line_ids
|
||||||
|
msgid "Loan Lines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Loan Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_term
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Loan Term"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Loan Term must be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Loan lines"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__display_name
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Loan name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.asset_group_form_view_inherit_loan
|
||||||
|
msgid "Loan(s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loans
|
||||||
|
#: model:ir.ui.menu,name:account_loans.menu_action_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loans_analysis
|
||||||
|
#: model:ir.ui.menu,name:account_loans.menu_action_loans_analysis
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_pivot_view
|
||||||
|
msgid "Loans Analysis"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__long_term_account_id
|
||||||
|
msgid "Long Term Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__long_term_theoretical_balance
|
||||||
|
msgid "Long-Term"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.actions.act_window,help:account_loans.action_view_account_loans
|
||||||
|
msgid "Manage Your Acquired Loans with Automated Adjustments."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_error
|
||||||
|
msgid "Message Delivery error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_ids
|
||||||
|
msgid "Messages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_journal__loan_properties_definition
|
||||||
|
msgid "Model Properties"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_name
|
||||||
|
msgid "Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__nb_posted_entries
|
||||||
|
msgid "Nb Posted Entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_needaction_counter
|
||||||
|
msgid "Number of Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_error_counter
|
||||||
|
msgid "Number of errors"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_needaction_counter
|
||||||
|
msgid "Number of messages requiring action"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_error_counter
|
||||||
|
msgid "Number of messages with delivery error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_move.py:0
|
||||||
|
msgid "Original Loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__outstanding_balance
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__outstanding_balance
|
||||||
|
msgid "Outstanding Balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__payment_end_of_month
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__payment
|
||||||
|
msgid "Payment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Payments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Please add a name before computing the loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Posted Entries"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__preview
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Preview"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__principal
|
||||||
|
msgid "Principal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Principal & Interest"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Principals"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__loan_properties
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Properties"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__rating_ids
|
||||||
|
msgid "Ratings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Reclassification LT - ST"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Related Asset(s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_move_form_inherit_loan
|
||||||
|
msgid "Related Loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_asset_form_inherit_loan
|
||||||
|
msgid "Related Loan(s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset__linked_loans_ids
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset_group__linked_loan_ids
|
||||||
|
msgid "Related Loans"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Reversal reclassification LT - ST"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__running
|
||||||
|
msgid "Running"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_sms_error
|
||||||
|
msgid "SMS Delivery error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Search Loan"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Set to Draft"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.actions.act_window,help:account_loans.action_view_account_loans
|
||||||
|
msgid ""
|
||||||
|
"Set up your amortization schedule, or import it, and let Odoo handle the "
|
||||||
|
"monthly interest and principal adjustments automatically."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__short_term_account_id
|
||||||
|
msgid "Short Term Account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__short_term_theoretical_balance
|
||||||
|
msgid "Short-Term"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__skip_until_date
|
||||||
|
msgid "Skip until"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__start_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__start_date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Start Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__state
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_state
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "The First Payment Date must be before the end of the loan."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The amount borrowed must be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The duration must be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The interest must be positive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan accounts should be set."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid ""
|
||||||
|
"The loan amount %(loan_amount)s should be equal to the sum of the "
|
||||||
|
"principals: %(principal_sum)s (difference %(principal_difference)s)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan date should be earlier than the loan lines date."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan duration should be equal to the number of loan lines."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid ""
|
||||||
|
"The loan interest should be equal to the sum of the loan lines interest."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan journal should be set."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan name should be set."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "This entry has been %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "This entry has been reversed from %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Total Amounts Borrowed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Total Outstanding Balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total interests"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total payments"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total principals"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-javascript
|
||||||
|
#: code:addons/account_loans/static/src/components/loans/file_upload.xml:0
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Uploaded file"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__skip_until_date
|
||||||
|
msgid ""
|
||||||
|
"Upon confirmation of the loan, Odoo will ignore the loan lines that are up "
|
||||||
|
"to this date (included) and not create entries for them. This is useful if "
|
||||||
|
"you have already manually created entries prior to the creation of this "
|
||||||
|
"loan."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__website_message_ids
|
||||||
|
msgid "Website Messages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__website_message_ids
|
||||||
|
msgid "Website communication history"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
msgid "will be deleted and the loan will be marked as closed."
|
||||||
|
msgstr ""
|
||||||
|
|
@ -0,0 +1,977 @@
|
||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * account_loans
|
||||||
|
#
|
||||||
|
# Translators:
|
||||||
|
# Wil Odoo, 2025
|
||||||
|
# Malaz Abuidris <msea@odoo.com>, 2025
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 18.0+e\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-03-26 20:45+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-09-25 09:44+0000\n"
|
||||||
|
"Last-Translator: Malaz Abuidris <msea@odoo.com>, 2025\n"
|
||||||
|
"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Language: ar\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "1st amortization schedule"
|
||||||
|
msgstr "جدول الاستهلاك الأول "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30a/360
|
||||||
|
msgid "30A/360"
|
||||||
|
msgstr "30A/360"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30e/360
|
||||||
|
msgid "30E/360"
|
||||||
|
msgstr "30E/360"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30e/360_isda
|
||||||
|
msgid "30E/360 ISDA"
|
||||||
|
msgstr "30E/360 ISDA"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__30u/360
|
||||||
|
msgid "30U/360"
|
||||||
|
msgstr "30U/360"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"duration == 1\">months</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"duration != 1\">month</span>"
|
||||||
|
msgstr ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"duration == 1\">شهور</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"duration != 1\">شهر</span>"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"loan_term == 1\">years</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"loan_term != 1\">year</span>"
|
||||||
|
msgstr ""
|
||||||
|
"<span class=\"oe_inline\" invisible=\"loan_term == 1\">سنوات</span>\n"
|
||||||
|
" <span class=\"oe_inline\" invisible=\"loan_term != 1\">سنة</span> "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "<span class=\"oe_inline\">%</span>"
|
||||||
|
msgstr "<span class=\"oe_inline\">%</span>"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/360
|
||||||
|
msgid "A/360"
|
||||||
|
msgstr "A/360"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/365f
|
||||||
|
msgid "A/365F"
|
||||||
|
msgstr "A/365F"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/a_afb
|
||||||
|
msgid "A/A AFB"
|
||||||
|
msgstr "A/A AFB"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__compounding_method__a/a_isda
|
||||||
|
msgid "A/A ISDA"
|
||||||
|
msgstr "A/A ISDA"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_needaction
|
||||||
|
msgid "Action Needed"
|
||||||
|
msgstr "إجراء مطلوب"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__active
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__active
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "نشط"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
msgid "All draft entries after the"
|
||||||
|
msgstr "تمت إزالة موصل eBay. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Amortization schedule"
|
||||||
|
msgstr "جدول الاستهلاك "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__amount_borrowed
|
||||||
|
msgid "Amount Borrowed"
|
||||||
|
msgstr "المبلغ المقترض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__amount_borrowed_difference
|
||||||
|
msgid "Amount Borrowed Difference"
|
||||||
|
msgstr "فرق المبلغ المقترض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Apply"
|
||||||
|
msgstr "تطبيق"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_asset_group
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__asset_group_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_asset_group_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Asset Group"
|
||||||
|
msgstr "مجموعة الأصول "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_asset
|
||||||
|
msgid "Asset/Revenue Recognition"
|
||||||
|
msgstr "إثبات الأصل/الإيرادات "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__payment_end_of_month__at_anniversary
|
||||||
|
msgid "At Anniversary"
|
||||||
|
msgstr "في الذكرى السنوية "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_attachment_count
|
||||||
|
msgid "Attachment Count"
|
||||||
|
msgstr "عدد المرفقات"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Balance"
|
||||||
|
msgstr "الرصيد"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "إلغاء"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__cancelled
|
||||||
|
msgid "Cancelled"
|
||||||
|
msgstr "تم الإلغاء "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Close"
|
||||||
|
msgstr "إغلاق"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__date
|
||||||
|
msgid "Close Date"
|
||||||
|
msgstr "تاريخ الإغلاق"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loan_close_wizard
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_close_wizard
|
||||||
|
msgid "Close Loan Wizard"
|
||||||
|
msgstr "معالج إغلاق القروض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__closed
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr "مغلق"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Closed Loans"
|
||||||
|
msgstr "القروض المغلقة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_close_wizard.py:0
|
||||||
|
msgid "Closed on the %(date)s"
|
||||||
|
msgstr "تم إغلاقه في %(date)s "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__company_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__company_id
|
||||||
|
msgid "Company"
|
||||||
|
msgstr "الشركة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__compounding_method
|
||||||
|
msgid "Compounding Method"
|
||||||
|
msgstr "طريقة التركيب "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-javascript
|
||||||
|
#: code:addons/account_loans/static/src/components/loans/file_upload.xml:0
|
||||||
|
msgid "Compute"
|
||||||
|
msgstr "احتساب "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Compute New Loan"
|
||||||
|
msgstr "حساب قرض جديد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Confirm"
|
||||||
|
msgstr "تأكيد"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__count_linked_assets
|
||||||
|
msgid "Count Linked Assets"
|
||||||
|
msgstr "عدّ الأصول المرتبطة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset__count_linked_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset_group__count_linked_loans
|
||||||
|
msgid "Count Linked Loans"
|
||||||
|
msgstr "عدّ القروض المرتبطة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr "أنشئ بواسطة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__create_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr "أنشئ في"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__currency_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__currency_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__currency_id
|
||||||
|
msgid "Currency"
|
||||||
|
msgstr "العملة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Current"
|
||||||
|
msgstr "الحالي "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "التاريخ"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Discard"
|
||||||
|
msgstr "إهمال "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__display_name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "اسم العرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__draft
|
||||||
|
msgid "Draft"
|
||||||
|
msgstr "مسودة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Draft & Running Loans"
|
||||||
|
msgstr "مسودات القروض والقروض الجارية "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Due"
|
||||||
|
msgstr "مستحق"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__duration
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Duration"
|
||||||
|
msgstr "المدة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__duration_difference
|
||||||
|
msgid "Duration Difference"
|
||||||
|
msgstr "فرق المدة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__end_date
|
||||||
|
msgid "End Date"
|
||||||
|
msgstr "تاريخ الانتهاء"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan_compute_wizard__payment_end_of_month__end_of_month
|
||||||
|
msgid "End of Month"
|
||||||
|
msgstr "نهاية الشهر "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan_line__generated_move_ids
|
||||||
|
msgid "Entries that we generated from this loan line"
|
||||||
|
msgstr "الإدخالات التي قمنا بإنشائها من بند القرض هذا "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__expense_account_id
|
||||||
|
msgid "Expense Account"
|
||||||
|
msgstr "حساب النفقات "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__first_payment_date
|
||||||
|
msgid "First Payment"
|
||||||
|
msgstr "الدفعة الأولى "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_follower_ids
|
||||||
|
msgid "Followers"
|
||||||
|
msgstr "المتابعين"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_partner_ids
|
||||||
|
msgid "Followers (Partners)"
|
||||||
|
msgstr "المتابعين (الشركاء) "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__generated_move_ids
|
||||||
|
msgid "Generated Entries"
|
||||||
|
msgstr "القيود المُنشأة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__generating_loan_line_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__generating_loan_line_id
|
||||||
|
msgid "Generating Loan Line"
|
||||||
|
msgstr "إنشاء بند القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Group By"
|
||||||
|
msgstr "التجميع حسب "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__has_message
|
||||||
|
msgid "Has Message"
|
||||||
|
msgstr "يحتوي على رسالة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "المُعرف"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_needaction
|
||||||
|
msgid "If checked, new messages require your attention."
|
||||||
|
msgstr "إذا كان محددًا، فهناك رسائل جديدة عليك رؤيتها. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_error
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_sms_error
|
||||||
|
msgid "If checked, some messages have a delivery error."
|
||||||
|
msgstr "إذا كان محددًا، فقد حدث خطأ في تسليم بعض الرسائل."
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__interest
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__interest
|
||||||
|
msgid "Interest"
|
||||||
|
msgstr "الفائدة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__interest_difference
|
||||||
|
msgid "Interest Difference"
|
||||||
|
msgstr "فرق الفوائد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__interest_rate
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Interest Rate"
|
||||||
|
msgstr "نسبة الفوائد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Interest Rate must be between 0 and 100"
|
||||||
|
msgstr "يجب أن تكون نسبة الفوائد بين 0 و 100 "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Interests"
|
||||||
|
msgstr "الفوائد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_is_follower
|
||||||
|
msgid "Is Follower"
|
||||||
|
msgstr "متابع"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__is_loan_payment_move
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__is_loan_payment_move
|
||||||
|
msgid "Is Loan Payment Move"
|
||||||
|
msgstr "حركة سداد القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__is_payment_move_posted
|
||||||
|
msgid "Is Payment Move Posted"
|
||||||
|
msgstr "تم ترحيل حركة الدفع "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__is_wrong_date
|
||||||
|
msgid "Is Wrong Date"
|
||||||
|
msgstr "التاريخ غير صحيح "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_journal
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__journal_id
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr "دفتر اليومية"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "قيد اليومية"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:account.journal,name:account_loans.account_loans_journal_loan
|
||||||
|
msgid "Journal Loan Demo"
|
||||||
|
msgstr "العرض التوضيحي لقرض في دفتر اليومية "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr "آخر تحديث بواسطة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__write_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr "آخر تحديث في"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_bank_statement_line__generating_loan_line_id
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_move__generating_loan_line_id
|
||||||
|
msgid "Line of the loan that generated this entry"
|
||||||
|
msgstr "بند القرض الذي أدى إلى إنشاء هذا القيد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__linked_assets_ids
|
||||||
|
msgid "Linked Assets"
|
||||||
|
msgstr "الأصول المرتبطة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_asset.py:0
|
||||||
|
msgid "Linked loans"
|
||||||
|
msgstr "القروض المربطة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_bank_statement_line__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_close_wizard__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_id
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_move__loan_id
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Loan"
|
||||||
|
msgstr "قرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_amount
|
||||||
|
msgid "Loan Amount"
|
||||||
|
msgstr "مبلغ القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Loan Amount must be positive"
|
||||||
|
msgstr "يجب أن يكون مبلغ القرض موجباً "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loan_compute_wizard
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_compute_wizard
|
||||||
|
msgid "Loan Compute Wizard"
|
||||||
|
msgstr "معالج حساب القروض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
msgid "Loan Date"
|
||||||
|
msgstr "تاريخ القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Loan Entries"
|
||||||
|
msgstr "قيود القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model,name:account_loans.model_account_loan_line
|
||||||
|
msgid "Loan Line"
|
||||||
|
msgstr "بند القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__line_ids
|
||||||
|
msgid "Loan Lines"
|
||||||
|
msgstr "بنود القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Loan Settings"
|
||||||
|
msgstr "إعدادات القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__loan_term
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Loan Term"
|
||||||
|
msgstr "مدة القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "Loan Term must be positive"
|
||||||
|
msgstr "يجب أن تكون مدة القرض قيمة موجبة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Loan lines"
|
||||||
|
msgstr "بنود القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__display_name
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Loan name"
|
||||||
|
msgstr "اسم القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.asset_group_form_view_inherit_loan
|
||||||
|
msgid "Loan(s)"
|
||||||
|
msgstr "القروض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loans
|
||||||
|
#: model:ir.ui.menu,name:account_loans.menu_action_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Loans"
|
||||||
|
msgstr "القروض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.actions.act_window,name:account_loans.action_view_account_loans_analysis
|
||||||
|
#: model:ir.ui.menu,name:account_loans.menu_action_loans_analysis
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_pivot_view
|
||||||
|
msgid "Loans Analysis"
|
||||||
|
msgstr "تحليل القروض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__long_term_account_id
|
||||||
|
msgid "Long Term Account"
|
||||||
|
msgstr "حساب طويل الأجل "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__long_term_theoretical_balance
|
||||||
|
msgid "Long-Term"
|
||||||
|
msgstr "طويل لأجل "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.actions.act_window,help:account_loans.action_view_account_loans
|
||||||
|
msgid "Manage Your Acquired Loans with Automated Adjustments."
|
||||||
|
msgstr "قم بإدارة قروضك التي حصلت عليها من خلال التعديلات الآلية. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_error
|
||||||
|
msgid "Message Delivery error"
|
||||||
|
msgstr "خطأ في تسليم الرسائل"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_ids
|
||||||
|
msgid "Messages"
|
||||||
|
msgstr "الرسائل"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_journal__loan_properties_definition
|
||||||
|
msgid "Model Properties"
|
||||||
|
msgstr "خصائص النموذج "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__name
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_name
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "الاسم"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__nb_posted_entries
|
||||||
|
msgid "Nb Posted Entries"
|
||||||
|
msgstr "عدد القيود المُرحّلَة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_needaction_counter
|
||||||
|
msgid "Number of Actions"
|
||||||
|
msgstr "عدد الإجراءات"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_error_counter
|
||||||
|
msgid "Number of errors"
|
||||||
|
msgstr "عدد الأخطاء "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_needaction_counter
|
||||||
|
msgid "Number of messages requiring action"
|
||||||
|
msgstr "عدد الرسائل التي تتطلب اتخاذ إجراء"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__message_has_error_counter
|
||||||
|
msgid "Number of messages with delivery error"
|
||||||
|
msgstr "عدد الرسائل الحادث بها خطأ في التسليم"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_move.py:0
|
||||||
|
msgid "Original Loan"
|
||||||
|
msgstr "القرض الأصلي "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__outstanding_balance
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__outstanding_balance
|
||||||
|
msgid "Outstanding Balance"
|
||||||
|
msgstr "المبلغ المستحق "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__payment_end_of_month
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__payment
|
||||||
|
msgid "Payment"
|
||||||
|
msgstr "الدفع "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Payments"
|
||||||
|
msgstr "المدفوعات"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Please add a name before computing the loan"
|
||||||
|
msgstr "يرجى إضافة اسم قبل حساب مبلغ القرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Posted Entries"
|
||||||
|
msgstr "القيود المُرحّلة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__preview
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard
|
||||||
|
msgid "Preview"
|
||||||
|
msgstr "معاينة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__principal
|
||||||
|
msgid "Principal"
|
||||||
|
msgstr "أصل الدَّيْن "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Principal & Interest"
|
||||||
|
msgstr "أصل الدين والفوائد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Principals"
|
||||||
|
msgstr "أصول الديون "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__loan_properties
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Properties"
|
||||||
|
msgstr "الخصائص "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__rating_ids
|
||||||
|
msgid "Ratings"
|
||||||
|
msgstr "التقييمات "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Reclassification LT - ST"
|
||||||
|
msgstr "إعادة التصنيف LT - ST "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Related Asset(s)"
|
||||||
|
msgstr "الأصول ذات الصلة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_move_form_inherit_loan
|
||||||
|
msgid "Related Loan"
|
||||||
|
msgstr "القرض ذو الصلة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_asset_form_inherit_loan
|
||||||
|
msgid "Related Loan(s)"
|
||||||
|
msgstr "القروض ذات الصلة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset__linked_loans_ids
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_asset_group__linked_loan_ids
|
||||||
|
msgid "Related Loans"
|
||||||
|
msgstr "القرض ذو الصلة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "إعادة الضبط "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Reversal reclassification LT - ST"
|
||||||
|
msgstr "إعادة التصنيف العكسي LT - ST "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields.selection,name:account_loans.selection__account_loan__state__running
|
||||||
|
msgid "Running"
|
||||||
|
msgstr "جاري"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__message_has_sms_error
|
||||||
|
msgid "SMS Delivery error"
|
||||||
|
msgstr "خطأ في تسليم الرسائل النصية القصيرة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_search_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_search_view
|
||||||
|
msgid "Search Loan"
|
||||||
|
msgstr "البحث عن قرض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_form_view
|
||||||
|
msgid "Set to Draft"
|
||||||
|
msgstr "تعيين كمسودة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.actions.act_window,help:account_loans.action_view_account_loans
|
||||||
|
msgid ""
|
||||||
|
"Set up your amortization schedule, or import it, and let Odoo handle the "
|
||||||
|
"monthly interest and principal adjustments automatically."
|
||||||
|
msgstr ""
|
||||||
|
"قم بإعداد جدول الاستهلاك الخاص بك، أو قم باستيراده ودع أودو يتعامل مع "
|
||||||
|
"تعديلات الفوائد الشهرية وأصل الدين تلقائياً. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__short_term_account_id
|
||||||
|
msgid "Short Term Account"
|
||||||
|
msgstr "حساب قصير الأجل "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__short_term_theoretical_balance
|
||||||
|
msgid "Short-Term"
|
||||||
|
msgstr "قصير الأجل "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__skip_until_date
|
||||||
|
msgid "Skip until"
|
||||||
|
msgstr "تخطي إلى "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__start_date
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_compute_wizard__start_date
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Start Date"
|
||||||
|
msgstr "تاريخ البدء "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__state
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan_line__loan_state
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "الحالة"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/wizard/account_loan_compute_wizard.py:0
|
||||||
|
msgid "The First Payment Date must be before the end of the loan."
|
||||||
|
msgstr "يجب أن يكون تاريخ القسط الأول قبل نهاية القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The amount borrowed must be positive"
|
||||||
|
msgstr "يجب أن يكون المبلغ المقترض موجباً "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The duration must be positive"
|
||||||
|
msgstr "يجب أن تكون المدة موجبة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The interest must be positive"
|
||||||
|
msgstr "يجب أن تكون الفوائد قيمة موجبة "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan accounts should be set."
|
||||||
|
msgstr "يجب أن يتم تعيين حسابات القروض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid ""
|
||||||
|
"The loan amount %(loan_amount)s should be equal to the sum of the "
|
||||||
|
"principals: %(principal_sum)s (difference %(principal_difference)s)"
|
||||||
|
msgstr ""
|
||||||
|
"يجب أن يكون مبلغ القرض %(loan_amount)s مساوياً لمجموع أصول الدين: "
|
||||||
|
"%(principal_sum)s(الفرق %(principal_difference)s) "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan date should be earlier than the loan lines date."
|
||||||
|
msgstr "يجب أن يكون تاريخ القرض قبل تاريخ بنود القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan duration should be equal to the number of loan lines."
|
||||||
|
msgstr "يجب أن تكون مدة القرض مساوية لعدد بنود القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid ""
|
||||||
|
"The loan interest should be equal to the sum of the loan lines interest."
|
||||||
|
msgstr "يجب أن تكون فائدة القرض مساوية لمجموع فوائد بنود القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan journal should be set."
|
||||||
|
msgstr "يجب أن يتم تعيين دفتر يومية القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "The loan name should be set."
|
||||||
|
msgstr "يجب أن يتم تعيين اسم القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "This entry has been %s"
|
||||||
|
msgstr "هذا القيد %s "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "This entry has been reversed from %s"
|
||||||
|
msgstr "لقد تم عكس هذا القيد من %s "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Total Amounts Borrowed"
|
||||||
|
msgstr "إجمالي المبلغ المقترض "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_list_view
|
||||||
|
msgid "Total Outstanding Balance"
|
||||||
|
msgstr "إجمالي المبلغ المستحق "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total interests"
|
||||||
|
msgstr "إجمالي الفوائد "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total payments"
|
||||||
|
msgstr "إجمالي المدفوعات "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.account_loan_line_list_view
|
||||||
|
msgid "Total principals"
|
||||||
|
msgstr "إجمالي أصول الديون "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-javascript
|
||||||
|
#: code:addons/account_loans/static/src/components/loans/file_upload.xml:0
|
||||||
|
msgid "Upload"
|
||||||
|
msgstr "رفع"
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/account_loans/models/account_loan.py:0
|
||||||
|
msgid "Uploaded file"
|
||||||
|
msgstr "الملف الذي تم رفعه "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__skip_until_date
|
||||||
|
msgid ""
|
||||||
|
"Upon confirmation of the loan, Odoo will ignore the loan lines that are up "
|
||||||
|
"to this date (included) and not create entries for them. This is useful if "
|
||||||
|
"you have already manually created entries prior to the creation of this "
|
||||||
|
"loan."
|
||||||
|
msgstr ""
|
||||||
|
"عند تأكيد القرض، سوف يتجاهل أودو بنود القرض حتى هذا التاريخ ( شاملة لهذا "
|
||||||
|
"التاريخ) ولن يقوم بإنشاء قيود لها. يكون ذلك مفيداً إذا كنت قد قمت بالفعل "
|
||||||
|
"بإنشاء قيود يدوياً قبل إنشاء هذا القرض. "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,field_description:account_loans.field_account_loan__website_message_ids
|
||||||
|
msgid "Website Messages"
|
||||||
|
msgstr "رسائل الموقع الإلكتروني "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model:ir.model.fields,help:account_loans.field_account_loan__website_message_ids
|
||||||
|
msgid "Website communication history"
|
||||||
|
msgstr "سجل تواصل الموقع الإلكتروني "
|
||||||
|
|
||||||
|
#. module: account_loans
|
||||||
|
#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_close_wizard
|
||||||
|
msgid "will be deleted and the loan will be marked as closed."
|
||||||
|
msgstr "سيتم حذفه وسيتم وضع علامة على القرض على أنه مغلق. "
|
||||||
|
|
@ -0,0 +1,583 @@
|
||||||
|
|
||||||
|
# Disable Ruff as we simply import the code from pyloan without any modifications
|
||||||
|
# ruff: noqa
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import calendar as cal
|
||||||
|
import collections
|
||||||
|
from decimal import Decimal
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
Payment = collections.namedtuple('Payment',
|
||||||
|
['date', 'payment_amount', 'interest_amount', 'principal_amount',
|
||||||
|
'special_principal_amount', 'total_principal_amount',
|
||||||
|
'loan_balance_amount'])
|
||||||
|
|
||||||
|
Special_Payment = collections.namedtuple('Special_Payment', ['payment_amount', 'first_payment_date',
|
||||||
|
'special_payment_term',
|
||||||
|
'annual_payments'])
|
||||||
|
Loan_Summary = collections.namedtuple('Loan_Summary', ['loan_amount', 'total_payment_amount',
|
||||||
|
'total_principal_amount',
|
||||||
|
'total_interest_amount',
|
||||||
|
'residual_loan_balance',
|
||||||
|
'repayment_to_principal'])
|
||||||
|
|
||||||
|
|
||||||
|
class Loan(object):
|
||||||
|
|
||||||
|
def __init__(self, loan_amount, interest_rate, loan_term, start_date, payment_amount=None,
|
||||||
|
first_payment_date=None, payment_end_of_month=True, annual_payments=12,
|
||||||
|
interest_only_period=0, compounding_method='30E/360', loan_type='annuity'):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validtion for attribute loan_amount
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(loan_amount, int) or isinstance(loan_amount, float):
|
||||||
|
if loan_amount < 0:
|
||||||
|
raise ValueError('Variable LOAN_AMMOUNT can only be non-negative.')
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'Variable LOAN_AMOUNT can only be of type integer or float, both non-negative.')
|
||||||
|
|
||||||
|
# handle exceptions for loan_amount
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.loan_amount = Decimal(str(loan_amount))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute interet_rate
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(interest_rate, int) or isinstance(interest_rate, float):
|
||||||
|
if interest_rate < 0:
|
||||||
|
raise ValueError('Variable INTEREST_RATE can only be non-negative.')
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'Variable INTEREST_RATE can only be of type integer or float, both non-negative.')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.interest_rate = Decimal(str(interest_rate / 100)).quantize(Decimal(str(0.0001)))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute loan_term
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(loan_term, int):
|
||||||
|
if loan_term < 1:
|
||||||
|
raise ValueError(
|
||||||
|
'Variable LOAN_TERM can only be integers greater or equal to 1.')
|
||||||
|
else:
|
||||||
|
raise TypeError('Variable LOAN_TERM can only be of type integer.')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.loan_term = loan_term
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute payment_amount
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if payment_amount is None:
|
||||||
|
pass
|
||||||
|
elif payment_amount is not None and (
|
||||||
|
isinstance(payment_amount, int) or isinstance(payment_amount, float)):
|
||||||
|
if payment_amount < 0:
|
||||||
|
raise ValueError('Variable PAYMENT_AMOUNT can only be non-negative.')
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'Variable PAYMENT_AMOUNT can only be of type integer or float, both non-negative.')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.payment_amount = payment_amount
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute start_date
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if start_date is None:
|
||||||
|
raise TypeError('Varable START_DATE must by of type date with format YYYY-MM-DD')
|
||||||
|
elif bool(dt.datetime.strptime(start_date, '%Y-%m-%d')) is False:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.start_date = dt.datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute first_paymnt_date
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if first_payment_date is None:
|
||||||
|
pass
|
||||||
|
elif bool(dt.datetime.strptime(first_payment_date, '%Y-%m-%d')) is False:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.first_payment_date = dt.datetime.strptime(first_payment_date,
|
||||||
|
'%Y-%m-%d') if first_payment_date is not None else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.first_payment_date is None:
|
||||||
|
pass
|
||||||
|
elif self.start_date > self.first_payment_date:
|
||||||
|
raise ValueError('FIRST_PAYMENT_DATE cannot be before START_DATE')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute payment_end_of_month
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if not isinstance(payment_end_of_month, bool):
|
||||||
|
raise TypeError(
|
||||||
|
'Variable PAYMENT_END_OF_MONTH can only be of type boolean (either True or False)')
|
||||||
|
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.payment_end_of_month = payment_end_of_month
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute annual_payments
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(annual_payments, int):
|
||||||
|
if annual_payments not in [12, 4, 2, 1]:
|
||||||
|
raise ValueError(
|
||||||
|
'Attribute ANNUAL_PAYMENTS must be either set to 12, 4, 2 or 1.')
|
||||||
|
else:
|
||||||
|
raise TypeError('Attribute ANNUAL_PAYMENTS must be of type integer.')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.annual_payments = annual_payments
|
||||||
|
|
||||||
|
'''
|
||||||
|
Setting of no_of_payments and delta_dt if loan_term and annual_payments are set.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'loan_term') is False or hasattr(self, 'annual_payments') is False:
|
||||||
|
print(self.loan_term)
|
||||||
|
print(self.annual_payments)
|
||||||
|
raise ValueError(
|
||||||
|
'Please make sure that LOAN_TERM and/or ANNUAL_PAYMENTS were correctly defined.11')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.no_of_payments = self.loan_term * self.annual_payments
|
||||||
|
self.delta_dt = Decimal(str(12 / self.annual_payments))
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute interest_only_period
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(interest_only_period, int):
|
||||||
|
if interest_only_period < 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Attribute INTEREST_ONLY_PERIOD must be greater or equal to 0.')
|
||||||
|
elif hasattr(self, 'no_of_payments') is False:
|
||||||
|
raise ValueError(
|
||||||
|
'Please make sure that LOAN_TERM and/or ANNUAL_PAYMENTS were correctly defined.')
|
||||||
|
elif hasattr(self,
|
||||||
|
'no_of_payments') is True and self.no_of_payments - interest_only_period < 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Attribute INTEREST_ONLY_PERIOD is greater than product of LOAN_TERM and ANNUAL_PAYMENTS.')
|
||||||
|
else:
|
||||||
|
raise TypeError('Attribute INTEREST_ONLY_PERIOD must be of type integer.')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.interest_only_period = interest_only_period
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute compounding_method
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(compounding_method, str):
|
||||||
|
if compounding_method not in ['30A/360', '30U/360', '30E/360', '30E/360 ISDA',
|
||||||
|
'A/360', 'A/365F', 'A/A ISDA', 'A/A AFB']:
|
||||||
|
raise ValueError(
|
||||||
|
'Attribute COMPOUNDING_METHOD must be set to one of the following: 30A/360, 30U/360, 30E/360, 30E/360 ISDA, A/360, A/365F, A/A ISDA, A/A AFB.')
|
||||||
|
else:
|
||||||
|
raise TypeError('Attribute COMPOUNDING_METHOD must be of type string')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.compounding_method = compounding_method
|
||||||
|
|
||||||
|
'''
|
||||||
|
Input validation for attribute loan_type
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
if isinstance(loan_type, str):
|
||||||
|
if loan_type not in ['annuity', 'linear', 'interest-only']:
|
||||||
|
raise ValueError(
|
||||||
|
'Attribute LOAN_TYPE must be either set to annuity or linear or interest-only.')
|
||||||
|
else:
|
||||||
|
raise TypeError('Attribute LOAN_TYPE must be of type string')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
except TypeError as typ_e:
|
||||||
|
print(typ_e)
|
||||||
|
else:
|
||||||
|
self.loan_type = loan_type
|
||||||
|
|
||||||
|
# define non-input variables
|
||||||
|
self.special_payments = []
|
||||||
|
self.special_payments_schedule = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _quantize(amount):
|
||||||
|
return Decimal(str(amount)).quantize(Decimal(str(0.01)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_day_count(dt1, dt2, method, eom=False):
|
||||||
|
|
||||||
|
def get_julian_day_number(y, m, d):
|
||||||
|
julian_day_count = (1461 * (y + 4800 + (m - 14) / 12)) / 4 + (
|
||||||
|
367 * (m - 2 - 12 * ((m - 14) / 12))) / 12 - (
|
||||||
|
3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4 + d - 32075
|
||||||
|
return julian_day_count
|
||||||
|
|
||||||
|
y1, m1, d1 = dt1.year, dt1.month, dt1.day
|
||||||
|
y2, m2, d2 = dt2.year, dt2.month, dt2.day
|
||||||
|
dt1_eom_day = cal.monthrange(y1, m1)[1]
|
||||||
|
dt2_eom_day = cal.monthrange(y2, m2)[1]
|
||||||
|
|
||||||
|
if method in {'30A/360', '30U/360', '30E/360', '30E/360 ISDA'}:
|
||||||
|
if method == '30A/360':
|
||||||
|
d1 = min(d1, 30)
|
||||||
|
d2 = min(d2, 30) if d1 == 30 else d2
|
||||||
|
if method == '30U/360':
|
||||||
|
if eom and m1 == 2 and d1 == dt1_eom_day and m2 == 2 and d2 == dt2_eom_day:
|
||||||
|
d2 = 30
|
||||||
|
if eom and m1 == 2 and d1 == dt1_eom_day:
|
||||||
|
d1 = 30
|
||||||
|
if d2 == 31 and d1 >= 30:
|
||||||
|
d2 = 30
|
||||||
|
if d1 == 31:
|
||||||
|
d1 = 30
|
||||||
|
if method == '30E/360':
|
||||||
|
if d1 == 31:
|
||||||
|
d1 = 30
|
||||||
|
if d2 == 31:
|
||||||
|
d2 = 30
|
||||||
|
if method == '30E/360 ISDA':
|
||||||
|
if d1 == dt1_eom_day:
|
||||||
|
d1 = 30
|
||||||
|
if d2 == dt2_eom_day and m2 != 2:
|
||||||
|
d2 = 30
|
||||||
|
|
||||||
|
day_count = (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1))
|
||||||
|
year_days = 360
|
||||||
|
|
||||||
|
if method == 'A/365F':
|
||||||
|
day_count = (dt2 - dt1).days
|
||||||
|
year_days = 365
|
||||||
|
|
||||||
|
if method == 'A/360':
|
||||||
|
day_count = (dt2 - dt1).days
|
||||||
|
year_days = 360
|
||||||
|
|
||||||
|
if method in {'A/A ISDA', 'A/A AFB'}:
|
||||||
|
djn_dt1 = get_julian_day_number(y1, m1, d1)
|
||||||
|
djn_dt2 = get_julian_day_number(y2, m2, d2)
|
||||||
|
if y1 == y2:
|
||||||
|
day_count = djn_dt2 - djn_dt1
|
||||||
|
if method == 'A/A ISDA':
|
||||||
|
year_days = 366 if cal.isleap(y2) else 365
|
||||||
|
if method == 'A/A AFB':
|
||||||
|
year_days = 366 if cal.isleap(y1) and (m1 < 3) else 365
|
||||||
|
if y1 < y2:
|
||||||
|
djn_dt1_eoy = get_julian_day_number(y1, 12, 31)
|
||||||
|
day_count_dt1 = djn_dt1_eoy - djn_dt1
|
||||||
|
if method == 'A/A ISDA':
|
||||||
|
year_days_dt1 = 366 if cal.isleap(y1) else 365
|
||||||
|
if method == 'A/A AFB':
|
||||||
|
year_days_dt1 = 366 if cal.isleap(y1) and (m1 < 3) else 365
|
||||||
|
|
||||||
|
djn_dt2_boy = get_julian_day_number(y2, 1, 1)
|
||||||
|
day_count_dt2 = djn_dt2 - djn_dt2_boy
|
||||||
|
if method == 'A/A ISDA':
|
||||||
|
year_days_dt2 = 366 if cal.isleap(y2) else 365
|
||||||
|
if method == 'A/A AFB':
|
||||||
|
year_days_dt2 = 366 if cal.isleap(y2) and (m2 >= 3) else 365
|
||||||
|
|
||||||
|
diff = y2 - y1 - 1
|
||||||
|
|
||||||
|
day_count = (day_count_dt1 * year_days_dt2) + (day_count_dt2 * year_days_dt1) + (
|
||||||
|
diff * year_days_dt1 * year_days_dt2)
|
||||||
|
year_days = year_days_dt1 * year_days_dt2
|
||||||
|
|
||||||
|
factor = day_count / year_days
|
||||||
|
return factor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_special_payment_schedule(self, special_payment):
|
||||||
|
no_of_payments = special_payment.special_payment_term * special_payment.annual_payments
|
||||||
|
annual_payments = special_payment.annual_payments
|
||||||
|
dt0 = dt.datetime.strptime(special_payment.first_payment_date, '%Y-%m-%d')
|
||||||
|
|
||||||
|
special_payment_amount = self._quantize(special_payment.payment_amount)
|
||||||
|
initial_special_payment = Payment(date=dt0, payment_amount=self._quantize(0),
|
||||||
|
interest_amount=self._quantize(0),
|
||||||
|
principal_amount=self._quantize(0),
|
||||||
|
special_principal_amount=special_payment_amount,
|
||||||
|
total_principal_amount=self._quantize(0),
|
||||||
|
loan_balance_amount=self._quantize(0))
|
||||||
|
special_payment_schedule = [initial_special_payment]
|
||||||
|
|
||||||
|
for i in range(1, no_of_payments):
|
||||||
|
date = dt0 + relativedelta(months=i * 12 / annual_payments)
|
||||||
|
special_payment = Payment(date=date, payment_amount=self._quantize(0),
|
||||||
|
interest_amount=self._quantize(0),
|
||||||
|
principal_amount=self._quantize(0),
|
||||||
|
special_principal_amount=special_payment_amount,
|
||||||
|
total_principal_amount=self._quantize(0),
|
||||||
|
loan_balance_amount=self._quantize(0))
|
||||||
|
special_payment_schedule.append(special_payment)
|
||||||
|
|
||||||
|
return special_payment_schedule
|
||||||
|
|
||||||
|
'''
|
||||||
|
Define method that calculates payment schedule
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_payment_schedule(self):
|
||||||
|
|
||||||
|
attributes = ['loan_amount', 'interest_rate', 'loan_term', 'payment_amount', 'start_date',
|
||||||
|
'first_payment_date', 'payment_end_of_month', 'annual_payments',
|
||||||
|
'no_of_payments', 'delta_dt', 'interest_only_period', 'compounding_method',
|
||||||
|
'special_payments', 'special_payments_schedule']
|
||||||
|
raise_error_flag = 0
|
||||||
|
for attribute in attributes:
|
||||||
|
if hasattr(self, attribute) is False:
|
||||||
|
raise_error_flag = raise_error_flag + 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
if raise_error_flag != 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Necessary attributes are not well defined, please review your inputs')
|
||||||
|
|
||||||
|
except ValueError as val_e:
|
||||||
|
print(val_e)
|
||||||
|
else:
|
||||||
|
initial_payment = Payment(date=self.start_date, payment_amount=self._quantize(0),
|
||||||
|
interest_amount=self._quantize(0),
|
||||||
|
principal_amount=self._quantize(0),
|
||||||
|
special_principal_amount=self._quantize(0),
|
||||||
|
total_principal_amount=self._quantize(0),
|
||||||
|
loan_balance_amount=self._quantize(self.loan_amount))
|
||||||
|
payment_schedule = [initial_payment]
|
||||||
|
interest_only_period = self.interest_only_period
|
||||||
|
|
||||||
|
# take care of loan type
|
||||||
|
if self.loan_type == 'annuity':
|
||||||
|
if self.payment_amount is None:
|
||||||
|
regular_principal_payment_amount = self.loan_amount * (
|
||||||
|
(self.interest_rate / self.annual_payments) * (
|
||||||
|
1 + (self.interest_rate / self.annual_payments)) ** (
|
||||||
|
(self.no_of_payments - interest_only_period))) / ((1 + (
|
||||||
|
self.interest_rate / self.annual_payments)) ** ((
|
||||||
|
self.no_of_payments - interest_only_period)) - 1)
|
||||||
|
else:
|
||||||
|
regular_principal_payment_amount = self.payment_amount
|
||||||
|
|
||||||
|
if self.loan_type == 'linear':
|
||||||
|
if self.payment_amount is None:
|
||||||
|
regular_principal_payment_amount = self.loan_amount / (
|
||||||
|
self.no_of_payments - self.interest_only_period)
|
||||||
|
else:
|
||||||
|
regular_principal_payment_amount = self.payment_amount
|
||||||
|
|
||||||
|
if self.loan_type == 'interest-only':
|
||||||
|
regular_principal_payment_amount = 0
|
||||||
|
interest_only_period = self.no_of_payments
|
||||||
|
|
||||||
|
if self.first_payment_date is None:
|
||||||
|
if self.payment_end_of_month == True:
|
||||||
|
if self.start_date.day == \
|
||||||
|
cal.monthrange(self.start_date.year, self.start_date.month)[1]:
|
||||||
|
dt0 = self.start_date
|
||||||
|
else:
|
||||||
|
dt0 = dt.datetime(self.start_date.year, self.start_date.month,
|
||||||
|
cal.monthrange(self.start_date.year,
|
||||||
|
self.start_date.month)[1], 0,
|
||||||
|
0) + relativedelta(months=-12 / self.annual_payments)
|
||||||
|
else:
|
||||||
|
dt0 = self.start_date
|
||||||
|
else:
|
||||||
|
dt0 = max(self.first_payment_date, self.start_date) + relativedelta(
|
||||||
|
months=-12 / self.annual_payments)
|
||||||
|
|
||||||
|
# take care of special payments
|
||||||
|
special_payments_schedule_raw = []
|
||||||
|
special_payments_schedule = []
|
||||||
|
special_payments_dates = []
|
||||||
|
if len(self.special_payments_schedule) > 0:
|
||||||
|
for i in range(len(self.special_payments_schedule)):
|
||||||
|
for j in range(len(self.special_payments_schedule[i])):
|
||||||
|
special_payments_schedule_raw.append(
|
||||||
|
[self.special_payments_schedule[i][j].date,
|
||||||
|
self.special_payments_schedule[i][j].special_principal_amount])
|
||||||
|
if self.special_payments_schedule[i][j].date not in special_payments_dates:
|
||||||
|
special_payments_dates.append(self.special_payments_schedule[i][j].date)
|
||||||
|
|
||||||
|
for i in range(len(special_payments_dates)):
|
||||||
|
amt = self._quantize(str(0))
|
||||||
|
for j in range(len(special_payments_schedule_raw)):
|
||||||
|
if special_payments_schedule_raw[j][0] == special_payments_dates[i]:
|
||||||
|
amt += special_payments_schedule_raw[j][1]
|
||||||
|
special_payments_schedule.append([special_payments_dates[i], amt])
|
||||||
|
|
||||||
|
# calculate payment schedule
|
||||||
|
m = 0
|
||||||
|
for i in range(1, self.no_of_payments + 1):
|
||||||
|
|
||||||
|
date = dt0 + relativedelta(months=i * 12 / self.annual_payments)
|
||||||
|
if self.payment_end_of_month == True and self.first_payment_date is None:
|
||||||
|
eom_day = cal.monthrange(date.year, date.month)[1]
|
||||||
|
date = date.replace(day=eom_day) # dt.datetime(date.year,date.month,eom_day)
|
||||||
|
|
||||||
|
special_principal_amount = self._quantize(0)
|
||||||
|
bop_date = payment_schedule[(i + m) - 1].date
|
||||||
|
compounding_factor = Decimal(
|
||||||
|
str(self._get_day_count(bop_date, date, self.compounding_method,
|
||||||
|
eom=self.payment_end_of_month)))
|
||||||
|
balance_bop = self._quantize(payment_schedule[(i + m) - 1].loan_balance_amount)
|
||||||
|
|
||||||
|
for j in range(len(special_payments_schedule)):
|
||||||
|
if date == special_payments_schedule[j][0]:
|
||||||
|
special_principal_amount = special_payments_schedule[j][1]
|
||||||
|
if (bop_date < special_payments_schedule[j][0] and special_payments_schedule[j][
|
||||||
|
0] < date):
|
||||||
|
# handle special payment inserts
|
||||||
|
compounding_factor = Decimal(
|
||||||
|
str(self._get_day_count(bop_date, special_payments_schedule[j][0],
|
||||||
|
self.compounding_method,
|
||||||
|
eom=self.payment_end_of_month)))
|
||||||
|
interest_amount = self._quantize(0) if balance_bop == Decimal(
|
||||||
|
str(0)) else self._quantize(
|
||||||
|
balance_bop * self.interest_rate * compounding_factor)
|
||||||
|
principal_amount = self._quantize(0)
|
||||||
|
special_principal_amount = self._quantize(0) if balance_bop == Decimal(
|
||||||
|
str(0)) else min(special_payments_schedule[j][1] - interest_amount,
|
||||||
|
balance_bop)
|
||||||
|
total_principal_amount = min(principal_amount + special_principal_amount,
|
||||||
|
balance_bop)
|
||||||
|
total_payment_amount = total_principal_amount + interest_amount
|
||||||
|
balance_eop = max(balance_bop - total_principal_amount, self._quantize(0))
|
||||||
|
payment = Payment(date=special_payments_schedule[j][0],
|
||||||
|
payment_amount=total_payment_amount,
|
||||||
|
interest_amount=interest_amount,
|
||||||
|
principal_amount=principal_amount,
|
||||||
|
special_principal_amount=special_principal_amount,
|
||||||
|
total_principal_amount=special_principal_amount,
|
||||||
|
loan_balance_amount=balance_eop)
|
||||||
|
payment_schedule.append(payment)
|
||||||
|
m += 1
|
||||||
|
# handle regular payment inserts : update bop_date and bop_date, and special_principal_amount
|
||||||
|
bop_date = special_payments_schedule[j][0]
|
||||||
|
balance_bop = balance_eop
|
||||||
|
special_principal_amount = self._quantize(0)
|
||||||
|
compounding_factor = Decimal(
|
||||||
|
str(self._get_day_count(bop_date, date, self.compounding_method,
|
||||||
|
eom=self.payment_end_of_month)))
|
||||||
|
|
||||||
|
interest_amount = self._quantize(0) if balance_bop == Decimal(
|
||||||
|
str(0)) else self._quantize(
|
||||||
|
balance_bop * self.interest_rate * compounding_factor)
|
||||||
|
|
||||||
|
principal_amount = self._quantize(0) if balance_bop == Decimal(
|
||||||
|
str(0)) or interest_only_period >= i else min(
|
||||||
|
self._quantize(regular_principal_payment_amount) - (
|
||||||
|
interest_amount if self.loan_type == 'annuity' else 0), balance_bop)
|
||||||
|
special_principal_amount = min(balance_bop - principal_amount,
|
||||||
|
special_principal_amount) if interest_only_period < i else self._quantize(
|
||||||
|
0)
|
||||||
|
total_principal_amount = min(principal_amount + special_principal_amount,
|
||||||
|
balance_bop)
|
||||||
|
total_payment_amount = total_principal_amount + interest_amount
|
||||||
|
balance_eop = max(balance_bop - total_principal_amount, self._quantize(0))
|
||||||
|
|
||||||
|
payment = Payment(date=date, payment_amount=total_payment_amount,
|
||||||
|
interest_amount=interest_amount,
|
||||||
|
principal_amount=principal_amount,
|
||||||
|
special_principal_amount=special_principal_amount,
|
||||||
|
total_principal_amount=total_principal_amount,
|
||||||
|
loan_balance_amount=balance_eop)
|
||||||
|
payment_schedule.append(payment)
|
||||||
|
|
||||||
|
return payment_schedule
|
||||||
|
|
||||||
|
def add_special_payment(self, payment_amount, first_payment_date, special_payment_term,
|
||||||
|
annual_payments):
|
||||||
|
special_payment = Special_Payment(payment_amount=payment_amount,
|
||||||
|
first_payment_date=first_payment_date,
|
||||||
|
special_payment_term=special_payment_term,
|
||||||
|
annual_payments=annual_payments)
|
||||||
|
self.special_payments.append(special_payment)
|
||||||
|
self.special_payments_schedule.append(
|
||||||
|
self._get_special_payment_schedule(self, special_payment))
|
||||||
|
|
||||||
|
def get_loan_summary(self):
|
||||||
|
payment_schedule = self.get_payment_schedule()
|
||||||
|
total_payment_amount = 0
|
||||||
|
total_interest_amount = 0
|
||||||
|
total_principal_amount = 0
|
||||||
|
repayment_to_principal = 0
|
||||||
|
|
||||||
|
for payment in payment_schedule:
|
||||||
|
total_payment_amount += payment.payment_amount
|
||||||
|
total_interest_amount += payment.interest_amount
|
||||||
|
total_principal_amount += payment.total_principal_amount
|
||||||
|
|
||||||
|
repayment_to_principal = self._quantize(total_payment_amount / total_principal_amount)
|
||||||
|
loan_summary = Loan_Summary(loan_amount=self._quantize(self.loan_amount),
|
||||||
|
total_payment_amount=total_payment_amount,
|
||||||
|
total_principal_amount=total_principal_amount,
|
||||||
|
total_interest_amount=total_interest_amount,
|
||||||
|
residual_loan_balance=self._quantize(
|
||||||
|
self.loan_amount - total_principal_amount),
|
||||||
|
repayment_to_principal=repayment_to_principal)
|
||||||
|
|
||||||
|
return loan_summary
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from . import account_asset
|
||||||
|
from . import account_asset_group
|
||||||
|
from . import account_journal
|
||||||
|
from . import account_loan
|
||||||
|
from . import account_loan_line
|
||||||
|
from . import account_move
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAsset(models.Model):
|
||||||
|
_inherit = 'account.asset'
|
||||||
|
|
||||||
|
linked_loans_ids = fields.One2many(related='asset_group_id.linked_loan_ids')
|
||||||
|
count_linked_loans = fields.Integer(compute="_compute_count_linked_loans")
|
||||||
|
|
||||||
|
@api.depends('linked_loans_ids')
|
||||||
|
def _compute_count_linked_loans(self):
|
||||||
|
for asset in self:
|
||||||
|
asset.count_linked_loans = len(asset.linked_loans_ids)
|
||||||
|
|
||||||
|
def action_open_linked_loans(self):
|
||||||
|
return {
|
||||||
|
'name': _('Linked loans'),
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'res_model': 'account.loan',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'target': 'current',
|
||||||
|
'domain': [('id', 'in', self.linked_loans_ids.ids)],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from odoo import fields, models, api
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAssetGroup(models.Model):
|
||||||
|
_inherit = 'account.asset.group'
|
||||||
|
|
||||||
|
linked_loan_ids = fields.One2many('account.loan', 'asset_group_id', string='Related Loans')
|
||||||
|
count_linked_loans = fields.Integer(compute="_compute_count_linked_loans")
|
||||||
|
|
||||||
|
@api.depends('linked_loan_ids')
|
||||||
|
def _compute_count_linked_loans(self):
|
||||||
|
count_per_asset_group = {
|
||||||
|
asset_group.id: count
|
||||||
|
for asset_group, count in self.env['account.loan']._read_group(
|
||||||
|
domain=[
|
||||||
|
('asset_group_id', 'in', self.ids),
|
||||||
|
],
|
||||||
|
groupby=['asset_group_id'],
|
||||||
|
aggregates=['__count'],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for asset_group in self:
|
||||||
|
asset_group.count_linked_loans = count_per_asset_group.get(asset_group.id, 0)
|
||||||
|
|
||||||
|
def action_open_linked_loans(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'res_model': 'account.loan',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'domain': [('id', 'in', self.linked_loan_ids.ids)],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AccountJournal(models.Model):
|
||||||
|
_inherit = "account.journal"
|
||||||
|
|
||||||
|
loan_properties_definition = fields.PropertiesDefinition('Model Properties')
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _, Command
|
||||||
|
from odoo.tools import float_compare
|
||||||
|
from odoo.tools.misc import format_date
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLoan(models.Model):
|
||||||
|
_name = 'account.loan'
|
||||||
|
_description = 'Loan'
|
||||||
|
_inherit = ['mail.thread']
|
||||||
|
_order = 'date'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
values = super().default_get(fields_list)
|
||||||
|
if all(field not in fields_list for field in ['expense_account_id', 'long_term_account_id', 'short_term_account_id', 'journal_id']):
|
||||||
|
return values
|
||||||
|
previous_loan = self.search([
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('expense_account_id', '!=', False),
|
||||||
|
('long_term_account_id', '!=', False),
|
||||||
|
('short_term_account_id', '!=', False),
|
||||||
|
('journal_id', '!=', False),
|
||||||
|
], limit=1)
|
||||||
|
if previous_loan:
|
||||||
|
values['expense_account_id'] = previous_loan.expense_account_id.id
|
||||||
|
values['long_term_account_id'] = previous_loan.long_term_account_id.id
|
||||||
|
values['short_term_account_id'] = previous_loan.short_term_account_id.id
|
||||||
|
values['journal_id'] = previous_loan.journal_id.id
|
||||||
|
return values
|
||||||
|
|
||||||
|
name = fields.Char("Name", required=True, index="trigram", tracking=True)
|
||||||
|
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||||
|
currency_id = fields.Many2one(related='company_id.currency_id')
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
state = fields.Selection(
|
||||||
|
string="Status",
|
||||||
|
selection=[
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('running', 'Running'),
|
||||||
|
('closed', 'Closed'),
|
||||||
|
('cancelled', 'Cancelled'),
|
||||||
|
],
|
||||||
|
default='draft',
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
date = fields.Date('Loan Date', index="btree_not_null")
|
||||||
|
amount_borrowed = fields.Monetary(string='Amount Borrowed', tracking=True)
|
||||||
|
interest = fields.Monetary(string='Interest')
|
||||||
|
duration = fields.Integer('Duration')
|
||||||
|
skip_until_date = fields.Date(
|
||||||
|
string='Skip until',
|
||||||
|
help='Upon confirmation of the loan, Odoo will ignore the loan lines that are up to this date (included) and not create entries for them. '
|
||||||
|
'This is useful if you have already manually created entries prior to the creation of this loan.'
|
||||||
|
)
|
||||||
|
|
||||||
|
long_term_account_id = fields.Many2one('account.account', string='Long Term Account', tracking=True)
|
||||||
|
short_term_account_id = fields.Many2one('account.account', string='Short Term Account', tracking=True)
|
||||||
|
expense_account_id = fields.Many2one(
|
||||||
|
comodel_name='account.account',
|
||||||
|
string='Expense Account',
|
||||||
|
tracking=True,
|
||||||
|
domain="[('account_type', 'in', ('expense', 'expense_depreciation'))]",
|
||||||
|
)
|
||||||
|
journal_id = fields.Many2one(
|
||||||
|
comodel_name='account.journal',
|
||||||
|
string='Journal',
|
||||||
|
domain="[('type', '=', 'general')]",
|
||||||
|
)
|
||||||
|
asset_group_id = fields.Many2one('account.asset.group', string='Asset Group', tracking=True, index=True)
|
||||||
|
loan_properties = fields.Properties('Properties', definition='journal_id.loan_properties_definition')
|
||||||
|
|
||||||
|
line_ids = fields.One2many('account.loan.line', 'loan_id', string='Loan Lines') # Amortization schedule
|
||||||
|
|
||||||
|
# Computed fields
|
||||||
|
display_name = fields.Char("Loan name", compute='_compute_display_name', store=True) # stored for pivot view
|
||||||
|
start_date = fields.Date(compute='_compute_start_end_date')
|
||||||
|
end_date = fields.Date(compute='_compute_start_end_date')
|
||||||
|
is_wrong_date = fields.Boolean(compute='_compute_is_wrong_date')
|
||||||
|
amount_borrowed_difference = fields.Monetary(compute='_compute_amount_borrowed_difference')
|
||||||
|
interest_difference = fields.Monetary(compute='_compute_interest_difference')
|
||||||
|
duration_difference = fields.Integer(compute='_compute_duration_difference')
|
||||||
|
outstanding_balance = fields.Monetary(string='Outstanding Balance', compute='_compute_outstanding_balance') # Based on the posted entries
|
||||||
|
nb_posted_entries = fields.Integer(compute='_compute_nb_posted_entries')
|
||||||
|
linked_assets_ids = fields.One2many(
|
||||||
|
comodel_name='account.asset',
|
||||||
|
string="Linked Assets",
|
||||||
|
compute='_compute_linked_assets',
|
||||||
|
)
|
||||||
|
count_linked_assets = fields.Integer(compute="_compute_linked_assets")
|
||||||
|
|
||||||
|
# Constrains
|
||||||
|
@api.constrains('amount_borrowed', 'interest', 'duration')
|
||||||
|
def _require_positive_values(self):
|
||||||
|
for loan in self:
|
||||||
|
if float_compare(loan.amount_borrowed, 0.0, precision_rounding=loan.currency_id.rounding) < 0:
|
||||||
|
raise ValidationError(_('The amount borrowed must be positive'))
|
||||||
|
if float_compare(loan.interest, 0.0, precision_rounding=loan.currency_id.rounding) < 0:
|
||||||
|
raise ValidationError(_('The interest must be positive'))
|
||||||
|
if loan.duration < 0:
|
||||||
|
raise ValidationError(_('The duration must be positive'))
|
||||||
|
|
||||||
|
# Compute methods
|
||||||
|
@api.depends('name', 'start_date', 'end_date')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for loan in self:
|
||||||
|
if loan.name and loan.start_date and loan.end_date:
|
||||||
|
start_date = format_date(self.env, loan.start_date, date_format='MM y')
|
||||||
|
end_date = format_date(self.env, loan.end_date, date_format='MM y')
|
||||||
|
loan.display_name = f"{loan.name}: {start_date} - {end_date}"
|
||||||
|
else:
|
||||||
|
loan.display_name = loan.name
|
||||||
|
|
||||||
|
@api.depends('line_ids')
|
||||||
|
def _compute_start_end_date(self):
|
||||||
|
for loan in self:
|
||||||
|
if loan.line_ids:
|
||||||
|
loan.start_date = loan.line_ids[0].date
|
||||||
|
loan.end_date = loan.line_ids[-1].date
|
||||||
|
else:
|
||||||
|
loan.start_date = False
|
||||||
|
loan.end_date = False
|
||||||
|
|
||||||
|
@api.depends('date')
|
||||||
|
def _compute_is_wrong_date(self):
|
||||||
|
for loan in self:
|
||||||
|
loan.is_wrong_date = not loan.date or any(date < loan.date for date in loan.line_ids.mapped('date'))
|
||||||
|
|
||||||
|
@api.depends('amount_borrowed', 'line_ids.principal', 'currency_id')
|
||||||
|
def _compute_amount_borrowed_difference(self):
|
||||||
|
for loan in self:
|
||||||
|
if loan.currency_id:
|
||||||
|
loan.amount_borrowed_difference = abs(loan.amount_borrowed - loan.currency_id.round(sum(loan.line_ids.mapped('principal'))))
|
||||||
|
else:
|
||||||
|
loan.amount_borrowed_difference = 0
|
||||||
|
|
||||||
|
@api.depends('interest', 'line_ids.interest')
|
||||||
|
def _compute_interest_difference(self):
|
||||||
|
for loan in self:
|
||||||
|
if loan.interest and loan.line_ids:
|
||||||
|
loan.interest_difference = loan.interest - loan.currency_id.round(sum(loan.line_ids.mapped('interest')))
|
||||||
|
else:
|
||||||
|
loan.interest_difference = 0
|
||||||
|
|
||||||
|
@api.depends('duration', 'line_ids')
|
||||||
|
def _compute_duration_difference(self):
|
||||||
|
for loan in self:
|
||||||
|
loan.duration_difference = loan.duration - len(loan.line_ids)
|
||||||
|
|
||||||
|
@api.depends('line_ids.generated_move_ids')
|
||||||
|
def _compute_nb_posted_entries(self):
|
||||||
|
for loan in self:
|
||||||
|
loan.nb_posted_entries = len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted'))
|
||||||
|
|
||||||
|
@api.depends('amount_borrowed', 'line_ids.principal', 'state', 'line_ids.is_payment_move_posted')
|
||||||
|
def _compute_outstanding_balance(self):
|
||||||
|
for loan in self:
|
||||||
|
outstanding_balance = loan.amount_borrowed
|
||||||
|
if loan.state == 'running':
|
||||||
|
for line in loan.line_ids:
|
||||||
|
if line.is_payment_move_posted or (loan.skip_until_date and line.date < loan.skip_until_date):
|
||||||
|
outstanding_balance -= line.principal
|
||||||
|
loan.outstanding_balance = outstanding_balance
|
||||||
|
|
||||||
|
@api.depends('asset_group_id')
|
||||||
|
def _compute_linked_assets(self):
|
||||||
|
for loan in self:
|
||||||
|
loan.linked_assets_ids = loan.asset_group_id.linked_asset_ids
|
||||||
|
loan.count_linked_assets = len(loan.linked_assets_ids)
|
||||||
|
|
||||||
|
# Action methods
|
||||||
|
def action_confirm(self):
|
||||||
|
for loan in self:
|
||||||
|
# Verifications
|
||||||
|
if not loan.name:
|
||||||
|
raise UserError(_("The loan name should be set."))
|
||||||
|
if loan.is_wrong_date:
|
||||||
|
raise UserError(_("The loan date should be earlier than the loan lines date."))
|
||||||
|
if float_compare(loan.amount_borrowed_difference, 0.0, precision_rounding=loan.currency_id.rounding) != 0:
|
||||||
|
raise UserError(_(
|
||||||
|
"The loan amount %(loan_amount)s should be equal to the sum of the principals: %(principal_sum)s (difference %(principal_difference)s)",
|
||||||
|
loan_amount=loan.currency_id.format(loan.amount_borrowed),
|
||||||
|
principal_sum=loan.currency_id.format(sum(loan.line_ids.mapped('principal'))),
|
||||||
|
principal_difference=loan.currency_id.format(loan.amount_borrowed_difference),
|
||||||
|
))
|
||||||
|
if float_compare(loan.interest_difference, 0.0, precision_rounding=loan.currency_id.rounding) != 0:
|
||||||
|
raise UserError(_("The loan interest should be equal to the sum of the loan lines interest."))
|
||||||
|
if loan.duration_difference != 0:
|
||||||
|
raise UserError(_("The loan duration should be equal to the number of loan lines."))
|
||||||
|
if not loan.long_term_account_id or not loan.short_term_account_id or not loan.expense_account_id:
|
||||||
|
raise UserError(_("The loan accounts should be set."))
|
||||||
|
if not loan.journal_id:
|
||||||
|
raise UserError(_("The loan journal should be set."))
|
||||||
|
|
||||||
|
payment_moves_values = []
|
||||||
|
reclassification_moves_values = []
|
||||||
|
reclassification_reversed_moves_values = []
|
||||||
|
for i, line in enumerate(loan.line_ids):
|
||||||
|
if loan.skip_until_date and line.date < loan.skip_until_date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Principal and interest (to match with the bank statement)
|
||||||
|
payment_moves_values.append({
|
||||||
|
'company_id': loan.company_id.id,
|
||||||
|
'auto_post': 'at_date',
|
||||||
|
'generating_loan_line_id': line.id,
|
||||||
|
'is_loan_payment_move': True,
|
||||||
|
'date': line.date + relativedelta(day=31),
|
||||||
|
'journal_id': loan.journal_id.id,
|
||||||
|
'ref': f"{loan.name} - {_('Principal & Interest')} {format_date(self.env, line.date, date_format='MM/y')}",
|
||||||
|
'line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.long_term_account_id.id,
|
||||||
|
'debit': line.principal,
|
||||||
|
'name': f"{loan.name} - {_('Principal')} {format_date(self.env, line.date, date_format='MM/y')}",
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.short_term_account_id.id,
|
||||||
|
'credit': line.payment,
|
||||||
|
'name': f"{loan.name} - {_('Due')} {format_date(self.env, line.date, date_format='MM/y')} "
|
||||||
|
f"({_('Principal')} {loan.currency_id.format(line.principal)} + {_('Interest')} {loan.currency_id.format(line.interest)})",
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.expense_account_id.id,
|
||||||
|
'debit': line.interest,
|
||||||
|
'name': f"{loan.name} - {_('Interest')} {format_date(self.env, line.date, date_format='MM/y')}",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
# Principal reclassification Long Term - Short Term
|
||||||
|
if line == loan.line_ids[-1]:
|
||||||
|
break
|
||||||
|
next_lines = loan.line_ids[i + 1: i + 13] # 13 = 1 (start offset) + 12 months
|
||||||
|
from_date = format_date(self.env, next_lines[0].date, date_format='MM/y')
|
||||||
|
to_date = format_date(self.env, next_lines[-1].date, date_format='MM/y')
|
||||||
|
common_reclassification_values = {
|
||||||
|
'company_id': loan.company_id.id,
|
||||||
|
'auto_post': 'at_date',
|
||||||
|
'generating_loan_line_id': line.id,
|
||||||
|
'is_loan_payment_move': False,
|
||||||
|
'journal_id': loan.journal_id.id,
|
||||||
|
}
|
||||||
|
reclassification_moves_values.append({
|
||||||
|
**common_reclassification_values,
|
||||||
|
'date': line.date + relativedelta(day=31),
|
||||||
|
'ref': f"{loan.name} - {_('Reclassification LT - ST')} {from_date} to {to_date}",
|
||||||
|
'line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.long_term_account_id.id,
|
||||||
|
'debit': sum(next_lines.mapped('principal')),
|
||||||
|
'name': f"{loan.name} - {_('Reclassification LT - ST')} {from_date} to {to_date} (To {loan.short_term_account_id.code})",
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.short_term_account_id.id,
|
||||||
|
'credit': sum(next_lines.mapped('principal')),
|
||||||
|
'name': f"{loan.name} - {_('Reclassification LT - ST')} {from_date} to {to_date} (From {loan.long_term_account_id.code})",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
# Manually create the reverse (instead of using _reverse_moves()) for optimization reasons
|
||||||
|
reclassification_reversed_moves_values.append({
|
||||||
|
**common_reclassification_values,
|
||||||
|
'date': line.date + relativedelta(day=31) + relativedelta(days=1), # first day of next month
|
||||||
|
'ref': f"{loan.name} - {_('Reversal reclassification LT - ST')} {from_date} to {to_date}",
|
||||||
|
'line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.long_term_account_id.id,
|
||||||
|
'credit': sum(next_lines.mapped('principal')),
|
||||||
|
'name': f"{loan.name} - {_('Reversal reclassification LT - ST')} {from_date} to {to_date} (To {loan.short_term_account_id.code})",
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'account_id': loan.short_term_account_id.id,
|
||||||
|
'debit': sum(next_lines.mapped('principal')),
|
||||||
|
'name': f"{loan.name} - {_('Reversal reclassification LT - ST')} {from_date} to {to_date} (From {loan.long_term_account_id.code})",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
def post_moves(moves):
|
||||||
|
moves.filtered(lambda m: m.date <= fields.Date.context_today(self)).action_post()
|
||||||
|
|
||||||
|
payment_moves = self.env['account.move'].create(payment_moves_values)
|
||||||
|
reclassification_moves = self.env['account.move'].create(reclassification_moves_values)
|
||||||
|
reclassification_reversed_moves = self.env['account.move'].create(reclassification_reversed_moves_values)
|
||||||
|
post_moves(payment_moves | reclassification_moves | reclassification_reversed_moves)
|
||||||
|
|
||||||
|
for (reclassification_move, reclassification_reversed_move) in zip(reclassification_moves, reclassification_reversed_moves):
|
||||||
|
reclassification_reversed_move.reversed_entry_id = reclassification_move
|
||||||
|
reclassification_reversed_move.message_post(body=_('This entry has been reversed from %s', reclassification_move._get_html_link()))
|
||||||
|
|
||||||
|
bodies = {}
|
||||||
|
for move, reverse in zip(reclassification_moves, reclassification_reversed_moves):
|
||||||
|
bodies[move.id] = _('This entry has been %s', reverse._get_html_link(title=_("reversed")))
|
||||||
|
reclassification_moves._message_log_batch(bodies=bodies)
|
||||||
|
|
||||||
|
if any(m.state != 'posted' for m in payment_moves | reclassification_moves | reclassification_reversed_moves):
|
||||||
|
loan.state = 'running'
|
||||||
|
|
||||||
|
def action_upload_amortization_schedule(self, attachment_id):
|
||||||
|
"""Called when uploading an amortization schedule file"""
|
||||||
|
attachment = self.env['ir.attachment'].browse(attachment_id)
|
||||||
|
loan = self or self.create({
|
||||||
|
'name': attachment.name,
|
||||||
|
})
|
||||||
|
loan.line_ids.unlink()
|
||||||
|
loan.message_post(body=_('Uploaded file'), attachment_ids=[attachment.id])
|
||||||
|
import_wizard = self.env['base_import.import'].create({
|
||||||
|
'res_model': 'account.loan.line',
|
||||||
|
'file': attachment.raw,
|
||||||
|
'file_name': attachment.name,
|
||||||
|
'file_type': attachment.mimetype,
|
||||||
|
})
|
||||||
|
ctx = {
|
||||||
|
**self.env.context,
|
||||||
|
'wizard_id': import_wizard.id,
|
||||||
|
'default_loan_id': loan.id,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'import_loan',
|
||||||
|
'params': {
|
||||||
|
'model': 'account.loan.line',
|
||||||
|
'context': ctx,
|
||||||
|
'filename': attachment.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_file_uploaded(self):
|
||||||
|
"""Called after the amortization schedule has been imported by the wizard"""
|
||||||
|
self.ensure_one()
|
||||||
|
action = {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _("Loans"),
|
||||||
|
'res_model': 'account.loan',
|
||||||
|
'views': [(False, 'list'), (False, 'form')],
|
||||||
|
'target': 'self',
|
||||||
|
}
|
||||||
|
if self.line_ids:
|
||||||
|
self.amount_borrowed = sum(self.line_ids.mapped('principal'))
|
||||||
|
self.interest = sum(self.line_ids.mapped('interest'))
|
||||||
|
self.duration = len(self.line_ids)
|
||||||
|
self.date = self.line_ids[0].date
|
||||||
|
return {
|
||||||
|
**action,
|
||||||
|
'res_id': self.id,
|
||||||
|
'views': [(False, 'form')],
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
|
||||||
|
def action_open_compute_wizard(self):
|
||||||
|
if not self:
|
||||||
|
raise UserError(_("Please add a name before computing the loan"))
|
||||||
|
wizard = self.env['account.loan.compute.wizard'].create({
|
||||||
|
'loan_id': self.id,
|
||||||
|
'loan_amount': self.amount_borrowed,
|
||||||
|
})
|
||||||
|
if self.date:
|
||||||
|
wizard["start_date"] = self.date
|
||||||
|
wizard["first_payment_date"] = self.date.replace(day=1) + relativedelta(months=1) # first day of next month
|
||||||
|
return {
|
||||||
|
'name': _("Compute New Loan"),
|
||||||
|
'res_id': wizard.id,
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'account.loan.compute.wizard',
|
||||||
|
'target': 'new',
|
||||||
|
'views': [[False, 'form']],
|
||||||
|
'context': self.env.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_reset(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.line_ids.unlink()
|
||||||
|
|
||||||
|
def action_close(self):
|
||||||
|
self.ensure_one()
|
||||||
|
wizard = self.env['account.loan.close.wizard'].create({
|
||||||
|
'loan_id': self.id,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'name': _('Close'),
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'account.loan.close.wizard',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'target': 'new',
|
||||||
|
'res_id': wizard.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_cancel(self):
|
||||||
|
self.line_ids.generated_move_ids.filtered(lambda m: m.state != 'cancel')._unlink_or_reverse()
|
||||||
|
self.state = 'cancelled'
|
||||||
|
|
||||||
|
def action_set_to_draft(self):
|
||||||
|
self.line_ids.generated_move_ids.filtered(lambda m: m.state != 'cancel')._unlink_or_reverse()
|
||||||
|
self.state = 'draft'
|
||||||
|
|
||||||
|
def action_open_loan_entries(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': _('Loan Entries'),
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'res_model': 'account.move',
|
||||||
|
'views': [(self.env.ref('account_loans.account_loan_view_account_move_list_view').id, 'list'), (False, 'form')],
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'domain': [('id', 'in', self.line_ids.generated_move_ids.ids)],
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_linked_assets(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': _('Linked Assets'),
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'res_model': 'account.asset',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'domain': [('id', 'in', self.linked_assets_ids.ids)],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Model methods
|
||||||
|
@api.ondelete(at_uninstall=False)
|
||||||
|
def _unlink_loan(self):
|
||||||
|
for loan in self:
|
||||||
|
loan.line_ids.generated_move_ids._unlink_or_reverse()
|
||||||
|
loan.line_ids.unlink()
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
from odoo import fields, models, api
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLoanLine(models.Model):
|
||||||
|
_name = 'account.loan.line'
|
||||||
|
_description = 'Loan Line'
|
||||||
|
_order = 'date, id'
|
||||||
|
|
||||||
|
sequence = fields.Integer("#", compute='_compute_sequence')
|
||||||
|
loan_id = fields.Many2one('account.loan', string='Loan', required=True, ondelete="cascade", index=True)
|
||||||
|
loan_name = fields.Char(related='loan_id.name')
|
||||||
|
loan_state = fields.Selection(related='loan_id.state')
|
||||||
|
loan_date = fields.Date(related='loan_id.date')
|
||||||
|
loan_asset_group_id = fields.Many2one(related='loan_id.asset_group_id')
|
||||||
|
active = fields.Boolean(related='loan_id.active')
|
||||||
|
company_id = fields.Many2one(related='loan_id.company_id')
|
||||||
|
currency_id = fields.Many2one(related='company_id.currency_id')
|
||||||
|
date = fields.Date('Date', required=True)
|
||||||
|
principal = fields.Monetary(string='Principal')
|
||||||
|
interest = fields.Monetary(string='Interest')
|
||||||
|
payment = fields.Monetary(
|
||||||
|
string='Payment',
|
||||||
|
compute='_compute_payment',
|
||||||
|
store=True, # stored for pivot view
|
||||||
|
)
|
||||||
|
outstanding_balance = fields.Monetary(
|
||||||
|
string='Outstanding Balance',
|
||||||
|
compute='_compute_outstanding_balance',
|
||||||
|
) # theoretical outstanding balance at the date of the line
|
||||||
|
long_term_theoretical_balance = fields.Monetary(
|
||||||
|
string='Long-Term',
|
||||||
|
compute='_compute_theoretical_balances',
|
||||||
|
store=True, # stored for pivot view
|
||||||
|
)
|
||||||
|
short_term_theoretical_balance = fields.Monetary(
|
||||||
|
string='Short-Term',
|
||||||
|
compute='_compute_theoretical_balances',
|
||||||
|
store=True, # stored for pivot view
|
||||||
|
)
|
||||||
|
generated_move_ids = fields.One2many(
|
||||||
|
comodel_name='account.move',
|
||||||
|
inverse_name='generating_loan_line_id',
|
||||||
|
string='Generated Entries',
|
||||||
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
|
help="Entries that we generated from this loan line"
|
||||||
|
)
|
||||||
|
is_payment_move_posted = fields.Boolean(compute='_compute_is_payment_move_posted')
|
||||||
|
|
||||||
|
@api.depends('principal', 'interest')
|
||||||
|
def _compute_payment(self):
|
||||||
|
for line in self:
|
||||||
|
line.payment = line.principal + line.interest
|
||||||
|
|
||||||
|
@api.depends('loan_id.line_ids', 'loan_id.amount_borrowed', 'principal')
|
||||||
|
def _compute_outstanding_balance(self):
|
||||||
|
for line in self:
|
||||||
|
line.outstanding_balance = line.loan_id.amount_borrowed - sum(line.loan_id.line_ids.filtered(lambda l: line.date and l.date <= line.date).mapped('principal'))
|
||||||
|
|
||||||
|
@api.depends('principal', 'date', 'loan_id.line_ids.date', 'loan_id.line_ids.principal')
|
||||||
|
def _compute_theoretical_balances(self):
|
||||||
|
for line in self:
|
||||||
|
filtered_lines = line.loan_id.line_ids.filtered(lambda l: line.date and l.date and l.date > line.date)
|
||||||
|
line.long_term_theoretical_balance = sum(filtered_lines[12:].mapped('principal'))
|
||||||
|
line.short_term_theoretical_balance = sum(filtered_lines[:12].mapped('principal'))
|
||||||
|
|
||||||
|
@api.depends('loan_id.line_ids', 'date')
|
||||||
|
def _compute_sequence(self):
|
||||||
|
for line in self.sorted('date'):
|
||||||
|
line.sequence = len(line.loan_id.line_ids.filtered(lambda l: line.date and l.date <= line.date))
|
||||||
|
|
||||||
|
@api.depends('generated_move_ids.state')
|
||||||
|
def _compute_is_payment_move_posted(self):
|
||||||
|
for line in self:
|
||||||
|
generated_moves = line.generated_move_ids.filtered(lambda m: m.is_loan_payment_move)
|
||||||
|
# In case of audit trail being activated, we can have more than 1 generated move (i.e. after loan closing/cancellation and re-confirmation),
|
||||||
|
# so we take the one that has no reversal move.
|
||||||
|
if len(generated_moves) > 1 and any(m.reversal_move_ids for m in generated_moves):
|
||||||
|
generated_moves = generated_moves.filtered(lambda m: not m.reversal_move_ids)
|
||||||
|
line.is_payment_move_posted = any(m.state == 'posted' for m in generated_moves)
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
from odoo import models, fields, _
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = "account.move"
|
||||||
|
|
||||||
|
generating_loan_line_id = fields.Many2one(
|
||||||
|
comodel_name='account.loan.line',
|
||||||
|
string='Generating Loan Line',
|
||||||
|
help="Line of the loan that generated this entry",
|
||||||
|
copy=False,
|
||||||
|
readonly=True,
|
||||||
|
index=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
loan_id = fields.Many2one(related='generating_loan_line_id.loan_id')
|
||||||
|
is_loan_payment_move = fields.Boolean()
|
||||||
|
|
||||||
|
def _post(self, soft=True):
|
||||||
|
posted = super()._post(soft)
|
||||||
|
for move in self:
|
||||||
|
skip_date = move.loan_id.skip_until_date
|
||||||
|
if move.loan_id and all(l.is_payment_move_posted or skip_date and l.date < skip_date for l in move.loan_id.line_ids):
|
||||||
|
move.loan_id.state = 'closed'
|
||||||
|
return posted
|
||||||
|
|
||||||
|
def open_loan(self):
|
||||||
|
self.ensure_one()
|
||||||
|
action = {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _("Original Loan"),
|
||||||
|
'views': [(False, 'form')],
|
||||||
|
'res_model': 'account.loan',
|
||||||
|
'res_id': self.loan_id.id,
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="account_loan_rule" model="ir.rule">
|
||||||
|
<field name="name">Account Loan multi-company</field>
|
||||||
|
<field name="model_id" ref="model_account_loan"/>
|
||||||
|
<field name="domain_force">[('company_id', 'parent_of', company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_line_rule" model="ir.rule">
|
||||||
|
<field name="name">Account Loan Line multi-company</field>
|
||||||
|
<field name="model_id" ref="model_account_loan_line"/>
|
||||||
|
<field name="domain_force">[('company_id', 'parent_of', company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||||
|
"access_account_loan_readonly","access.account.loan","model_account_loan","account.group_account_readonly",1,0,0,0
|
||||||
|
"access_account_loan_write","access.account.loan","model_account_loan","account.group_account_manager",1,1,1,1
|
||||||
|
"access_account_loan_line_readonly","access.account.loan.line","model_account_loan_line","account.group_account_readonly",1,0,0,0
|
||||||
|
"access_account_loan_line_write","access.account.loan.line","model_account_loan_line","account.group_account_manager",1,1,1,1
|
||||||
|
"access_account_loan_close_wizard","access.account.loan.close.wizard","model_account_loan_close_wizard","account.group_account_manager",1,1,1,1
|
||||||
|
"access_account_loan_compute_wizard","access.account.loan.compute.wizard","model_account_loan_compute_wizard","account.group_account_manager",1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { FileUploader } from "@web/views/fields/file_handler";
|
||||||
|
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
|
||||||
|
|
||||||
|
export class NewLoanComponent extends Component {
|
||||||
|
static template = "odex30_account_accountant.NewLoan";
|
||||||
|
static components = {
|
||||||
|
FileUploader,
|
||||||
|
};
|
||||||
|
static props = {
|
||||||
|
...standardWidgetProps,
|
||||||
|
record: { type: Object, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.action = useService("action");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFileUploaded(file) {
|
||||||
|
if (this.props.record && this.props.record.data.name){ //Save the record before calling the wizard
|
||||||
|
await this.props.record.model.root.save({reload: false});
|
||||||
|
}
|
||||||
|
const att_data = {
|
||||||
|
name: file.name,
|
||||||
|
mimetype: file.type,
|
||||||
|
datas: file.data,
|
||||||
|
};
|
||||||
|
const [att_id] = await this.orm.create("ir.attachment", [att_data]);
|
||||||
|
const action = await this.orm.call("account.loan", "action_upload_amortization_schedule", [this.props.record?.resId, att_id]);
|
||||||
|
this.action.doAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openComputeWizard() {
|
||||||
|
if (this.props.record && this.props.record.data.name){ //Save the record before calling the wizard
|
||||||
|
await this.props.record.model.root.save({reload: false});
|
||||||
|
}
|
||||||
|
const action = await this.orm.call("account.loan", "action_open_compute_wizard", [this.props.record?.resId]);
|
||||||
|
this.action.doAction(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newLoan = {
|
||||||
|
component: NewLoanComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("view_widgets").add("new_loan", newLoan);
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
<!-- Upload from form view-->
|
||||||
|
<t t-name="odex30_account_accountant.NewLoan">
|
||||||
|
<FileUploader
|
||||||
|
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||||
|
fileUploadClass="'new_loan'"
|
||||||
|
multiUpload="false"
|
||||||
|
onUploaded.bind="onFileUploaded"
|
||||||
|
>
|
||||||
|
<t t-set-slot="toggler">
|
||||||
|
<span groups="account.group_account_invoice">
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</FileUploader>
|
||||||
|
<span class="ms-1" groups="account.group_account_invoice">
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
t-on-click="openComputeWizard"
|
||||||
|
>
|
||||||
|
Compute
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useState, onWillStart } from "@odoo/owl";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { ImportAction } from "@base_import/import_action/import_action";
|
||||||
|
import { BaseImportModel } from "@base_import/import_model";
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLoanImportModel extends BaseImportModel {
|
||||||
|
async init() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class AccountLoanImportAction extends ImportAction {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
|
||||||
|
this.action = useService("action");
|
||||||
|
|
||||||
|
this.model = useState(new AccountLoanImportModel({
|
||||||
|
env: this.env,
|
||||||
|
resModel: this.resModel,
|
||||||
|
context: this.props.action.params.context || {},
|
||||||
|
orm: this.orm,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
if (this.props.action.params.context) {
|
||||||
|
this.model.id = this.props.action.params.context.wizard_id;
|
||||||
|
await super.handleFilesUpload([{ name: this.props.action.params.filename }])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async exit() {
|
||||||
|
if (this.model.resModel === "account.loan.line") {
|
||||||
|
const action = await this.orm.call("account.loan", "action_file_uploaded", [this.model.context.default_loan_id]);
|
||||||
|
return this.action.doAction(action);
|
||||||
|
}
|
||||||
|
super.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("actions").add("import_loan", AccountLoanImportAction);
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
.o_view_nocontent {
|
||||||
|
.o_view_nocontent_amortization {
|
||||||
|
@extend %o-nocontent-init-image;
|
||||||
|
width: 600px;
|
||||||
|
height: 300px;
|
||||||
|
background: transparent url(/account_loans/static/src/img/amortization.svg) no-repeat center;
|
||||||
|
background-size: 300px 230px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_loan_management
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
from odoo import Command, fields
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tools import file_open
|
||||||
|
|
||||||
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
|
from odoo.addons.account_loans import _account_loans_add_date_column
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestLoanManagement(AccountTestInvoicingCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.loan_journal = cls.env['account.journal'].search([
|
||||||
|
('company_id', '=', cls.company.id),
|
||||||
|
('type', '=', 'general'),
|
||||||
|
('id', '!=', cls.company.currency_exchange_journal_id.id),
|
||||||
|
], limit=1)
|
||||||
|
cls.long_term_account = cls.env['account.account'].search([
|
||||||
|
('company_ids', '=', cls.company.id),
|
||||||
|
('account_type', '=', 'liability_non_current'),
|
||||||
|
], limit=1)
|
||||||
|
cls.short_term_account = cls.env['account.account'].search([
|
||||||
|
('company_ids', '=', cls.company.id),
|
||||||
|
('account_type', '=', 'liability_current'),
|
||||||
|
], limit=1)
|
||||||
|
cls.expense_account = cls.env['account.account'].search([
|
||||||
|
('company_ids', '=', cls.company.id),
|
||||||
|
('account_type', '=', 'expense'),
|
||||||
|
('id', '!=', cls.company.account_journal_early_pay_discount_loss_account_id.id),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
def create_loan(self, name, date, duration, amount_borrowed, interest, validate=False, skip_until_date=False):
|
||||||
|
loan = self.env['account.loan'].create({
|
||||||
|
'name': name,
|
||||||
|
'date': date,
|
||||||
|
'duration': duration,
|
||||||
|
'amount_borrowed': amount_borrowed,
|
||||||
|
'interest': interest,
|
||||||
|
'skip_until_date': skip_until_date,
|
||||||
|
'journal_id': self.loan_journal.id,
|
||||||
|
'long_term_account_id': self.long_term_account.id,
|
||||||
|
'short_term_account_id': self.short_term_account.id,
|
||||||
|
'expense_account_id': self.expense_account.id,
|
||||||
|
'line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'date': fields.Date.to_date(date) + relativedelta(months=month),
|
||||||
|
'principal': amount_borrowed / duration,
|
||||||
|
'interest': interest / duration,
|
||||||
|
}) for month in range(duration)
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if validate:
|
||||||
|
loan.action_confirm()
|
||||||
|
return loan
|
||||||
|
|
||||||
|
@freeze_time('2024-07-31')
|
||||||
|
def test_loan_values(self):
|
||||||
|
"""Test that the loan values are correctly computed"""
|
||||||
|
# Create the loan
|
||||||
|
loan = self.create_loan('Odoomobile Loan 🚗', '2024-01-01', 2 * 12, 24_000, 2_400, validate=True)
|
||||||
|
|
||||||
|
# Verify that the outstanding balance of the loan is correct
|
||||||
|
self.assertEqual(loan.outstanding_balance, 17_000) # = 24_000 - (1_000 of principal * 7 months (Jan -> July))
|
||||||
|
|
||||||
|
# Verify that the loan lines are correct (computed fields)
|
||||||
|
self.assertEqual(len(loan.line_ids), 24) # 24 months
|
||||||
|
self.assertRecordValues(loan.line_ids[0] | loan.line_ids[4] | loan.line_ids[-1], [{
|
||||||
|
'date': fields.Date.to_date('2024-01-01'),
|
||||||
|
'principal': 1_000,
|
||||||
|
'interest': 100,
|
||||||
|
'payment': 1_100,
|
||||||
|
'outstanding_balance': 23_000, # = 24_000 - 1_000 principal * 1 month
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2024-05-01'),
|
||||||
|
'principal': 1_000,
|
||||||
|
'interest': 100,
|
||||||
|
'payment': 1_100,
|
||||||
|
'outstanding_balance': 19_000, # = 24_000 - 1_000 principal * 5 months
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-12-01'),
|
||||||
|
'principal': 1_000,
|
||||||
|
'interest': 100,
|
||||||
|
'payment': 1_100,
|
||||||
|
'outstanding_balance': 0,
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Verify that the generated moves are correct
|
||||||
|
payment_moves = loan.line_ids.generated_move_ids.filtered(lambda m: len(m.line_ids) == 3) # payments moves have 3 lines (principal + interest = payment)
|
||||||
|
self.assertEqual(len(payment_moves), 24) # 24 months
|
||||||
|
reclassification_moves = (loan.line_ids.generated_move_ids - payment_moves).filtered(lambda m: not m.reversed_entry_id) # reclassification moves have 2 lines (moving principal from one account to another) and have no link to a reversed entry
|
||||||
|
self.assertEqual(len(reclassification_moves), 23) # one less because we have an offset of one month (the first month is already started and should not be reclassified)
|
||||||
|
reclassification_reverse_moves = (loan.line_ids.generated_move_ids - payment_moves) - reclassification_moves
|
||||||
|
self.assertEqual(len(reclassification_reverse_moves), 23) # same
|
||||||
|
|
||||||
|
# Verify that the payment_moves are correct
|
||||||
|
self.assertRecordValues(payment_moves[0] | payment_moves[11] | payment_moves[15] | payment_moves[-1], [{
|
||||||
|
'date': fields.Date.to_date('2024-01-31'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Principal & Interest 01/2024",
|
||||||
|
'amount_total': 1_100, # 1_000 principal + 100 interest
|
||||||
|
'generating_loan_line_id': loan.line_ids[0].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2024-12-31'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Principal & Interest 12/2024",
|
||||||
|
'amount_total': 1_100, # 1_000 principal + 100 interest
|
||||||
|
'generating_loan_line_id': loan.line_ids[11].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-04-30'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Principal & Interest 04/2025",
|
||||||
|
'amount_total': 1_100, # 1_000 principal + 100 interest
|
||||||
|
'generating_loan_line_id': loan.line_ids[15].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-12-31'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Principal & Interest 12/2025",
|
||||||
|
'amount_total': 1_100, # 1_000 principal + 100 interest
|
||||||
|
'generating_loan_line_id': loan.line_ids[-1].id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
self.assertRecordValues(payment_moves[0].line_ids.sorted(lambda l: -l.debit), [{
|
||||||
|
'name': 'Odoomobile Loan 🚗 - Principal 01/2024',
|
||||||
|
'debit': 1_000,
|
||||||
|
'credit': 0,
|
||||||
|
'account_id': self.long_term_account.id,
|
||||||
|
}, {
|
||||||
|
'name': 'Odoomobile Loan 🚗 - Interest 01/2024',
|
||||||
|
'debit': 100,
|
||||||
|
'credit': 0,
|
||||||
|
'account_id': self.expense_account.id,
|
||||||
|
}, {
|
||||||
|
'name': 'Odoomobile Loan 🚗 - Due 01/2024 (Principal $ 1,000.00 + Interest $ 100.00)',
|
||||||
|
'debit': 0,
|
||||||
|
'credit': 1_100,
|
||||||
|
'account_id': self.short_term_account.id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Verify that the reclassification moves are correct
|
||||||
|
self.assertRecordValues(reclassification_moves[0] | reclassification_moves[11] | reclassification_moves[15], [{
|
||||||
|
'date': fields.Date.to_date('2024-01-31'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reclassification LT - ST 02/2024 to 01/2025", # offset of 1 month
|
||||||
|
'amount_total': 12_000, # sum of the principals of the next 12 months
|
||||||
|
'generating_loan_line_id': loan.line_ids[0].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2024-12-31'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reclassification LT - ST 01/2025 to 12/2025", # offset of 1 month
|
||||||
|
'amount_total': 12_000, # sum of the principals between 01/25 and 12/25
|
||||||
|
'generating_loan_line_id': loan.line_ids[11].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-04-30'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reclassification LT - ST 05/2025 to 12/2025", # offset of 1 month
|
||||||
|
'amount_total': 8_000, # sum of the principals between 05/25 and 12/25
|
||||||
|
'generating_loan_line_id': loan.line_ids[15].id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
self.assertRecordValues(reclassification_moves[0].line_ids.sorted(lambda l: l.credit), [{
|
||||||
|
'name': f'Odoomobile Loan 🚗 - Reclassification LT - ST 02/2024 to 01/2025 (To {self.short_term_account.code})',
|
||||||
|
'debit': 12_000,
|
||||||
|
'credit': 0,
|
||||||
|
'account_id': self.long_term_account.id,
|
||||||
|
}, {
|
||||||
|
'name': f'Odoomobile Loan 🚗 - Reclassification LT - ST 02/2024 to 01/2025 (From {self.long_term_account.code})',
|
||||||
|
'debit': 0,
|
||||||
|
'credit': 12_000,
|
||||||
|
'account_id': self.short_term_account.id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Verify that the reverse reclassification moves are correct
|
||||||
|
self.assertRecordValues(reclassification_reverse_moves[0] | reclassification_reverse_moves[11] | reclassification_reverse_moves[15], [{
|
||||||
|
'date': fields.Date.to_date('2024-02-01'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reversal reclassification LT - ST 02/2024 to 01/2025", # offset of 1 month
|
||||||
|
'amount_total': 12_000, # sum of the principals of the next 12 months
|
||||||
|
'generating_loan_line_id': loan.line_ids[0].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-01-01'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reversal reclassification LT - ST 01/2025 to 12/2025", # offset of 1 month
|
||||||
|
'amount_total': 12_000, # sum of the principals between 01/25 and 12/25
|
||||||
|
'generating_loan_line_id': loan.line_ids[11].id,
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.to_date('2025-05-01'),
|
||||||
|
'ref': "Odoomobile Loan 🚗 - Reversal reclassification LT - ST 05/2025 to 12/2025", # offset of 1 month
|
||||||
|
'amount_total': 8_000, # sum of the principals between 05/25 and 12/25
|
||||||
|
'generating_loan_line_id': loan.line_ids[15].id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
self.assertRecordValues(reclassification_reverse_moves[0].line_ids.sorted(lambda l: l.debit), [{
|
||||||
|
'name': f'Odoomobile Loan 🚗 - Reversal reclassification LT - ST 02/2024 to 01/2025 (To {self.short_term_account.code})',
|
||||||
|
'credit': 12_000,
|
||||||
|
'debit': 0,
|
||||||
|
'account_id': self.long_term_account.id,
|
||||||
|
}, {
|
||||||
|
'name': f'Odoomobile Loan 🚗 - Reversal reclassification LT - ST 02/2024 to 01/2025 (From {self.long_term_account.code})',
|
||||||
|
'credit': 0,
|
||||||
|
'debit': 12_000,
|
||||||
|
'account_id': self.short_term_account.id,
|
||||||
|
}])
|
||||||
|
|
||||||
|
@freeze_time('2024-07-31')
|
||||||
|
def test_loan_states(self):
|
||||||
|
"""Test the flow of the loan: Draft, Running, Closed, Cancelled"""
|
||||||
|
# Create the loan
|
||||||
|
loan = self.create_loan('Odoomobile Loan 🚗', '2024-01-01', 12, 24_000, 2_400)
|
||||||
|
|
||||||
|
# Verify that the loan is in draft
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertFalse(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Verify that the loan is running
|
||||||
|
loan.action_confirm()
|
||||||
|
self.assertEqual(loan.state, 'running')
|
||||||
|
self.assertTrue(loan.line_ids.generated_move_ids)
|
||||||
|
self.assertTrue(
|
||||||
|
any(m.state == 'posted' for m in loan.line_ids.generated_move_ids)
|
||||||
|
and
|
||||||
|
any(m.state == 'draft' for m in loan.line_ids.generated_move_ids)
|
||||||
|
) # Mix of draft & posted entries
|
||||||
|
|
||||||
|
# Verify that the loan is cancelled
|
||||||
|
loan.action_cancel()
|
||||||
|
self.assertEqual(loan.state, 'cancelled')
|
||||||
|
self.assertFalse(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Verify that we can reset to draft the loan
|
||||||
|
loan.action_set_to_draft()
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertFalse(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Run it again
|
||||||
|
loan.action_confirm()
|
||||||
|
self.assertEqual(loan.state, 'running')
|
||||||
|
self.assertTrue(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Close the loan, only draft entries should be removed
|
||||||
|
action = loan.action_close()
|
||||||
|
wizard = self.env[action['res_model']].browse(action['res_id'])
|
||||||
|
wizard.date = fields.Date.to_date('2024-10-31')
|
||||||
|
wizard.action_save()
|
||||||
|
self.assertEqual(loan.state, 'closed')
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 30) # = 24 payment moves + 23 reclassification moves + 23 reversed reclass - (14 + 13 + 13) cancelled moves
|
||||||
|
|
||||||
|
# Reset to draft, the entries should be removed
|
||||||
|
loan.action_set_to_draft()
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertFalse(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Create a new loan that should be automatically closed when the last generated move is posted
|
||||||
|
loan2 = self.create_loan('Odoomobile Loan 🚗', '2024-01-01', 12, 24_000, 2_400, validate=True)
|
||||||
|
self.assertEqual(loan2.state, 'running')
|
||||||
|
with freeze_time('2024-12-31'):
|
||||||
|
self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger()
|
||||||
|
self.assertEqual(loan2.state, 'closed')
|
||||||
|
|
||||||
|
@freeze_time('2024-06-23')
|
||||||
|
def test_loan_states_with_audit_trail(self):
|
||||||
|
"""Test the flow of the loan: Draft, Running, Closed, Cancelled"""
|
||||||
|
self.company.check_account_audit_trail = True
|
||||||
|
# Create the loan
|
||||||
|
loan = self.create_loan('Odoomobile Loan 🚗', '2024-01-01', 12, 24_000, 2_400)
|
||||||
|
|
||||||
|
# Verify that the loan is in draft
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertFalse(loan.line_ids.generated_move_ids)
|
||||||
|
|
||||||
|
# Verify that the loan is running
|
||||||
|
loan.action_confirm()
|
||||||
|
self.assertEqual(loan.state, 'running')
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted')), 15)
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 34)
|
||||||
|
|
||||||
|
# Verify that the loan is cancelled
|
||||||
|
loan.action_cancel()
|
||||||
|
self.assertEqual(loan.state, 'cancelled')
|
||||||
|
self.assertFalse(len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted')))
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 15)
|
||||||
|
|
||||||
|
# Verify that we can reset to draft the loan
|
||||||
|
loan.action_set_to_draft()
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 15)
|
||||||
|
|
||||||
|
# Run it again
|
||||||
|
loan.action_confirm()
|
||||||
|
self.assertEqual(loan.state, 'running')
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted')), 15)
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 49)
|
||||||
|
|
||||||
|
# Close the loan, only draft entries should be removed
|
||||||
|
action = loan.action_close()
|
||||||
|
wizard = self.env[action['res_model']].browse(action['res_id'])
|
||||||
|
wizard.action_save()
|
||||||
|
self.assertEqual(loan.state, 'closed')
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted')), 15)
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 33)
|
||||||
|
|
||||||
|
# Reset to draft, the entries should be removed
|
||||||
|
loan.action_set_to_draft()
|
||||||
|
self.assertEqual(loan.state, 'draft')
|
||||||
|
self.assertFalse(len(loan.line_ids.generated_move_ids.filtered(lambda m: m.state == 'posted')))
|
||||||
|
self.assertEqual(len(loan.line_ids.generated_move_ids), 30)
|
||||||
|
|
||||||
|
@freeze_time('2024-01-01')
|
||||||
|
def test_loan_import_amortization_schedule(self):
|
||||||
|
"""Test that we can import an amortization schedule from a file"""
|
||||||
|
# Upload the file from the List View -> Create a new Loan
|
||||||
|
with file_open('account_loans/demo/files/loan_amortization_demo.csv', 'rb') as f:
|
||||||
|
attachment = self.env['ir.attachment'].create({
|
||||||
|
'name': 'loan_amortization_demo.csv',
|
||||||
|
'raw': f.read(),
|
||||||
|
})
|
||||||
|
attachment = _account_loans_add_date_column(attachment) # fill the date column from -1 year to +3 years
|
||||||
|
|
||||||
|
action = self.env['account.loan'].action_upload_amortization_schedule(attachment.id)
|
||||||
|
loan = self.env['account.loan'].browse(action.get('params', {}).get('context', {}).get('default_loan_id'))
|
||||||
|
import_wizard = self.env['base_import.import'].browse(action.get('params', {}).get('context', {}).get('wizard_id'))
|
||||||
|
result = import_wizard.parse_preview({
|
||||||
|
'quoting': '"',
|
||||||
|
'separator': ',',
|
||||||
|
'date_format': '%Y-%m-%d',
|
||||||
|
'has_headers': True,
|
||||||
|
})
|
||||||
|
import_wizard.with_context(default_loan_id=loan.id).execute_import(
|
||||||
|
['date', 'principal', 'interest'],
|
||||||
|
[],
|
||||||
|
result["options"],
|
||||||
|
)
|
||||||
|
loan.action_file_uploaded()
|
||||||
|
loan.write({
|
||||||
|
'journal_id': self.loan_journal.id,
|
||||||
|
'long_term_account_id': self.long_term_account.id,
|
||||||
|
'short_term_account_id': self.short_term_account.id,
|
||||||
|
'expense_account_id': self.expense_account.id,
|
||||||
|
})
|
||||||
|
loan.action_confirm()
|
||||||
|
|
||||||
|
self.assertRecordValues(loan, [{
|
||||||
|
'date': fields.Date.from_string('2023-01-01'),
|
||||||
|
'state': 'running',
|
||||||
|
'name': attachment.name,
|
||||||
|
'amount_borrowed': 19_900.25, # Sum of principals
|
||||||
|
'outstanding_balance': 15_981.65, # = 19_900.25 - principal of first 12 lines
|
||||||
|
}])
|
||||||
|
|
||||||
|
self.assertEqual(len(loan.line_ids), 48) # 4 years
|
||||||
|
self.assertRecordValues(loan.line_ids[0] | loan.line_ids[-1], [{
|
||||||
|
'date': fields.Date.from_string('2023-01-01'),
|
||||||
|
'principal': 304.60,
|
||||||
|
'interest': 250.00,
|
||||||
|
'payment': 554.6,
|
||||||
|
'outstanding_balance': 19_595.65, # = 19_900.25 - 304.60
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.from_string('2026-12-01'),
|
||||||
|
'principal': 547.72,
|
||||||
|
'interest': 6.88,
|
||||||
|
'payment': 554.6,
|
||||||
|
'outstanding_balance': 0,
|
||||||
|
}])
|
||||||
|
|
||||||
|
# Upload the file from the Form View -> Update the current Loan
|
||||||
|
loan2 = self.create_loan('Loan 2', '2024-01-01', 2 * 12, 24_000, 2_400, validate=True)
|
||||||
|
self.assertEqual(len(loan2.line_ids), 24)
|
||||||
|
|
||||||
|
# Override all previous lines, and recompute amount_borrowed, date, ...
|
||||||
|
action = loan2.action_upload_amortization_schedule(attachment.id)
|
||||||
|
self.assertEqual(action.get('params', {}).get('context', {}).get('default_loan_id'), loan2.id)
|
||||||
|
import_wizard = self.env['base_import.import'].browse(action.get('params', {}).get('context', {}).get('wizard_id'))
|
||||||
|
result = import_wizard.parse_preview({
|
||||||
|
'quoting': '"',
|
||||||
|
'separator': ',',
|
||||||
|
'date_format': '%Y-%m-%d',
|
||||||
|
'has_headers': True,
|
||||||
|
})
|
||||||
|
import_wizard.with_context(default_loan_id=loan2.id).execute_import(
|
||||||
|
['date', 'principal', 'interest'],
|
||||||
|
[],
|
||||||
|
result["options"],
|
||||||
|
)
|
||||||
|
loan2.action_file_uploaded()
|
||||||
|
loan2.action_confirm()
|
||||||
|
|
||||||
|
self.assertRecordValues(loan, [{
|
||||||
|
'date': fields.Date.from_string('2023-01-01'),
|
||||||
|
'state': 'running',
|
||||||
|
'name': attachment.name,
|
||||||
|
'amount_borrowed': 19_900.25, # Sum of principals
|
||||||
|
'outstanding_balance': 15_981.65, # = 19_900.25 - principal of first 12 lines
|
||||||
|
}])
|
||||||
|
|
||||||
|
self.assertEqual(len(loan.line_ids), 48) # 4 years
|
||||||
|
self.assertRecordValues(loan.line_ids[0] | loan.line_ids[-1], [{
|
||||||
|
'date': fields.Date.from_string('2023-01-01'),
|
||||||
|
'principal': 304.60,
|
||||||
|
'interest': 250.00,
|
||||||
|
'payment': 554.6,
|
||||||
|
'outstanding_balance': 19_595.65, # = 19_900.25 - 304.60
|
||||||
|
}, {
|
||||||
|
'date': fields.Date.from_string('2026-12-01'),
|
||||||
|
'principal': 547.72,
|
||||||
|
'interest': 6.88,
|
||||||
|
'payment': 554.6,
|
||||||
|
'outstanding_balance': 0,
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_loan_zero_interest(self):
|
||||||
|
loan = self.env['account.loan'].create({'name': '0 interest loan', 'date': '2024-01-01', 'amount_borrowed': 24_000})
|
||||||
|
wizard = self.env['account.loan.compute.wizard'].browse(loan.action_open_compute_wizard()['res_id'])
|
||||||
|
wizard.interest_rate = 0
|
||||||
|
wizard.action_save()
|
||||||
|
self.assertEqual(len(loan.line_ids), 12) # default loan term is 1 year = 12 months
|
||||||
|
self.assertTrue(all(payment == 2000 for payment in loan.line_ids.mapped('payment'))) # 24,000 / 12 months = 2,000/month
|
||||||
|
|
||||||
|
@freeze_time('2024-07-31')
|
||||||
|
def test_loan_skip_until_date(self):
|
||||||
|
"""Test the skip_until_date field"""
|
||||||
|
loan = self.create_loan('Odoomobile Loan 🚗', '2024-01-01', 12, 24_000, 2_400, validate=True, skip_until_date='2024-05-15')
|
||||||
|
|
||||||
|
self.assertEqual(loan.state, 'running')
|
||||||
|
self.assertTrue(loan.line_ids.generated_move_ids)
|
||||||
|
# Outstanding balance should be 24_000 - 2_000 * 7 months (Jan -> July), including skipped period
|
||||||
|
self.assertEqual(loan.outstanding_balance, 10_000)
|
||||||
|
|
||||||
|
@freeze_time('2025-01-01')
|
||||||
|
def test_loan_skip_until_date_2(self):
|
||||||
|
"""Test loan closing when skip_until_date field is set"""
|
||||||
|
loan = self.env['account.loan'].create({
|
||||||
|
'name': 'Test',
|
||||||
|
'date': '2024-01-01',
|
||||||
|
'duration': 12,
|
||||||
|
'amount_borrowed': 20_000,
|
||||||
|
'interest': 107.87,
|
||||||
|
'skip_until_date': '2024-10-31',
|
||||||
|
'journal_id': self.loan_journal.id,
|
||||||
|
'long_term_account_id': self.long_term_account.id,
|
||||||
|
'short_term_account_id': self.short_term_account.id,
|
||||||
|
'expense_account_id': self.expense_account.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
wizard = self.env['account.loan.compute.wizard'].browse(loan.action_open_compute_wizard()['res_id'])
|
||||||
|
wizard.action_save()
|
||||||
|
loan.action_confirm()
|
||||||
|
|
||||||
|
self.assertTrue(loan.line_ids.generated_move_ids)
|
||||||
|
self.assertEqual(loan.state, 'closed')
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
<record id="asset_group_form_view_inherit_loan" model="ir.ui.view">
|
||||||
|
<field name="name">account.asset.group.form</field>
|
||||||
|
<field name="model">account.asset.group</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_asset.asset_group_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="action_open_linked_loans"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-bars"
|
||||||
|
invisible="count_linked_loans == 0">
|
||||||
|
<field string="Loan(s)" name="count_linked_loans" widget="statinfo"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_account_asset_form_inherit_loan" model="ir.ui.view">
|
||||||
|
<field name="name">account.asset.form</field>
|
||||||
|
<field name="model">account.asset</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_asset.view_account_asset_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="action_open_linked_loans"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-bars"
|
||||||
|
invisible="count_linked_loans == 0">
|
||||||
|
<field string="Related Loan(s)" name="count_linked_loans" widget="statinfo"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<!-- ACCOUNT LOAN-->
|
||||||
|
|
||||||
|
<record id="account_loan_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.form</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Loan">
|
||||||
|
<header>
|
||||||
|
<button
|
||||||
|
name="action_confirm" string="Confirm" class="btn btn-primary"
|
||||||
|
type="object" groups="account.group_account_invoice"
|
||||||
|
invisible="state != 'draft'"
|
||||||
|
/>
|
||||||
|
<widget name="new_loan" invisible="line_ids"/>
|
||||||
|
<button
|
||||||
|
name="action_reset" string="Reset" class="btn btn-secondary"
|
||||||
|
type="object" groups="account.group_account_manager"
|
||||||
|
invisible="state != 'draft' or not line_ids"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
name="action_close" string="Close" class="btn btn-secondary"
|
||||||
|
type="object" groups="account.group_account_manager"
|
||||||
|
invisible="state != 'running'"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
name="action_set_to_draft" string="Set to Draft" class="btn btn-secondary"
|
||||||
|
type="object" groups="account.group_account_manager"
|
||||||
|
invisible="state in ('draft', 'running')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
name="action_cancel" string="Cancel" class="btn btn-secondary"
|
||||||
|
type="object" groups="account.group_account_manager"
|
||||||
|
invisible="state != 'running'"
|
||||||
|
/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,running,closed"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-bars"
|
||||||
|
type="object"
|
||||||
|
name="action_open_loan_entries"
|
||||||
|
invisible="state == 'draft'"
|
||||||
|
>
|
||||||
|
<field string="Posted Entries" name="nb_posted_entries" widget="statinfo"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-bars"
|
||||||
|
type="object"
|
||||||
|
name="action_open_linked_assets"
|
||||||
|
invisible="count_linked_assets == 0"
|
||||||
|
>
|
||||||
|
<field string="Related Asset(s)" name="count_linked_assets" widget="statinfo"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<field name="company_id" invisible="1"/> <!-- Needed so the company_id is fetched when creating a new record -->
|
||||||
|
<field name="currency_id" invisible="1"/> <!-- Needed for monetary widget -->
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Loan name"/>
|
||||||
|
</h1>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="amount_borrowed" widget="monetary" decoration-danger="amount_borrowed_difference != 0" readonly="state != 'draft'"/>
|
||||||
|
<field name="interest" widget="monetary" decoration-danger="interest_difference != 0" readonly="state != 'draft'"/>
|
||||||
|
<field name="outstanding_balance" widget="monetary"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="date" decoration-danger="is_wrong_date" readonly="state != 'draft'"/>
|
||||||
|
<label for="duration" string="Duration"/>
|
||||||
|
<div>
|
||||||
|
<field name="duration" class="oe_inline" decoration-danger="duration_difference != 0" readonly="state != 'draft'"/>
|
||||||
|
<span class="oe_inline" invisible="duration == 1">months</span>
|
||||||
|
<span class="oe_inline" invisible="duration != 1">month</span>
|
||||||
|
</div>
|
||||||
|
<field name="asset_group_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
|
<field name="loan_properties" columns="2"/>
|
||||||
|
<notebook>
|
||||||
|
<page string="Amortization schedule" name="amortization_schedule">
|
||||||
|
<field name="line_ids" readonly="state != 'draft'"/>
|
||||||
|
</page>
|
||||||
|
<page string="Loan Settings" name="main_page">
|
||||||
|
<group>
|
||||||
|
<group name="loan_settings_column1">
|
||||||
|
<field name="long_term_account_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="short_term_account_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="expense_account_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="journal_id" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group name="loan_settings_column2">
|
||||||
|
<field name="skip_until_date" readonly="state != 'draft'" placeholder="1st amortization schedule"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_list_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.list</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Loans"
|
||||||
|
decoration-info="state == 'draft'"
|
||||||
|
decoration-muted="state in ('closed', 'cancelled')"
|
||||||
|
>
|
||||||
|
<field name="currency_id" column_invisible="1"/> <!-- Needed for monetary widget -->
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date" string="Start Date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="amount_borrowed" sum="Total Amounts Borrowed"/>
|
||||||
|
<field name="outstanding_balance" sum="Total Outstanding Balance"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'running'" decoration-danger="state in ('closed', 'cancelled')"/>
|
||||||
|
<field name="long_term_account_id" optional="hide"/>
|
||||||
|
<field name="short_term_account_id" optional="hide"/>
|
||||||
|
<field name="expense_account_id" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_search_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.search</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Loan">
|
||||||
|
<field name="name" string="Loan" filter_domain="['|', ('name', 'ilike', self), ('display_name', 'ilike', self)]"/>
|
||||||
|
<field name="asset_group_id"/>
|
||||||
|
<field name="loan_properties"/>
|
||||||
|
<filter string="Current" name="current" domain="[('state', 'in', ('draft', 'running'))]" help="Draft & Running Loans"/>
|
||||||
|
<filter string="Closed" name="closed" domain="[('state', '=', 'closed')]" help="Closed Loans"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Date" name="date" date="date"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Asset Group" name="group_by_asset_group" domain="[]" context="{'group_by': 'asset_group_id'}"/>
|
||||||
|
<filter string="Properties" name="group_by_properties" domain="[]" context="{'group_by': 'loan_properties'}"/>
|
||||||
|
<filter string="Date" name="group_by_date" domain="[]" context="{'group_by': 'date'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_kanban_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.search</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_mobile" sample="1">
|
||||||
|
<field name="currency_id"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="row mb4">
|
||||||
|
<field name="name" class="fw-bold fs-5 col-10"/>
|
||||||
|
<div class="col-2">
|
||||||
|
<field name="state" class="float-end" widget="label_selection" options="{'classes': {'draft': 'primary', 'running': 'success', 'closed': 'default'}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5">
|
||||||
|
<field name="amount_borrowed" widget='monetary'/>
|
||||||
|
</div>
|
||||||
|
<div class="col-7 text-end">
|
||||||
|
<field name="date"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_pivot_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.pivot</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot sample="1">
|
||||||
|
<field name="date" interval="year" type="col"/>
|
||||||
|
<field name="name" type="row"/>
|
||||||
|
<field name="amount_borrowed" type="measure"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_graph_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.graph</field>
|
||||||
|
<field name="model">account.loan</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph type="line" sample="1">
|
||||||
|
<field name="date" interval="year"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="amount_borrowed" type="measure"/>
|
||||||
|
<field name="interest" type="measure"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACCOUNT LOAN LINE-->
|
||||||
|
|
||||||
|
<record id="account_loan_line_list_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.line.list</field>
|
||||||
|
<field name="model">account.loan.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Loan lines" editable="bottom" delete="1" limit="80" decoration-info="not is_payment_move_posted">
|
||||||
|
<field name="currency_id" column_invisible="1"/> <!-- Needed for monetary widget -->
|
||||||
|
<field name="sequence" width="30px" optional="hide"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="principal" string="Principals" sum="Total principals"/>
|
||||||
|
<field name="interest" string="Interests" sum="Total interests"/>
|
||||||
|
<field name="payment" string="Payments" sum="Total payments"/>
|
||||||
|
<field name="outstanding_balance"/>
|
||||||
|
<field name="long_term_theoretical_balance" optional="hide"/>
|
||||||
|
<field name="short_term_theoretical_balance" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_line_search_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.line.search</field>
|
||||||
|
<field name="model">account.loan.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Loan">
|
||||||
|
<field name="loan_name" string="Loan" filter_domain="[('loan_name', 'ilike', self)]"/>
|
||||||
|
<filter string="Current" name="current" domain="[('loan_state', 'in', ('draft', 'running'))]" help="Draft & Running Loans"/>
|
||||||
|
<filter string="Closed" name="closed" domain="[('loan_state', '=', 'closed')]" help="Closed Loans"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Loan Date" name="loan_line_date" date="date"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Loan" name="group_by_loan" domain="[]" context="{'group_by': 'loan_id'}"/>
|
||||||
|
<filter string="Loan Date" name="group_by_loan_line_date" domain="[]" context="{'group_by': 'date'}"/>
|
||||||
|
<filter string="Asset Group" name="group_by_asset_group" domain="[]" context="{'group_by': 'loan_asset_group_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="account_loan_line_pivot_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.line.pivot</field>
|
||||||
|
<field name="model">account.loan.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Loans Analysis" sample="1">
|
||||||
|
<field name="date" interval="year" type="col"/>
|
||||||
|
<field name="loan_id" type="row"/>
|
||||||
|
<field name="principal" type="measure"/>
|
||||||
|
<field name="interest" type="measure"/>
|
||||||
|
<field name="payment" type="measure"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACCOUNT MOVE LIST VIEW-->
|
||||||
|
|
||||||
|
<record id="account_loan_view_account_move_list_view" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.list</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_tree"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//list" position="attributes">
|
||||||
|
<attribute name="default_order">date,name,ref</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ACTIONS -->
|
||||||
|
|
||||||
|
<record id="action_view_account_loans" model="ir.actions.act_window">
|
||||||
|
<field name="name">Loans</field>
|
||||||
|
<field name="path">loans</field>
|
||||||
|
<field name="res_model">account.loan</field>
|
||||||
|
<field name="search_view_id" ref="account_loan_search_view"/>
|
||||||
|
<field name="context">{'search_default_current':1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'list', 'view_id': ref('account_loan_list_view')}),
|
||||||
|
(0, 0, {'view_mode': 'form', 'view_id': ref('account_loan_form_view')}),
|
||||||
|
(0, 0, {'view_mode': 'kanban', 'view_id': ref('account_loan_kanban_view')}),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('account_loan_pivot_view')}),
|
||||||
|
(0, 0, {'view_mode': 'graph', 'view_id': ref('account_loan_graph_view')}),
|
||||||
|
]"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_amortization"/>
|
||||||
|
<h2>
|
||||||
|
Manage Your Acquired Loans with Automated Adjustments.
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Set up your amortization schedule, or import it, and let Odoo handle the monthly interest and principal adjustments automatically.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_account_loans_analysis" model="ir.actions.act_window">
|
||||||
|
<field name="name">Loans Analysis</field>
|
||||||
|
<field name="path">loans-analysis</field>
|
||||||
|
<field name="res_model">account.loan.line</field>
|
||||||
|
<field name="search_view_id" ref="account_loan_line_search_view"/>
|
||||||
|
<field name="context">{'search_default_current':1}</field>
|
||||||
|
<field name="view_ids" eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'pivot', 'view_id': ref('account_loan_line_pivot_view')}),
|
||||||
|
]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- MENU ITEMS -->
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_action_loans"
|
||||||
|
sequence="62"
|
||||||
|
parent="account.menu_finance_entries"
|
||||||
|
action="action_view_account_loans"
|
||||||
|
groups="account.group_account_readonly"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_action_loans_analysis"
|
||||||
|
name="Loans Analysis"
|
||||||
|
action="action_view_account_loans_analysis"
|
||||||
|
parent="account.account_reports_management_menu"
|
||||||
|
groups="account.group_account_readonly"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_move_form_inherit_loan" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.form.inherit</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="odex30_account_accountant.view_move_form_inherit"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button
|
||||||
|
name="open_loan"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-bars"
|
||||||
|
type="object"
|
||||||
|
invisible="not generating_loan_line_id"
|
||||||
|
string="Related Loan"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import account_loan_compute_wizard
|
||||||
|
from . import account_loan_close_wizard
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from odoo import models, fields, _
|
||||||
|
from odoo.tools import format_date
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCloseWizard(models.TransientModel):
|
||||||
|
_name = 'account.loan.close.wizard'
|
||||||
|
_description = 'Close Loan Wizard'
|
||||||
|
|
||||||
|
loan_id = fields.Many2one(
|
||||||
|
comodel_name='account.loan',
|
||||||
|
string='Loan',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
date = fields.Date(
|
||||||
|
string='Close Date',
|
||||||
|
default=fields.Date.context_today,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_save(self):
|
||||||
|
self.loan_id.line_ids.generated_move_ids.filtered(lambda m: m.generating_loan_line_id.date > self.date and m.state == 'draft').unlink()
|
||||||
|
self.loan_id.state = 'closed'
|
||||||
|
self.loan_id.message_post(body=_("Closed on the %(date)s", date=format_date(self.env, self.date)))
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_account_loan_close_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.close.wizard.form</field>
|
||||||
|
<field name="model">account.loan.close.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
All draft entries after the
|
||||||
|
<field name="date" class="oe-inline col-1"/>
|
||||||
|
will be deleted and the loan will be marked as closed.
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button string="Apply" name="action_save" type="object" class="btn-primary" data-hotkey="q"/>
|
||||||
|
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_account_loan_close_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Close Loan Wizard</field>
|
||||||
|
<field name="res_model">account.loan.close.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_account_loan_close_wizard"/>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import models, fields, _, api
|
||||||
|
from odoo.tools.misc import format_date
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
from ..lib import pyloan
|
||||||
|
|
||||||
|
|
||||||
|
class AccountLoanComputeWizard(models.TransientModel):
|
||||||
|
_name = 'account.loan.compute.wizard'
|
||||||
|
_description = 'Loan Compute Wizard'
|
||||||
|
|
||||||
|
loan_id = fields.Many2one(
|
||||||
|
comodel_name='account.loan',
|
||||||
|
string='Loan',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
currency_id = fields.Many2one(related='loan_id.currency_id')
|
||||||
|
loan_amount = fields.Monetary(
|
||||||
|
string='Loan Amount',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
interest_rate = fields.Float(
|
||||||
|
string='Interest Rate',
|
||||||
|
default=1.0,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
loan_term = fields.Integer(
|
||||||
|
string='Loan Term',
|
||||||
|
default=1,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
start_date = fields.Date(
|
||||||
|
string='Start Date',
|
||||||
|
required=True,
|
||||||
|
default=fields.Date.context_today,
|
||||||
|
)
|
||||||
|
first_payment_date = fields.Date(
|
||||||
|
string='First Payment',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: fields.Date.context_today(self).replace(day=1) + relativedelta(months=1), # first day of next month
|
||||||
|
)
|
||||||
|
payment_end_of_month = fields.Selection(
|
||||||
|
string='Payment',
|
||||||
|
selection=[
|
||||||
|
('end_of_month', 'End of Month'),
|
||||||
|
('at_anniversary', 'At Anniversary'),
|
||||||
|
],
|
||||||
|
default='end_of_month',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
compounding_method = fields.Selection(
|
||||||
|
string='Compounding Method',
|
||||||
|
selection=[
|
||||||
|
('30A/360', '30A/360'),
|
||||||
|
('30U/360', '30U/360'),
|
||||||
|
('30E/360', '30E/360'),
|
||||||
|
('30E/360 ISDA', '30E/360 ISDA'),
|
||||||
|
('A/360', 'A/360'),
|
||||||
|
('A/365F', 'A/365F'),
|
||||||
|
('A/A ISDA', 'A/A ISDA'),
|
||||||
|
('A/A AFB', 'A/A AFB'),
|
||||||
|
],
|
||||||
|
default='30E/360',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
preview = fields.Text(compute='_compute_preview')
|
||||||
|
|
||||||
|
# Onchange
|
||||||
|
@api.onchange('loan_amount', 'interest_rate', 'loan_term', 'start_date', 'first_payment_date')
|
||||||
|
def _onchange_preview(self):
|
||||||
|
if self.loan_amount < 0:
|
||||||
|
raise ValidationError(_("Loan Amount must be positive"))
|
||||||
|
if self.interest_rate < 0 or self.interest_rate > 100:
|
||||||
|
raise ValidationError(_("Interest Rate must be between 0 and 100"))
|
||||||
|
if self.loan_term < 0:
|
||||||
|
raise ValidationError(_("Loan Term must be positive"))
|
||||||
|
if self.first_payment_date and self.start_date and self.start_date + relativedelta(years=self.loan_term) < self.first_payment_date:
|
||||||
|
raise ValidationError(_("The First Payment Date must be before the end of the loan."))
|
||||||
|
|
||||||
|
@api.onchange('start_date')
|
||||||
|
def _onchange_start_date(self):
|
||||||
|
self.first_payment_date = self.start_date and self.start_date.replace(day=1) + relativedelta(months=1) # first day of next month
|
||||||
|
|
||||||
|
# Compute
|
||||||
|
def _get_loan_payment_schedule(self):
|
||||||
|
self.ensure_one()
|
||||||
|
loan = pyloan.Loan(
|
||||||
|
loan_amount=self.loan_amount,
|
||||||
|
interest_rate=self.interest_rate,
|
||||||
|
loan_term=self.loan_term,
|
||||||
|
start_date=format_date(self.env, self.start_date, date_format='yyyy-MM-dd'),
|
||||||
|
first_payment_date=format_date(self.env, self.first_payment_date, date_format='yyyy-MM-dd') if self.first_payment_date and self.payment_end_of_month == 'at_anniversary' else None,
|
||||||
|
payment_end_of_month=self.payment_end_of_month == 'end_of_month',
|
||||||
|
compounding_method=self.compounding_method,
|
||||||
|
loan_type='annuity' if self.interest_rate else 'linear',
|
||||||
|
)
|
||||||
|
if schedule := loan.get_payment_schedule():
|
||||||
|
return schedule[1:] # Skip first line which is always 0 (simply the start of the loan)
|
||||||
|
return []
|
||||||
|
|
||||||
|
@api.depends('loan_amount', 'interest_rate', 'loan_term', 'start_date', 'first_payment_date', 'payment_end_of_month', 'compounding_method')
|
||||||
|
def _compute_preview(self):
|
||||||
|
def get_preview_row(payment):
|
||||||
|
return (
|
||||||
|
f"{format_date(self.env, payment.date): <12} "
|
||||||
|
f"{wizard.currency_id.format(float(payment.principal_amount)):>15} "
|
||||||
|
f"{wizard.currency_id.format(float(payment.interest_amount)):>15} "
|
||||||
|
f"{wizard.currency_id.format(float(payment.payment_amount)):>15} "
|
||||||
|
f"{wizard.currency_id.format(float(payment.loan_balance_amount)):>15}\n"
|
||||||
|
)
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.loan_amount and wizard.loan_term and wizard.start_date:
|
||||||
|
schedule = self._get_loan_payment_schedule()
|
||||||
|
if not schedule:
|
||||||
|
wizard.preview = ''
|
||||||
|
continue
|
||||||
|
preview = "{: <12} {:>15} {:>15} {:>15} {:>15}\n".format(_('Date'), _('Principal'), _('Interest'), _('Payment'), _('Balance'))
|
||||||
|
for payment in schedule[:5]:
|
||||||
|
preview += get_preview_row(payment)
|
||||||
|
preview += "{: <12} {:>15} {:>15} {:>15} {:>15}\n".format("...", "...", "...", "...", "...")
|
||||||
|
for payment in schedule[-5:]:
|
||||||
|
preview += get_preview_row(payment)
|
||||||
|
wizard.preview = preview
|
||||||
|
else:
|
||||||
|
wizard.preview = ''
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
def action_save(self):
|
||||||
|
loan_lines_values = []
|
||||||
|
for payment in self._get_loan_payment_schedule():
|
||||||
|
loan_lines_values.append({
|
||||||
|
'loan_id': self.loan_id.id,
|
||||||
|
'date': payment.date,
|
||||||
|
'principal': float(payment.principal_amount),
|
||||||
|
'interest': float(payment.interest_amount),
|
||||||
|
})
|
||||||
|
self.env['account.loan.line'].create(loan_lines_values)
|
||||||
|
self.loan_id.write({
|
||||||
|
'date': self.start_date,
|
||||||
|
'amount_borrowed': self.loan_amount,
|
||||||
|
'interest': sum(self.loan_id.line_ids.mapped('interest')),
|
||||||
|
'duration': len(self.loan_id.line_ids),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'name': self.loan_id.name,
|
||||||
|
'res_id': self.loan_id.id,
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': self.loan_id._name,
|
||||||
|
'target': 'self',
|
||||||
|
'views': [[False, 'form']],
|
||||||
|
'context': self.env.context,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_account_loan_compute_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">account.loan.compute.wizard.form</field>
|
||||||
|
<field name="model">account.loan.compute.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<field name="currency_id" invisible="1"/> <!-- Needed for monetary widget -->
|
||||||
|
<group>
|
||||||
|
<group name="loan_values">
|
||||||
|
<field name="loan_amount" widget="monetary"/>
|
||||||
|
<label for="interest_rate" string="Interest Rate"/>
|
||||||
|
<div>
|
||||||
|
<field name="interest_rate" class="oe_inline"/>
|
||||||
|
<span class="oe_inline">%</span>
|
||||||
|
</div>
|
||||||
|
<label for="loan_term" string="Loan Term"/>
|
||||||
|
<div>
|
||||||
|
<field name="loan_term" class="oe_inline"/>
|
||||||
|
<span class="oe_inline" invisible="loan_term == 1">years</span>
|
||||||
|
<span class="oe_inline" invisible="loan_term != 1">year</span>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<group name="loan_dates">
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="payment_end_of_month"/>
|
||||||
|
<field name="first_payment_date" invisible="payment_end_of_month == 'end_of_month'"/>
|
||||||
|
<field name="compounding_method"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<label for="preview" string="Preview" invisible="not preview"/>
|
||||||
|
<pre invisible="not preview"><field name="preview"/></pre>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button string="Apply" name="action_save" type="object" class="btn-primary" data-hotkey="q"/>
|
||||||
|
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_view_account_loan_compute_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Loan Compute Wizard</field>
|
||||||
|
<field name="res_model">account.loan.compute.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_account_loan_compute_wizard"/>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -2022,7 +2022,7 @@ class TestBankRecWidget(TestBankRecWidgetCommon):
|
||||||
|
|
||||||
def test_auto_reconcile_cron(self):
|
def test_auto_reconcile_cron(self):
|
||||||
self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink()
|
self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink()
|
||||||
cron = self.env.ref('account_accountant.auto_reconcile_bank_statement_line')
|
cron = self.env.ref('odex30_account_accountant.auto_reconcile_bank_statement_line')
|
||||||
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
|
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
|
||||||
|
|
||||||
st_line = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01')
|
st_line = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01')
|
||||||
|
|
@ -2763,7 +2763,7 @@ class TestBankRecWidget(TestBankRecWidgetCommon):
|
||||||
|
|
||||||
def test_auto_reconcile_cron_with_time_limit(self):
|
def test_auto_reconcile_cron_with_time_limit(self):
|
||||||
self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink()
|
self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink()
|
||||||
cron = self.env.ref('account_accountant.auto_reconcile_bank_statement_line')
|
cron = self.env.ref('odex30_account_accountant.auto_reconcile_bank_statement_line')
|
||||||
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
|
self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink()
|
||||||
|
|
||||||
st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01')
|
st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01')
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Keeps track of depreciations, and creates corresponding journal entries.
|
||||||
'data/menuitems.xml',
|
'data/menuitems.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
'demo/odex30_account_asset_demo.xml',
|
'demo/account_asset_demo.xml',
|
||||||
],
|
],
|
||||||
'auto_install': True,
|
'auto_install': True,
|
||||||
'assets': {
|
'assets': {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue