make new modul

This commit is contained in:
mohammed-alkhazrji 2026-01-15 00:32:24 +03:00
parent 56ccd84d63
commit 7c8cdfcd0c
36 changed files with 4678 additions and 3 deletions

View File

@ -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()

View File

@ -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/**/*',
],
}
}

View File

@ -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>

View File

@ -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
1 Principal Interest
2 304.60 250.00
3 308.42 246.17
4 312.30 242.30
5 316.22 238.38
6 320.19 234.40
7 324.22 230.38
8 328.29 226.31
9 332.41 222.18
10 336.59 218.01
11 340.82 213.78
12 345.10 209.50
13 349.44 205.16
14 353.83 200.77
15 358.27 196.33
16 362.77 191.83
17 367.33 187.27
18 371.94 182.65
19 376.62 177.98
20 381.35 173.25
21 386.14 168.46
22 390.99 163.61
23 395.90 158.70
24 400.87 153.72
25 405.91 148.69
26 411.01 143.59
27 416.17 138.42
28 421.40 133.20
29 426.70 127.90
30 432.06 122.54
31 437.48 117.11
32 442.98 111.62
33 448.54 106.05
34 454.18 100.42
35 459.89 94.71
36 465.66 88.94
37 471.51 83.09
38 477.44 77.16
39 483.43 71.16
40 489.51 65.09
41 495.66 58.94
42 501.88 52.71
43 508.19 46.41
44 514.57 40.03
45 521.04 33.56
46 527.58 27.02
47 534.21 20.39
48 540.92 13.68
49 547.72 6.88

View File

@ -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 ""

View File

@ -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 "سيتم حذفه وسيتم وضع علامة على القرض على أنه مغلق. "

View File

@ -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

View File

@ -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

View File

@ -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)],
}

View File

@ -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)],
}

View File

@ -0,0 +1,7 @@
from odoo import models, fields
class AccountJournal(models.Model):
_inherit = "account.journal"
loan_properties_definition = fields.PropertiesDefinition('Model Properties')

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_loan_readonly access.account.loan model_account_loan account.group_account_readonly 1 0 0 0
3 access_account_loan_write access.account.loan model_account_loan account.group_account_manager 1 1 1 1
4 access_account_loan_line_readonly access.account.loan.line model_account_loan_line account.group_account_readonly 1 0 0 0
5 access_account_loan_line_write access.account.loan.line model_account_loan_line account.group_account_manager 1 1 1 1
6 access_account_loan_close_wizard access.account.loan.close.wizard model_account_loan_close_wizard account.group_account_manager 1 1 1 1
7 access_account_loan_compute_wizard access.account.loan.compute.wizard model_account_loan_compute_wizard account.group_account_manager 1 1 1 1

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
from . import test_loan_management

View File

@ -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')

View File

@ -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>

View File

@ -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>

View File

@ -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 &amp; 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 &amp; 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>

View File

@ -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>

View File

@ -0,0 +1,2 @@
from . import account_loan_compute_wizard
from . import account_loan_close_wizard

View File

@ -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'}

View File

@ -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>

View File

@ -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,
}

View File

@ -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>

View File

@ -2022,7 +2022,7 @@ class TestBankRecWidget(TestBankRecWidgetCommon):
def test_auto_reconcile_cron(self):
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()
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):
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()
st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01')

View File

@ -26,7 +26,7 @@ Keeps track of depreciations, and creates corresponding journal entries.
'data/menuitems.xml',
],
'demo': [
'demo/odex30_account_asset_demo.xml',
'demo/account_asset_demo.xml',
],
'auto_install': True,
'assets': {