From 7c8cdfcd0c6e8a569875f5f02672d10673ca7628 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Thu, 15 Jan 2026 00:32:24 +0300 Subject: [PATCH] make new modul --- .../account_journal.cpython-311.pyc | Bin 4759 -> 4787 bytes .../account_loans/__init__.py | 64 ++ .../account_loans/__manifest__.py | 34 + .../account_loans/demo/account_loans_demo.xml | 61 ++ .../demo/files/loan_amortization_demo.csv | 49 + .../demo/files/loan_amortization_demo.xlsx | Bin 0 -> 6680 bytes .../account_loans/i18n/account_loans.pot | 961 +++++++++++++++++ .../account_loans/i18n/ar.po | 977 ++++++++++++++++++ .../account_loans/lib/pyloan.py | 583 +++++++++++ .../account_loans/models/__init__.py | 6 + .../account_loans/models/account_asset.py | 23 + .../models/account_asset_group.py | 33 + .../account_loans/models/account_journal.py | 7 + .../account_loans/models/account_loan.py | 426 ++++++++ .../account_loans/models/account_loan_line.py | 80 ++ .../account_loans/models/account_move.py | 36 + .../security/account_loans_security.xml | 14 + .../security/ir.model.access.csv | 7 + .../src/components/loans/file_upload.js | 50 + .../src/components/loans/file_upload.xml | 30 + .../src/components/loans/import_action.js | 45 + .../static/src/img/amortization.svg | 76 ++ .../static/src/scss/account_loan.scss | 9 + .../account_loans/tests/__init__.py | 1 + .../tests/test_loan_management.py | 443 ++++++++ .../views/account_asset_group_views.xml | 20 + .../views/account_asset_views.xml | 20 + .../views/account_loan_views.xml | 332 ++++++ .../views/account_move_views.xml | 22 + .../account_loans/wizard/__init__.py | 2 + .../wizard/account_loan_close_wizard.py | 24 + .../wizard/account_loan_close_wizard.xml | 32 + .../wizard/account_loan_compute_wizard.py | 154 +++ .../wizard/account_loan_compute_wizard.xml | 54 + .../tests/test_bank_rec_widget.py | 4 +- .../odex30_account_asset/__manifest__.py | 2 +- 36 files changed, 4678 insertions(+), 3 deletions(-) create mode 100644 dev_odex30_accounting/account_loans/__init__.py create mode 100644 dev_odex30_accounting/account_loans/__manifest__.py create mode 100644 dev_odex30_accounting/account_loans/demo/account_loans_demo.xml create mode 100644 dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.csv create mode 100644 dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.xlsx create mode 100644 dev_odex30_accounting/account_loans/i18n/account_loans.pot create mode 100644 dev_odex30_accounting/account_loans/i18n/ar.po create mode 100644 dev_odex30_accounting/account_loans/lib/pyloan.py create mode 100644 dev_odex30_accounting/account_loans/models/__init__.py create mode 100644 dev_odex30_accounting/account_loans/models/account_asset.py create mode 100644 dev_odex30_accounting/account_loans/models/account_asset_group.py create mode 100644 dev_odex30_accounting/account_loans/models/account_journal.py create mode 100644 dev_odex30_accounting/account_loans/models/account_loan.py create mode 100644 dev_odex30_accounting/account_loans/models/account_loan_line.py create mode 100644 dev_odex30_accounting/account_loans/models/account_move.py create mode 100644 dev_odex30_accounting/account_loans/security/account_loans_security.xml create mode 100644 dev_odex30_accounting/account_loans/security/ir.model.access.csv create mode 100644 dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.js create mode 100644 dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.xml create mode 100644 dev_odex30_accounting/account_loans/static/src/components/loans/import_action.js create mode 100644 dev_odex30_accounting/account_loans/static/src/img/amortization.svg create mode 100644 dev_odex30_accounting/account_loans/static/src/scss/account_loan.scss create mode 100644 dev_odex30_accounting/account_loans/tests/__init__.py create mode 100644 dev_odex30_accounting/account_loans/tests/test_loan_management.py create mode 100644 dev_odex30_accounting/account_loans/views/account_asset_group_views.xml create mode 100644 dev_odex30_accounting/account_loans/views/account_asset_views.xml create mode 100644 dev_odex30_accounting/account_loans/views/account_loan_views.xml create mode 100644 dev_odex30_accounting/account_loans/views/account_move_views.xml create mode 100644 dev_odex30_accounting/account_loans/wizard/__init__.py create mode 100644 dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.py create mode 100644 dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.xml create mode 100644 dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.py create mode 100644 dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.xml diff --git a/dev_odex30_accounting/account_chart_of_accounts/models/__pycache__/account_journal.cpython-311.pyc b/dev_odex30_accounting/account_chart_of_accounts/models/__pycache__/account_journal.cpython-311.pyc index b472f764d21739f63a1245e26d318eb2ad194a6b..d3b0fe0da4e8c5c22d0a34b6a499d5bceddd0c49 100644 GIT binary patch delta 765 zcmaJ;&ui2`6wYL3vYTIjKf58@iK`%NZfK_d6ii`4zocTQwi^-9}dp2P;SG4tGsFI>qg7!Y=?&5Ky zPa?lH-zQJzT2bFv34^xpwtdfW1J8%Lx?42(3#Gs9Acl(8QiFMC&UJ&%ayx=I+C3!q zerPfgQ3HOOm+Ljodnbf-9>Ob$OMWGFlpmt&+^uKplTli44kSU6Hy#Yc8lpGRK!A=_ wMRnM;nq$p`n@Jtyryd7hr{zzFw*U>@xZOY~A^bnQn0Yz7e~5oQp)7sk7le1CAOHXW delta 779 zcma)3O=}ZT6rJ}m-S@iqf8_O*|lQ{v&y%1!h%Qg zDa_wc59>xAX`u!m;tWZpoc%**?~vzW>faqqD62Vmt=#!<^{7L=pR>f&!N1{_{DRS)T05+REh(3ZauDMR?^* urq4%~euphBf1~N$s#gmB6b$dXaB+f=DY7B`eOfKuzxJ)S|KlIB_=ulT^r1ok diff --git a/dev_odex30_accounting/account_loans/__init__.py b/dev_odex30_accounting/account_loans/__init__.py new file mode 100644 index 0000000..18847e5 --- /dev/null +++ b/dev_odex30_accounting/account_loans/__init__.py @@ -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() diff --git a/dev_odex30_accounting/account_loans/__manifest__.py b/dev_odex30_accounting/account_loans/__manifest__.py new file mode 100644 index 0000000..4f35984 --- /dev/null +++ b/dev_odex30_accounting/account_loans/__manifest__.py @@ -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/**/*', + ], + } +} diff --git a/dev_odex30_accounting/account_loans/demo/account_loans_demo.xml b/dev_odex30_accounting/account_loans/demo/account_loans_demo.xml new file mode 100644 index 0000000..7058ba4 --- /dev/null +++ b/dev_odex30_accounting/account_loans/demo/account_loans_demo.xml @@ -0,0 +1,61 @@ + + + + + loan_amortization_demo.csv + + + + + loan_amortization_demo.xlsx + + + + + Journal Loan Demo + general + LOAN + + + + + Loan Demo 1 + + + + + + + + + + Loan Demo 2 + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.csv b/dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.csv new file mode 100644 index 0000000..a915a78 --- /dev/null +++ b/dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.csv @@ -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 diff --git a/dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.xlsx b/dev_odex30_accounting/account_loans/demo/files/loan_amortization_demo.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7cd317f1a230e78a69b5e897275dc9014c84466d GIT binary patch literal 6680 zcmaJ_1z6Ne*C(Vq1yoW}T3AA)Q92hSTzY9(x*JwPxQ8~-`y%*@I8X{un}phUyP#YN++rZqsjMg*wuCaz!yH$L90u_CEM z{R2PYvmL)^u0vs@G;Ur|v#Me-1e2czx0GTnaQ(P zS+&5wShR(l2>cA+XAT_DI55AHLQG4@uc$_r&a}GSOFh;Q$8?@Du%3}Y(bTx=>TJG` zjc6ec=eKtEQ)3BGRNa0_b?3f`@^i!7vw>9#O%)tmXqHgcHtG*2i{bB;X5cDK?npTs4gAxVq-GLs%r+ecIO{z%Ke zoFfCnIA`QYl0*sSTQYqN=U&*a-|5iL(;e(IdoQ8c$QwQb-<-7&&kz`9eu9;5C^N`w zI*s^-+$}^%j`{A41b{8{m;eOx4L(@H4RXlfe8mzqM51cVo})$Z&3cUYh9_F2s^?h* zaPe^HF#{nejX5HAh(wrUQ<_jQu4_&&zuKXW8ya3*$rLYwur{XH3LkiJT=rzfBsTqr ze~^uKf1E)d49l)|w3u(&t2a@6Nn5aL0e4Y?=8!W;+NMyIOKxcL$K3_XpdOpc8`XxfUeyQ}`LJ=R*L3Is3Wtd{4z zaLk{W4O%TqnA7iW=G_4fvY~_SL94$;i&CoR+z$CxM=kuNeZ6<9{L=TExaQdI-JSp$ zaE@Za*<&*Wwx~)2T>OkvW>@Ej*gvzzzGH>voyjv$PLLSMV)tt_F-8jKftIVX8)t4M z^hPc6<91S04;_uQDUJR+2-Rv=SL{5XuiC5KQl zraM{2H@irv&N6v$>}4TmMAIww^{+-k`(YB&jjbPzhjZE{rH^+0{J5bDU4tXH@XKj?dSn0tFRdSk&K!QtJdP1YeNDt^H9UV<8L z(Et~c_Pjs`H|3Fp4AA*@C;2^Z`T*P%Ue2~5fywul3tZuilkmsGIxcav+CBO2KT9Y< zEfur#1|@VAW5~fuF}@b_FZ~F~hzo+Nrb2D8%&V8g!)!<~ADC}$b0~3tC{uYP!*x`&ODWEXKYrg^e%x}m3?tG^q%EOqLY0iA z)_q|L!zHdzFi_M`w+lAIk{86kUs??yQ}&~<4B_BNnAG2e(gruRa}!vIiVVPEB}*5i zIZD8|?i%7eJ5{6~6Ge?gc0bFMG$RuS#qE)7=5Wq@{14P8+_~PEkI>;IfGE~J*I7F) zZ$Q*!Xr}syY8Ys>-lD%JW7NKR5;BIEJ!M-LM}o3j=rVXL>pp?fOXy$azgm%-!OlmDbw(-gjmESPPKj1b!UG_>It~in|Wlvv`BwD$Km<*{Y$RPi0^IFbuj$ z*U0^7ELk2t8e9<-&aP{=r@E5#Jp!hma8O-cVgp9wTEclD#O#(tW5+&fRBWVveCy?- z(-YvvXFw@|SB_m;$0^x;`mPpctDRQiNZKw!b8LaIjDRjx_K1Dhi#TFs#Qv-C7!oDK zbP)BjNBqt<|GZd_#=*nxsxi3nVG^`C1SKP#T%at+=^w9Wl7`I zW-7^?Y7fjFzlnoXY&;0Q{etNTzw0@6M+DK*>RP+!#AaU|XnMTCZ7At@aAKo~W20yG z81i!dB4fxJ^zxH}v2a~;PLK!kY^^!yvxLZ2y3@u}sWAL++n#M(r{`txd?N^wcSpa* zOZdgK&=&8`;+ue{?x&M0@S_4y9jTHzb#qJkS`VFZ-Mr4@n&}k#!y*F)&&uui)Y{5^ zKbyb?o2_Xu|HZ~s_c0Qof866)&sjEC*ABqy0w+Jo33EF=8$mW-G@N_4 zU4Hft58YdbBN0pc%gx#O;k>syCGC-`o)=l|hP0B3kKj8nziqal;U>quGio~T*Sp(K z=kbd7q>%vCc9=^br5*{uqI>GYAcs9A;6?Dgay73hcL&L`|FQ+k+(#^dHzbB0z zJJIVqlDALrd0j6(4nG3Z@$nn_NOYxy(0M=AuI;kh{Mc%tKdA}j%073FN-ZnxSo5!^ zue}U8iOm{r6Gm(Y<~;eRBB8|Av(xkP6ebtcHqZ`;s5Vl!j%Z%Yvuz_(kZbKDg^j>J zwx9aHb5pO#7;ptRqZ^_J15d99{-#dvT-1iuj)TU@=FdC1U`J~6zL#h zOcnT9W_=9gTH_s>R;dxqs`ouTno2M7xjA|p@2b3|o4G><%`LrnBAwBx!TTI+2&q!< zG}oeV>*SBDcq%$GWigw&D25L7b~POPBow97iuBLYvn9c(UnqMy#mCgwQtsNRmV zY&08^mi9JFpg^KbKON7C&`~(nK^Rs}w{D<@S4L~y8P7Et!i6U<{MlS&Z1KZiVT+3V|;T&TP+z_bqlUZp?A9v-Y6j1D@Yky^?q^O2x zHRD4Iy1YscM(UEKCXs2RkV|X`_hdECE+a1loIIFwwD4bxH=sU z3t|Hn%M|QKO03GKo&q!-%|fdEZARsozQk|@D_AhkgZhYlANpVsM#-+I>|z_xOq2MmmUOuUa+83k7o)01 z0-)*=T0|+*%iDeT4Z)tEFQM5L%RWss5%2{g7Z%~>Ec~@lAt`NETJ?}@8D>fJlsqeL zzIG@30_^>(yX3@s6;fJcvIKA6%d?tOq%uY-<86_ELuBwaW&H|KN{9vr0Hz|oY|)Hd ztlB{w1cb__PxA6qzs3*|uS`j>a^xmmST0bdL~FsM?v3T?W3$I!AjNS#+kd6 zM$@lv1N7$TVdIh^OYJhyjeaefn~Il%H~ecFOw&*oHCYs!P}atqlwaNI6FORk8f88TGL4%VQvam~Mj<7KS_Ck*l_qby&b zAV5PYx>!j0t#Wj6IQXK#M*+7ki!e7L+y?acSN@l~aBhQMpm;CXb>}x?bIhD-nC}nMVux694 zwmd79oCWO$rRo5B4Qegg5@3I>rzow39rky_H8U5Oqhi#6Nz7c&lPXZ7t$|fQv;J$! zWIvCC7&itflciJ7xn*^yDm3VEaCBD9^0GvE&1S^rHE_uVVNK5a&v*fX$ zNG*)yQdrkR{hP!s6VJpQF2jfs&qS47LJ0?!>~C>Jmn;<3US#t!g&RG^v9kL0fK#~v z^}w`Wx0jb%88MoGoKl5x6e*wMd6jS!X|M0I9-tI2E)HTQK{LR2WJv?rcf zcM?hg5QbYiil^Mj6?BuL2M87|;?kxmM_jadvYV^}lD6)xoIKf1Zq1X618Pe8RFz&23p<}6L(*ec~LeM`Pm&KPd%ZLQ$&dXy5Vw4Yd3 z^s3*kW*KfAAeq3RWlfYbwG%vR6K1X3buzlElj+ z4BWRxMPy#2a$aFsUL+UvxJN|gt3>hhzHWGc(d2o=`MYfIdXM8bL`}`_yj!;2f2wSQ zg69019A|cobPj#$Ts?MX%6iORdYDxHh=RM!Z&&%8>UToRT%;U29qfMECU5?H1375; zAnC}nxgr(WDK{q9pkrOX%=XzHE4yf&z32Hr?&62Q`r*UMR=)N|mmVdNX{ra?tdbC( zhj)w*l7_71s2FB`h%4+p4Vr5@!F&hpZD7r~b7(z3bf`Al;4>}fR_1@c;sH9V<~bcW z_+l`!nw<0A{)ujxm59UsGy1CR8KO^)&4E*)rx$PhXezNkNxE-Wm~2KYA=++FZYA3j zRuh6P{9wl^D_?Zmr|pz!ek;f@ z_VVKUTHbol$)9^rY7fCLDf17i;9dy;v5yXbI#^9l$kxTzuM@RRoqx*#lT)*-DLI@Jo?EgXt z&Z!<9!j*6=VnkbGWU9`O(7UykK6U|;ChIN(d2bovgEv1WfBdx9rug%lQ0%wJE;&N% z()=6H4!9?7YsmTb)*`JQB7bhdhBf{T19@2s#Z>T3B|gKNJy8Q*^Uj0^pu#?7^^#A0 zO<`MFd;_~mV=vGVR5}$#n6*y#G@IQzFW*>vA|f^^@O#PMON57TrBA#+q8)F(aF0(A zXW#L7<>87H^PMS{{;IH-+5j)S2@@;&9bWh$!L5( z_8mp-oAqB&08}GGNI&mZzO&Ce z+a`F1!aHzqy*b)%yhH7a1$CkQXZ(WcU#sF=_awb; zBr|8{D;wn`s6jff0KHQx-L5({!}?H7x?0;AJlhBEHVs@VPkYDxIh3@H4+&nOuN_;q z*VE7Pfi!%3i5J4&7Ri8RG_fD)mkd_QyxcNslQ)R`tSG6ENhB^SZGRvw)#9;G5SqXm zibpil^Hq-qry2(#mpH@b;kzLmU$Xze;nWz-)29Y`rsH-?wM-= zYhneL#{+maDgy_%v77ckkVHnVSsn;!(Xjnb2oc_ddh+o z*@ym|o6%mZn9>e5HMBsKE@r{Yq5#p(b6Ra)`xAwBU5qs(+x~{~Wbf`!bS)IJtwJ+)aQ!&R{ptU)ORV zLgND?W&2K*&AY75)ef35CmgA`rY;`d!T*XRTL`1?x^+F1X?J>E4_>QbU z&FodoIgCchdsn>u6*BbbQ#1m;NRMq2<2RKCxVGcTEES z5E6Y6W@GYRkN_!XXZhPS`;YlXomREVsojK{z85y?L+cpQfAv`x&3N*iL3N?*oe{4< zqp(|stxV@~yVwhgHLBQv>Sd{=f;)#7w=Zn>EAy$3Y8n;;NF+l7wj~+Rr5a}5eZQ|S zitO^*j#O7;#Mn@{{OT1QarlCE9Nc{yOY@g~9(fT|?ymWV{YOUZK*zi5;ct4~+Ux=IfBo6+Zc!3{hC- zRcZSht^AYqdhc+B`TQms?7#K?KLqGcDc9@lRiE*jC{Wo8wSa%jZoi40{D0KCKiRJ*maDw`n+8!)_jjrMPZs`@`+E3Z1?g|XqPoWY nI~DyY;JR!5w*Vwc!2gJOO%*JZnb6SiQLkWB6$!n0_3Qrt-=l!r literal 0 HcmV?d00001 diff --git a/dev_odex30_accounting/account_loans/i18n/account_loans.pot b/dev_odex30_accounting/account_loans/i18n/account_loans.pot new file mode 100644 index 0000000..cc44119 --- /dev/null +++ b/dev_odex30_accounting/account_loans/i18n/account_loans.pot @@ -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 "" +"months\n" +" month" +msgstr "" + +#. module: account_loans +#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard +msgid "" +"years\n" +" year" +msgstr "" + +#. module: account_loans +#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard +msgid "%" +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 "" diff --git a/dev_odex30_accounting/account_loans/i18n/ar.po b/dev_odex30_accounting/account_loans/i18n/ar.po new file mode 100644 index 0000000..f218329 --- /dev/null +++ b/dev_odex30_accounting/account_loans/i18n/ar.po @@ -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 , 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 , 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 "" +"months\n" +" month" +msgstr "" +"شهور\n" +" شهر" + +#. module: account_loans +#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard +msgid "" +"years\n" +" year" +msgstr "" +"سنوات\n" +" سنة " + +#. module: account_loans +#: model_terms:ir.ui.view,arch_db:account_loans.view_account_loan_compute_wizard +msgid "%" +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 "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 "سيتم حذفه وسيتم وضع علامة على القرض على أنه مغلق. " diff --git a/dev_odex30_accounting/account_loans/lib/pyloan.py b/dev_odex30_accounting/account_loans/lib/pyloan.py new file mode 100644 index 0000000..47604aa --- /dev/null +++ b/dev_odex30_accounting/account_loans/lib/pyloan.py @@ -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 diff --git a/dev_odex30_accounting/account_loans/models/__init__.py b/dev_odex30_accounting/account_loans/models/__init__.py new file mode 100644 index 0000000..3f49ffc --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/__init__.py @@ -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 diff --git a/dev_odex30_accounting/account_loans/models/account_asset.py b/dev_odex30_accounting/account_loans/models/account_asset.py new file mode 100644 index 0000000..d1e510f --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_asset.py @@ -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)], + } diff --git a/dev_odex30_accounting/account_loans/models/account_asset_group.py b/dev_odex30_accounting/account_loans/models/account_asset_group.py new file mode 100644 index 0000000..f412d9c --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_asset_group.py @@ -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)], + } diff --git a/dev_odex30_accounting/account_loans/models/account_journal.py b/dev_odex30_accounting/account_loans/models/account_journal.py new file mode 100644 index 0000000..4486dc5 --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_journal.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + loan_properties_definition = fields.PropertiesDefinition('Model Properties') diff --git a/dev_odex30_accounting/account_loans/models/account_loan.py b/dev_odex30_accounting/account_loans/models/account_loan.py new file mode 100644 index 0000000..1451d28 --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_loan.py @@ -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() diff --git a/dev_odex30_accounting/account_loans/models/account_loan_line.py b/dev_odex30_accounting/account_loans/models/account_loan_line.py new file mode 100644 index 0000000..af62b2e --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_loan_line.py @@ -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) diff --git a/dev_odex30_accounting/account_loans/models/account_move.py b/dev_odex30_accounting/account_loans/models/account_move.py new file mode 100644 index 0000000..dc562c5 --- /dev/null +++ b/dev_odex30_accounting/account_loans/models/account_move.py @@ -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 diff --git a/dev_odex30_accounting/account_loans/security/account_loans_security.xml b/dev_odex30_accounting/account_loans/security/account_loans_security.xml new file mode 100644 index 0000000..aa353c2 --- /dev/null +++ b/dev_odex30_accounting/account_loans/security/account_loans_security.xml @@ -0,0 +1,14 @@ + + + + Account Loan multi-company + + [('company_id', 'parent_of', company_ids)] + + + + Account Loan Line multi-company + + [('company_id', 'parent_of', company_ids)] + + diff --git a/dev_odex30_accounting/account_loans/security/ir.model.access.csv b/dev_odex30_accounting/account_loans/security/ir.model.access.csv new file mode 100644 index 0000000..28c2786 --- /dev/null +++ b/dev_odex30_accounting/account_loans/security/ir.model.access.csv @@ -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 diff --git a/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.js b/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.js new file mode 100644 index 0000000..3789c62 --- /dev/null +++ b/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.js @@ -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); diff --git a/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.xml b/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.xml new file mode 100644 index 0000000..249d047 --- /dev/null +++ b/dev_odex30_accounting/account_loans/static/src/components/loans/file_upload.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/static/src/components/loans/import_action.js b/dev_odex30_accounting/account_loans/static/src/components/loans/import_action.js new file mode 100644 index 0000000..35eca1f --- /dev/null +++ b/dev_odex30_accounting/account_loans/static/src/components/loans/import_action.js @@ -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); diff --git a/dev_odex30_accounting/account_loans/static/src/img/amortization.svg b/dev_odex30_accounting/account_loans/static/src/img/amortization.svg new file mode 100644 index 0000000..27d8a4b --- /dev/null +++ b/dev_odex30_accounting/account_loans/static/src/img/amortization.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/static/src/scss/account_loan.scss b/dev_odex30_accounting/account_loans/static/src/scss/account_loan.scss new file mode 100644 index 0000000..72b82b2 --- /dev/null +++ b/dev_odex30_accounting/account_loans/static/src/scss/account_loan.scss @@ -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; + } +} diff --git a/dev_odex30_accounting/account_loans/tests/__init__.py b/dev_odex30_accounting/account_loans/tests/__init__.py new file mode 100644 index 0000000..f7bc805 --- /dev/null +++ b/dev_odex30_accounting/account_loans/tests/__init__.py @@ -0,0 +1 @@ +from . import test_loan_management diff --git a/dev_odex30_accounting/account_loans/tests/test_loan_management.py b/dev_odex30_accounting/account_loans/tests/test_loan_management.py new file mode 100644 index 0000000..464a7e7 --- /dev/null +++ b/dev_odex30_accounting/account_loans/tests/test_loan_management.py @@ -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') diff --git a/dev_odex30_accounting/account_loans/views/account_asset_group_views.xml b/dev_odex30_accounting/account_loans/views/account_asset_group_views.xml new file mode 100644 index 0000000..6b997f6 --- /dev/null +++ b/dev_odex30_accounting/account_loans/views/account_asset_group_views.xml @@ -0,0 +1,20 @@ + + + + account.asset.group.form + account.asset.group + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/views/account_asset_views.xml b/dev_odex30_accounting/account_loans/views/account_asset_views.xml new file mode 100644 index 0000000..f9f9686 --- /dev/null +++ b/dev_odex30_accounting/account_loans/views/account_asset_views.xml @@ -0,0 +1,20 @@ + + + + account.asset.form + account.asset + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/views/account_loan_views.xml b/dev_odex30_accounting/account_loans/views/account_loan_views.xml new file mode 100644 index 0000000..1ef94fa --- /dev/null +++ b/dev_odex30_accounting/account_loans/views/account_loan_views.xml @@ -0,0 +1,332 @@ + + + + + + + + account.loan.form + account.loan + +
+
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + account.loan.list + account.loan + + + + + + + + + + + + + + + + + + account.loan.search + account.loan + + + + + + + + + + + + + + + + + + + + account.loan.search + account.loan + + + + + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ + + account.loan.pivot + account.loan + + + + + + + + + + + account.loan.graph + account.loan + + + + + + + + + + + + + + account.loan.line.list + account.loan.line + + + + + + + + + + + + + + + + + account.loan.line.search + account.loan.line + + + + + + + + + + + + + + + + + + account.loan.line.pivot + account.loan.line + + + + + + + + + + + + + + + account.move.list + account.move + + primary + + + date,name,ref + + + + + + + + Loans + loans + account.loan + + {'search_default_current':1} + + +

+

+ Manage Your Acquired Loans with Automated Adjustments. +

+

+ Set up your amortization schedule, or import it, and let Odoo handle the monthly interest and principal adjustments automatically. +

+
+
+ + + Loans Analysis + loans-analysis + account.loan.line + + {'search_default_current':1} + + + + + + + + + +
+
diff --git a/dev_odex30_accounting/account_loans/views/account_move_views.xml b/dev_odex30_accounting/account_loans/views/account_move_views.xml new file mode 100644 index 0000000..f7f7e16 --- /dev/null +++ b/dev_odex30_accounting/account_loans/views/account_move_views.xml @@ -0,0 +1,22 @@ + + + + account.move.form.inherit + account.move + + + + + + + + + diff --git a/dev_odex30_accounting/account_loans/wizard/__init__.py b/dev_odex30_accounting/account_loans/wizard/__init__.py new file mode 100644 index 0000000..2f733f8 --- /dev/null +++ b/dev_odex30_accounting/account_loans/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import account_loan_compute_wizard +from . import account_loan_close_wizard diff --git a/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.py b/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.py new file mode 100644 index 0000000..3b74550 --- /dev/null +++ b/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.py @@ -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'} diff --git a/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.xml b/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.xml new file mode 100644 index 0000000..af8a4a1 --- /dev/null +++ b/dev_odex30_accounting/account_loans/wizard/account_loan_close_wizard.xml @@ -0,0 +1,32 @@ + + + + + + account.loan.close.wizard.form + account.loan.close.wizard + +
+ + All draft entries after the + + will be deleted and the loan will be marked as closed. + +
+
+
+
+
+ + + Close Loan Wizard + account.loan.close.wizard + form + + new + + +
+
diff --git a/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.py b/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.py new file mode 100644 index 0000000..829727e --- /dev/null +++ b/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.py @@ -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, + } diff --git a/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.xml b/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.xml new file mode 100644 index 0000000..3791ae5 --- /dev/null +++ b/dev_odex30_accounting/account_loans/wizard/account_loan_compute_wizard.xml @@ -0,0 +1,54 @@ + + + + + + account.loan.compute.wizard.form + account.loan.compute.wizard + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ + + Loan Compute Wizard + account.loan.compute.wizard + form + + new + + +
+
diff --git a/dev_odex30_accounting/odex30_account_accountant/tests/test_bank_rec_widget.py b/dev_odex30_accounting/odex30_account_accountant/tests/test_bank_rec_widget.py index 5bec5d6..a40bad4 100644 --- a/dev_odex30_accounting/odex30_account_accountant/tests/test_bank_rec_widget.py +++ b/dev_odex30_accounting/odex30_account_accountant/tests/test_bank_rec_widget.py @@ -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') diff --git a/dev_odex30_accounting/odex30_account_asset/__manifest__.py b/dev_odex30_accounting/odex30_account_asset/__manifest__.py index f53d21c..a783c85 100644 --- a/dev_odex30_accounting/odex30_account_asset/__manifest__.py +++ b/dev_odex30_accounting/odex30_account_asset/__manifest__.py @@ -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': {