From 9a39ec5ce9061014d72451a4698432e6b9bd9243 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Mon, 30 Dec 2024 15:16:01 +0530 Subject: [PATCH 01/38] fix: add item qty --- .../gst_sales_register_beta/gst_sales_register_beta.py | 6 ++++++ india_compliance/gst_india/utils/gstr_1/gstr_1_data.py | 1 + 2 files changed, 7 insertions(+) diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py index c424dca2b8..80744ba694 100644 --- a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py +++ b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py @@ -222,6 +222,12 @@ def get_columns(filters): columns.extend( [ + { + "label": _("Item Qty"), + "fieldname": "qty", + "fieldtype": "Data", + "width": 100, + }, { "label": _("HSN Code"), "fieldname": "gst_hsn_code", diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py index 315facf4d1..a068dc37a9 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py @@ -448,6 +448,7 @@ def get_invoices_for_hsn_wise_summary(self): frappe.qb.from_(query) .select( "*", + Sum(query.qty).as_("qty"), Sum(query.taxable_value).as_("taxable_value"), Sum(query.cgst_amount).as_("cgst_amount"), Sum(query.sgst_amount).as_("sgst_amount"), From c6201f41002c90be4f1deb7716599a7a54c84dfc Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Tue, 31 Dec 2024 10:42:00 +0530 Subject: [PATCH 02/38] fix: use db_set instead of direct assignment --- .../gst_india/doctype/gst_return_log/gst_return_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py index 52283eb89a..f203ef4797 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -178,7 +178,7 @@ def get_return_status(self): self.gstin, self.return_period, ) - self.filing_status = status + self.db_set("filing_status", status) return status From bf9eadf46df4342e95d5336f52ace83fe97e134d Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Thu, 2 Jan 2025 12:59:04 +0530 Subject: [PATCH 03/38] fix: handle irn generated in other portal error --- india_compliance/gst_india/api_classes/e_invoice.py | 2 ++ india_compliance/gst_india/utils/e_waybill.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/india_compliance/gst_india/api_classes/e_invoice.py b/india_compliance/gst_india/api_classes/e_invoice.py index c59b6c7543..e337623014 100644 --- a/india_compliance/gst_india/api_classes/e_invoice.py +++ b/india_compliance/gst_india/api_classes/e_invoice.py @@ -21,6 +21,8 @@ class EInvoiceAPI(BaseAPI): # Cancel IRN errors "9999": "Invoice is not active", "4002": "EwayBill is already generated for this IRN", + # IRN Generated in different Portal + "2148": "Requested IRN data is not available", # Invalid GSTIN error "3028": "GSTIN is invalid", "3029": "GSTIN is not active", diff --git a/india_compliance/gst_india/utils/e_waybill.py b/india_compliance/gst_india/utils/e_waybill.py index fa6e0e21e2..99aed4ce7f 100644 --- a/india_compliance/gst_india/utils/e_waybill.py +++ b/india_compliance/gst_india/utils/e_waybill.py @@ -170,6 +170,11 @@ def _generate_e_waybill(doc, throw=True, force=False): if result.error_code == "4002": result = api(doc).get_e_waybill_by_irn(doc.get("irn")) + if result.error_code == "2148": + with_irn = False + data = EWaybillData(doc).get_data(with_irn=with_irn) + result = EWaybillAPI(doc).generate_e_waybill(data) + except GSPServerError as e: handle_server_errors(settings, doc, "e-Waybill", e) return From cbac5593d56364319ba055ffd440983fb6003c6c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 2 Jan 2025 17:03:37 +0530 Subject: [PATCH 04/38] refactor: add application permission check and update app screen configuration --- india_compliance/__init__.py | 13 +++++++++++++ india_compliance/hooks.py | 12 ++++++++++++ .../public/images/india-compliance-logo.png | Bin 0 -> 7312 bytes 3 files changed, 25 insertions(+) create mode 100644 india_compliance/public/images/india-compliance-logo.png diff --git a/india_compliance/__init__.py b/india_compliance/__init__.py index 384aaaec78..3cc51b948c 100644 --- a/india_compliance/__init__.py +++ b/india_compliance/__init__.py @@ -1 +1,14 @@ +import frappe +from frappe.utils.user import is_website_user + __version__ = "16.0.0-dev" + + +def check_app_permission(): + if frappe.session.user == "Administrator": + return True + + if is_website_user(): + return False + + return True diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index c03d87911a..96a87b35cf 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -7,6 +7,18 @@ app_email = "hello@indiacompliance.app" app_license = "GNU General Public License (v3)" required_apps = ["frappe/erpnext"] +app_home = "/app/gst-india" + +add_to_apps_screen = [ + { + "name": app_name, + "logo": "/assets/india_compliance/images/india-compliance-logo.png", + "title": app_title, + "route": app_home, + "has_permission": "india_compliance.check_app_permission", + } +] + before_install = "india_compliance.patches.check_version_compatibility.execute" after_install = "india_compliance.install.after_install" diff --git a/india_compliance/public/images/india-compliance-logo.png b/india_compliance/public/images/india-compliance-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f1cad060e2992c0fd60893fd675185e85b76b217 GIT binary patch literal 7312 zcmdU!+RheMNCHAgOzN~F~x_-GgoTu1!pur z>9-5f2g}>e_ly;6ySK=@X^o?XZEU?TclMXqhM4@oFtQb|55`_-9SJD^I|oYDd|vRj z`>HCSD~vuGBr__L!xq68HGZsX^F+2oMJldO#8d7W)L8d&XtAeXOACFlx-{7-_XAT6pgxF4fB?YO;m37K5$y%`SdR;eJqFK{ZAk&mV`=&JL~JNCj7JU44>MS0o`)a*yLc+*|PO@+b1xoC-0u z%vHUhW2zGPGCVpu|8EqVyV5m@1OXN(ytuxNsQ0snmvW;1pj;dC_ks(|i5-#5AW#93 zY+4t0wSXdr13EULB(Q**@XL;+W80Axr3VS@1^&T1dZlR8rG@o6{7#f$TMU*;{oVM` z%(4Z#4mt#ev^8tPJJ45u)EL+#UeF{HW$p_U7&s~)#`>q{JqMdd1bE}09{`8d!<|!~yItvJ=W-;DG(}Iy(-ogy4>)?E9 zFQGC_{0vyJP`~KV(-}ljvG9AGd9Vww8N2^F>1{e2-7gowZMm~(F9F+pzn0=qaQSY< zPOZY$N_$YL{ZlLI0`efK?zXo^j%HPirwKut_si=KA*u7d{or8ga@_`qS8#uEs)g>; ztRg><85E8*U>tPi&o`$(JMff^0XQZq53a@nM? z;k$N}MB{05Db5e1nhoax(M}NGP4MlPZJQL*6l^JjLb{u*;?whYM3uik?l+(NZW&>S zH}>SZqwxbLK~9PIiU%WRmeXuPwu_xM~tJ=3|Snp$v*O~Yf8E%7Fc79JN{a?Id~ zd>ibiR5%L(v?l$=D9vRci!uYAWa(0${V=9H@o<>(H-&X!+Cfs2QtPrnmVB%3_! z^)y|Yt~1~j+{fDJ?V0kcYh}ZCbO2S9sE#IQO+s7{Dgtq;d58ED+f|DcC%8DUk=sqS zi4yiVJO1kR*ba8+#?LwS_B;LeFu0mPA2VG_>zZ>IUapIum2oKLBZ5pTFz-4WPp0Lq zVO2Q62xkBKQsNv4 zYmvU*i;eEe@ycQxIelYLkui85gn8N8?}W`$p`wA zuunc)ldAY%e5L9=>I<9C2}-YUuxND2A_P#eC!Q;Hp*nJlAi`8@5|52Rdvnbz#iuY? zYJ5%dKD@=Ca%07!D_^n+3Ug3rXVd&vMs`T<>eOBH#yf(KN+6g-k0Hkxp4i6-Zy3}u zz2j9YTEq6i#HM6>az8PHD(xdl1ZDFFy5IM~@*8uM)dwd_Yt-h_(l(v$hsC3q)pA>y z_dgbx9iB1UY58I3??ld?z$hY#tDhx+)4uiF?-s6omB`yvPm1tsUmy)lca)nYkdUWg zLHt=Aq(553+kj>N#vc{R;U95+e_{VaH+!=)FW(5xB$B#EZ; zaV4OgVo>mYlqpZ57;LM#MpI;p@8p!;spDVoD1J|-c9Zg07cx>8!N31SFg&yf2Qx$! z(O%0alzAW0K>+T+ec4+K8V_4?eQJ7OF^*|yuzg@Ci|>NtL^lm#ayv6^NPmpc|f*GJ}K_Yv@xDgG4#j>Hs7SG5i}By zW0^6(fL49sLI{2|7m*~;2jqi33@y~~FKIIoxFx?Cw9iPXp)a%VIk+&7C=+jKG4a^?61;N+_wgADX%4${HrK{l;+YHjt^*DV=Z5}s) z(31%8iUhhaHif5O-xDV=_#W6VhaT0-iF$&A@&!(p9-aTQ7tzy<_0dtnLm2$wu{pR5 zyul0cqDJXSk04|h=CjX?PHs(AF7KN3Fvt@-Za!WRy!M*Gim-Cd;`E5q6NN&eqrHI| z>CQH8zi@%>DZS%oPHWVR=VWo%U~G^)4(t23g=}oI5`HV!b8Zm&YW(0FiBS*$==y_V8``P1tTkr4An?|WIT9rtG2JVNMIfs;`2|*hfwUx zJSR4GW*Pr5OK48`4nNbEZ*=e0 zi=xu%K1=J$_e`%OR$P63$kt?R25#bH=DPWUoX$p6p`hRBoD)jNtyblh=MT3GSA+fO z-NTTp%j@=8!KzsZ?Y)%?pQ}PoH$b`X%56w0%I3%M?UJ{&DDvb|JH0Vp$O@GRzca8I zOy2)jZwKyLbNDIpcEg$?dR~MYRc?5g!%-*2h_~+We)@6r9(T5+&8rFtS2Y)379=>R z|C(THWA`BRc3EBzL}AU-HfO6Nr|o$aE}vILwDrDuaE>Zdzr4A>Q8-#ghPwOfw@3{n-6cfhw7Y|!o z%4~Pk_JS}2zZ~$op<-=-&W|qTx9+RmHP!cF!&P5-|5?6-eGaPTL;rE$TybcS&Qf2o z+u+Od)8rg4y$3}AHllqbDd}jgFOZTH@1{{*7wAsoJ|hOcb)T#{RAWogow|6;Zj!t1 zr-)QqS=`2(*7~poQwAPboVa;z6W>u|zwk%ZN;elMAX)KB$!Zny zjWm-(;sQDg8u~rC|GP&qcud}#4u2m}ihVaVd-?SEA&lZ*>iOXFf96|cN?1vfW37f} z=4n>C84_%5FPRl8zr#1xT6uiYsY_`>`KkWtqBHN_fFg2os>Kj6fvdO&i=ocj)GAF; zA4wD?dIRkM1*A4DUR^t4QGKee;v2hAcrtCF#WKRJC;x23245%&*sMJl8Jq?&c?aeU zF^Y(_;jd*oQ5E0WRkGPm21f8a>&4#WHBdHl^NPJ=#U#Jb@vwuBNe1JyqP{p`(%AMQ z?@4xh?W)9AcxR>g{_bz`-Zd|oimGE>T>W%SdE2FXK9uqh5L2Ije=OZZky2D($*)l= zb@K}P8i&~nrP-NLWQWbC>pr;ep4I?5cI7lu2T9v$v|e}+J0Y=vOB|g$@XJ-#x@qJ7 zn9a9$Kqobh)9x!q0w#QvIyTtU_^OJe#SjZ6x%b%qd!DHsY2$0-7e^jje?vyDQb?qQ z2X=9>P8b%>cMI%AkH|h^zrb7`DyW_T8ZeDwdTO2r@Y7E57MtHa1rXfqNx1f`tn)SI zUcd!_lYI;OSOrmSPJ!Fb_Ki%Dan=IqAe;^Hei#IW2>tTni2QnF_0z07eN2JVvU#B1 z_1Nx}qWh|=+@0WuK4BI-3Kdo5PCJd^5mIp& z!Tkw~7|RH=E)dV*R6eqP+AFtCJGxo5|@QzD<|V zg_5Y>U1SfL#O9J#RGmgPt}p>uXBkS@4PTiWODdxi_Fui5rM~xGiqhE523p*8BlCwB zdo3f$KK|&#qYHG`tDvW%vd2oB@X_Qi`D^#FFWWwLxIJU19r(JuL0@}8^E`xF6!qNV z)|nW-Db}XMiA5lO&{t9eSq-6hfi5Ce!x#S)23iX8vI}1w=FbmR}hv1hZd*?Da#L2nzG7mYMp;mC;Fx zI2T@4{a!;hPU0|FmF0GC4abA%dl%RHv4x$A=|xRPPpgtcEn_;?KqT^udHtbWmkoaNzHb+cu+dKlf|ZIx4QKG=~{zjVTb;7MA1T^27oF7vLi zA)JoPmD?c_AU6YJC{+eq%$rV~t2TX^b^Yhxuw!;QmiepH#QG7(eo8%aj|K0qLtm;< z*>E$xr^bj7(DePT{Ww~;NdXrXZYF`SwfzzK&+%!VYacp}eemGZkI1(Zf2a9L4=q3I z+rElCoyLfaOb?Qx^=WcQ;0jTygmoyoI&5_ubd%w|g3>T2(4f=rOPjs&myA|3$lc%JMmr~6;m z4{+gZY-V>9HrCTrYHSsc{l&z)kTNLE2%QT18)NS%aaL0hy|Li^ISTmU4+NN*(Te#J zwksNAGNx>4zdsoGh-h=#$QEAyT6lrm>Ej6o4*=upD9T-Vv!>@1G5)3 z)@+cAacawwtKbkba~1090CO1VH z76DGRYytp|9L;-|s3#h;Y2_Ii3k^4iBNXV8{|G#T?y2Vvg3f zZnXzw*;DElz!V?IKMFa0V82`n4oUQyGw0H8fA7C?c*S7+R;zThWVkUM+Z-m?DAW1F zq6kUCA@fYeXt$EZvw>De+MM~NEw7`$dbbJ-A{h_Dbd z@agD4K=Y;s|3^Y~6=%c#Y=AoVt1y$xk+7?I7Se)%>c-2TkCyq9ca=@wphCzHWedkp zP4LL=CGE)tW)-t_+7mbKI;G;?uQt;CS_#c_|>zlRIZ`}3bsq4=~7^qQ;)%y{yGr7qYf#hl75&H}WU2s<%MSJtaK2}&u^}(8t zFid3&JjB~2M;6Yp<$rb-zg0Soim;i>JCdoSUsLx4A{Kgb`>NxB({-`IL#I<^g3QHo zlh>5?RqrX%b4M9v!Pb=b_M=O=pl+3`LHBVa{YQ-467&KMC3>y_#UmT#XH?tCNh(?O z%Fe;jcfQODOGZV4-Nilj^rO6197gUDM;3cB(qJ=dT_Bf~qLr;OB8|K(piA5EUYQ0a z33pJRiHzN|2|Vtca>EOAv`71*1HTD&?MdBCP&I1k(b0Ez&e}*|S-nPgr?iUnG;!V> z(^T$~Ji|L&^okHw(mX7vuOOWai5am~)=0<75_tD=jDsy~W*!Rk>}&9~`~8GJWo ziR7QfK%x5gtTQFzg=mY+1WaYiw2C4yO@Dy}ncz0Uop`7| zU_xB5*J!NBCz?^O+bP}x*kNwy`Bi0VCv)dH{Z@B^BNE|mJg&!?32>Qik_{TamE`dE z8bx+2m9ON}4Rp#VB0->n*}RArx?ccBbkv8S6^SmdTo^A;A$w1-;MkmP2J($eIP(IB zA1q&Sf}^QU%xMOE*)3xRMl0x-EEp$!u+{ghOee{KXkwupSYL4$N~HNTT!WCm2dru zSofS@Q2)nNUL9oUtr!QS!vjAS9g+_OffB$K*raFZnL}w3+K(vJ8z76Jl70^sySG3R zx^({x8U@4Wp4cGU45(DMJD5o@#@vcc)V1c~u7OuWIJaL*V4fd^9MHVfocWN8)TYus zwjv;@ur=6|HA1)H#kbNq`Q2dvEKF0E_bi>f@+s;+liYR{a{9C{M5Bi0bk-BYgcnrC z2e=edJ!$Mgj_5@Hdkp|C0ABK>wHDCgdO|M___QM9jYz|58{j*OJ-Wy3sh$Dvn%(_qGEhSG| zX^q~sX`lG6zkXKewBRvRYo^1$Dm{GA(~3lH728{v&l;uqhcG6Q*1jQtMCI%K@wrxA zzpXVNqTihE)cUIb-Q5jtHY__5Mu4+BzZ>B9MDGh9=2&JK!nEf&7 z>y1kOZl-4G@Ec;rlMd2^dH0EH zawL7&AEWfD_6H7M%j~lA0~_gWcB}70?-ib=bpHKSJXqSytSo!$#@0r^p9iQ1hCs-4 z>7e6Sd3^j%1ouD6m+l%r;M5%1rfr+Chq!?0+1i|5MT+ChRGw>{#?3!cy&BBIa< z1}#(G&=!H7=k;7*QNt^f`7SJMW2Ug{oy?A&wI3Q=G7{+X#_{CktxV$g_u2~bNuJb_ zn)m?vXs{A#+*4iB>mB@K`tJ{a@mk<%PcbQRD>8Dj{#PCfquEv(78pi~&jjO+|4O3f zjW5zBIJ?Ki@-cBg0K_&%vI5}| zlXdI*H?4jFHSu%N!fa=e`I!Ksl;^E!y=fRJG3iO|CkxI#TI!5di3wkzHs74^p;Q~f z+E5>CtsunG3}AH9cH&#$NTIwf{XU>_5})(fZk3e0s{Z|L4jupfvux0l>s$p`{{vKK}<*w+}}E literal 0 HcmV?d00001 From 2f81a79374bbbcde77c1a2f2620386eb962ee8a4 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 6 Jan 2025 14:47:43 +0530 Subject: [PATCH 05/38] chore: update workspace parent, so its visible in sidebar --- .../gst_india/workspace/gst_india/gst_india.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/india_compliance/gst_india/workspace/gst_india/gst_india.json b/india_compliance/gst_india/workspace/gst_india/gst_india.json index 91e05b853d..24bfe3d694 100644 --- a/india_compliance/gst_india/workspace/gst_india/gst_india.json +++ b/india_compliance/gst_india/workspace/gst_india/gst_india.json @@ -246,7 +246,7 @@ "type": "Link" } ], - "modified": "2024-10-05 10:28:10.623892", + "modified": "2025-01-06 14:41:57.899872", "modified_by": "Administrator", "module": "GST India", "name": "GST India", @@ -265,7 +265,7 @@ } ], "owner": "Administrator", - "parent_page": "Accounting", + "parent_page": "", "public": 1, "quick_lists": [], "restrict_to_domain": "", @@ -274,7 +274,7 @@ "role": "Accounts Manager" } ], - "sequence_id": 3.0, + "sequence_id": 15.0, "shortcuts": [ { "color": "Grey", From 06f61fa888055acfab144b434f71e56f98cd0ea8 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 6 Jan 2025 17:35:08 +0530 Subject: [PATCH 06/38] test: ignore test dependencies from EPRNext --- .../doctype/bill_of_entry/test_bill_of_entry.py | 16 +++++++++++++--- .../doctype/gst_hsn_code/test_gst_hsn_code.py | 5 ++--- .../doctype/gst_settings/test_gst_settings.py | 5 ++--- .../gst_india/doctype/gstin/test_gstin.py | 4 ---- .../test_purchase_reconciliation_tool.py | 5 ++--- .../vat_india/doctype/c_form/test_c_form.py | 2 ++ 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py index 0fd56b4797..03161896a0 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py +++ b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py @@ -17,13 +17,23 @@ from india_compliance.gst_india.utils import get_gst_accounts_by_type from india_compliance.gst_india.utils.tests import create_purchase_invoice +IGNORE_TEST_RECORD_DEPENDENCIES = [ + "Bill of Entry", + "Purchase Invoice", + "Cost Center", + # "Tax Category", + "Item", + # "UOM", + "Item Tax Template", + "Project", + "Company", + "Account", +] + class TestBillofEntry(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["Bill of Entry"] = [] - super().setUpClass() frappe.db.set_single_value("GST Settings", "enable_overseas_transactions", 1) diff --git a/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py b/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py index 115ece33d7..60f2d109aa 100644 --- a/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py +++ b/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py @@ -9,13 +9,12 @@ update_taxes_in_item_master, ) +IGNORE_TEST_RECORD_DEPENDENCIES = ["Item Tax Template"] + class TestGSTHSNCode(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["GST HSN Code"] = [] - super().setUpClass() @change_settings("GST Settings", {"validate_hsn_code": 0}) diff --git a/india_compliance/gst_india/doctype/gst_settings/test_gst_settings.py b/india_compliance/gst_india/doctype/gst_settings/test_gst_settings.py index cadab46401..6e36e45d3c 100644 --- a/india_compliance/gst_india/doctype/gst_settings/test_gst_settings.py +++ b/india_compliance/gst_india/doctype/gst_settings/test_gst_settings.py @@ -6,13 +6,12 @@ from frappe.tests import IntegrationTestCase, change_settings from frappe.utils.data import getdate +IGNORE_TEST_RECORD_DEPENDENCIES = ["Company", "Account"] + class TestGSTSettings(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["GST Settings"] = [] - super().setUpClass() @change_settings("GST Settings", {"enable_api": 1}) diff --git a/india_compliance/gst_india/doctype/gstin/test_gstin.py b/india_compliance/gst_india/doctype/gstin/test_gstin.py index 7c575314ca..3729edb41a 100644 --- a/india_compliance/gst_india/doctype/gstin/test_gstin.py +++ b/india_compliance/gst_india/doctype/gstin/test_gstin.py @@ -3,7 +3,6 @@ import responses from responses import matchers -import frappe from frappe.tests import IntegrationTestCase, change_settings from india_compliance.gst_india.doctype.gstin.gstin import validate_gst_transporter_id @@ -28,9 +27,6 @@ class TestGSTIN(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["GSTIN"] = [] - super().setUpClass() @responses.activate diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py index 04417fde86..ada502782d 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py @@ -14,6 +14,8 @@ create_purchase_invoice as _create_purchase_invoice, ) +IGNORE_TEST_RECORD_DEPENDENCIES = ["Company"] + PURCHASE_INVOICE_DEFAULT_ARGS = { "bill_no": "BILL-23-00001", "bill_date": "2023-12-11", @@ -53,9 +55,6 @@ class TestPurchaseReconciliationTool(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["Purchase Reconciliation Tool"] = [] - super().setUpClass() # create 2023-2024 fiscal year diff --git a/india_compliance/vat_india/doctype/c_form/test_c_form.py b/india_compliance/vat_india/doctype/c_form/test_c_form.py index 449f6dec68..40ad5e46b7 100644 --- a/india_compliance/vat_india/doctype/c_form/test_c_form.py +++ b/india_compliance/vat_india/doctype/c_form/test_c_form.py @@ -5,6 +5,8 @@ # test_records = frappe.get_test_records('C-Form') +IGNORE_TEST_RECORD_DEPENDENCIES = ["Company", "Customer", "Sales Invoice", "Territory"] + class TestCForm(unittest.TestCase): pass From a319212a9a3ede1c4f7befad538ce926885bbdb6 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 31 Dec 2024 11:52:01 +0530 Subject: [PATCH 07/38] fix: correct taxes for overseas customer --- india_compliance/gst_india/overrides/transaction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 06eeba91bf..5ed8e0b5b2 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -933,7 +933,9 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F or ( is_sales_transaction and is_export_without_payment_of_gst( - frappe._dict({**party_details, "doctype": doctype}) + party_details.copy().update( + doctype=doctype, place_of_supply=gst_details.place_of_supply + ) ) ) or ( From ca89535029010b2d883ed9ade1370af6efb724b1 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 6 Jan 2025 19:03:58 +0530 Subject: [PATCH 08/38] chore: consistent implementation --- .../gst_india/overrides/transaction.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 5ed8e0b5b2..2238db3275 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -898,11 +898,12 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F gst_details.update(party_gst_details) # POS - gst_details.place_of_supply = ( - party_details.place_of_supply - if (not update_place_of_supply and party_details.place_of_supply) - else get_place_of_supply(party_details, doctype) - ) + if not update_place_of_supply and party_details.place_of_supply: + gst_details.place_of_supply = party_details.place_of_supply + else: + place_of_supply = get_place_of_supply(party_details, doctype) + gst_details.place_of_supply = place_of_supply + party_details.place_of_supply = place_of_supply # set is_reverse_charge as per party_gst_details if not set if not is_sales_transaction and "is_reverse_charge" not in party_details: @@ -933,9 +934,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F or ( is_sales_transaction and is_export_without_payment_of_gst( - party_details.copy().update( - doctype=doctype, place_of_supply=gst_details.place_of_supply - ) + frappe._dict({**party_details, "doctype": doctype}) ) ) or ( @@ -979,11 +978,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F if default_tax := get_tax_template( master_doctype, company, - is_inter_state_supply( - party_details.copy().update( - doctype=doctype, place_of_supply=gst_details.place_of_supply - ), - ), + is_inter_state_supply(frappe._dict({**party_details, "doctype": doctype})), party_details.get(company_gstin_field)[:2], party_details.is_reverse_charge, ): From 33a4fe4014388d5368db67fbf5d9afadc6432619 Mon Sep 17 00:00:00 2001 From: Ninad1306 Date: Tue, 7 Jan 2025 18:39:59 +0530 Subject: [PATCH 09/38] fix: set recon status for unlinked document --- .../purchase_reconciliation_tool.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py index e9dc84c188..3dc69007f1 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -242,6 +242,9 @@ def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype if isup_linked_with := frappe.db.get_value( "GST Inward Supply", inward_supply_name, "link_name" ): + self.set_reconciliation_status( + link_doctype, (isup_linked_with,), "Unreconciled" + ) self._unlink_documents((inward_supply_name,)) purchases.append(isup_linked_with) @@ -268,7 +271,7 @@ def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype self.db_set("is_modified", 1) self.set_reconciliation_status( - link_doctype, [purchase_invoice_name], "Match Found" + link_doctype, (purchase_invoice_name,), "Match Found" ) return self.ReconciledData.get(purchases, inward_supplies) From 7206669a87539bb15a9bb7f266b9840d86c6034c Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 8 Jan 2025 10:03:20 +0530 Subject: [PATCH 10/38] test: restore meta for autoname as it's cached --- .../gst_india/overrides/test_purchase_invoice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/india_compliance/gst_india/overrides/test_purchase_invoice.py b/india_compliance/gst_india/overrides/test_purchase_invoice.py index cc887c16b5..54b2f56fb9 100644 --- a/india_compliance/gst_india/overrides/test_purchase_invoice.py +++ b/india_compliance/gst_india/overrides/test_purchase_invoice.py @@ -77,11 +77,12 @@ def test_validate_invoice_length(self): do_not_save=True, ) setattr(pinv, "__newname", "INV/2022/00001/asdfsadg") # NOQA - pinv.meta.autoname = "prompt" - pinv.save() self.assertEqual( frappe.parse_json(frappe.message_log[-1]).get("message"), "Transaction Name must be 16 characters or fewer to meet GST requirements", ) + + # Reset autoname (as it's cached) + pinv.meta.autoname = "naming_series:" From 49a431e95635f016b9886305ed599b8fbbb20d09 Mon Sep 17 00:00:00 2001 From: Sanket Shah <113279972+Sanket322@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:30:45 +0530 Subject: [PATCH 11/38] fix(gstr-1): handle gov api error when downloading and make it optional for unfiled returns (#2897) Co-authored-by: Sanket322 Co-authored-by: Smit Vora --- .../doctype/gst_return_log/generate_gstr_1.py | 20 +++++++- .../doctype/gst_return_log/gst_return_log.py | 21 +++++--- .../doctype/gst_settings/gst_settings.json | 29 +++++++---- .../doctype/gstr_1_beta/gstr_1_beta.js | 50 +++++++++++-------- .../doctype/gstr_1_beta/gstr_1_beta.py | 15 ++++-- india_compliance/gst_india/setup/__init__.py | 3 +- india_compliance/patches.txt | 1 + .../patches/v14/rename_gstr1_settings.py | 12 +++++ .../v14/update_default_gstr1_settings.py | 2 +- 9 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 india_compliance/patches/v14/rename_gstr1_settings.py diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index e62a22490c..cb71b83bb5 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -555,8 +555,26 @@ def generate_gstr1_data(self, filters, callback=None): else: gov_data_field = "unfiled" + if ( + status != "Filed" + and frappe.get_cached_value("GST Settings", None, "compare_unfiled_data") + != 1 + ): + return self.generate_only_books_data(data, filters, callback) + # Get Data - gov_data, is_enqueued = self.get_gov_gstr1_data() + try: + gov_data, is_enqueued = self.get_gov_gstr1_data() + except frappe.ValidationError as error: + self.generate_only_books_data(data, filters) + self.update_status("Failed", commit=True) + + error_log = frappe.log_error( + title="GSTR-1 Generation Failed", + message=str(error), + reference_doctype="GSTR-1 Beta", + ) + return callback and callback(filters, error_log.name) books_data = self.get_books_gstr1_data(filters) diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py index f203ef4797..6e32d29d7e 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -33,11 +33,11 @@ def update_status(self, status, commit=False): self.db_set("generation_status", status, commit=commit) # FILE UTILITY - def load_data(self, file_field=None): + def load_data(self, *file_field): data = {} if file_field: - file_fields = [file_field] + file_fields = list(file_field) else: file_fields = self.get_applicable_file_fields() @@ -128,7 +128,7 @@ def is_gstr1_api_enabled(self, settings=None, warn_for_missing_credentials=False if not is_production_api_enabled(settings): return False - if not settings.compare_gstr_1_data: + if not settings.enable_gstr_1_api: return False if not settings.has_valid_credentials(self.gstin, "Returns"): @@ -184,15 +184,20 @@ def get_return_status(self): def get_applicable_file_fields(self, settings=None): # Books aggregated data stored in filed (as to file) + if not settings: + settings = frappe.get_cached_doc("GST Settings") + fields = ["books", "books_summary"] if self.is_gstr1_api_enabled(settings): - fields.extend(["reconcile", "reconcile_summary"]) - if self.filing_status == "Filed": - fields.extend(["filed", "filed_summary"]) - else: - fields.extend(["unfiled", "unfiled_summary"]) + fields.extend( + ["reconcile", "reconcile_summary", "filed", "filed_summary"] + ) + elif settings.compare_unfiled_data: + fields.extend( + ["reconcile", "reconcile_summary", "unfiled", "unfiled_summary"] + ) return fields diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json index 391cabae49..bcb34dae2a 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json @@ -48,7 +48,8 @@ "e_invoice_applicable_from", "e_invoice_applicable_companies", "gstr_1_section_break", - "compare_gstr_1_data", + "enable_gstr_1_api", + "compare_unfiled_data", "filing_frequency", "column_break_cxmn", "restrict_changes_after_gstr_1", @@ -610,14 +611,6 @@ "fieldtype": "Check", "label": "Restrict Changes to Transactions After Filing" }, - { - "default": "0", - "depends_on": "eval: india_compliance.is_api_enabled(doc)", - "description": "Use APIs to compare records with GST Portal Data Before and After Filing", - "fieldname": "compare_gstr_1_data", - "fieldtype": "Check", - "label": "Compare Data with GST Portal" - }, { "default": "0", "depends_on": "eval: doc.enable_e_waybill", @@ -668,12 +661,28 @@ "label": "Default Reason for e-Invoice Cancellation", "mandatory_depends_on": "eval: doc.auto_cancel_e_invoice", "options": "Duplicate\nOrder Cancelled\nData Entry Mistake" + }, + { + "default": "1", + "depends_on": "eval: india_compliance.is_api_enabled(doc) && doc.enable_gstr_1_api", + "description": "Use API to compare records with GST Portal Before Filing.", + "fieldname": "compare_unfiled_data", + "fieldtype": "Check", + "label": "Compare Data with GST Portal Before Filing" + }, + { + "default": "0", + "depends_on": "eval: india_compliance.is_api_enabled(doc)", + "description": "Use APIs to compare records with GST Portal and File GST Returns", + "fieldname": "enable_gstr_1_api", + "fieldtype": "Check", + "label": "Enable GSTR-1 API Features" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-12-12 19:09:07.185191", + "modified": "2025-01-07 16:34:55.885085", "modified_by": "Administrator", "module": "GST India", "name": "GST Settings", diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 272e0ddc00..378fe8adc9 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -158,7 +158,7 @@ frappe.ui.form.on(DOCTYPE, { }); frappe.realtime.on("gstr1_data_prepared", message => { - const { filters } = message; + const { filters, error_log } = message; if ( frm.doc.company_gstin !== filters.company_gstin || @@ -167,7 +167,17 @@ frappe.ui.form.on(DOCTYPE, { ) return; - frm.taxpayer_api_call("generate_gstr1").then(r => { + const only_books_data = error_log != undefined; + if (error_log) { + frappe.msgprint({ + message: __("Error while preparing GSTR-1 data, please check {0} for more deatils.", + [`error log`]), + title: "GSTR-1 Download Failed", + indicator: "red", + }) + } + + frm.taxpayer_api_call("generate_gstr1", { only_books_data }).then(r => { frm.doc.__gst_data = r.message; frm.trigger("load_gstr1_data"); }); @@ -1140,10 +1150,10 @@ class TabManager { args[2]?.indent == 0 ? `${value}` : isDescriptionCell - ? ` + ? `

${value}

` - : value; + : value; return value; } @@ -1898,9 +1908,9 @@ class FiledTab extends GSTR1_TabManager { const { include_uploaded, delete_missing } = dialog ? dialog.get_values() : { - include_uploaded: true, - delete_missing: false, - }; + include_uploaded: true, + delete_missing: false, + }; const doc = me.instance.frm.doc; @@ -2144,7 +2154,7 @@ class ReconcileTab extends FiledTab { }); } - get_creation_time_string() {} // pass + get_creation_time_string() { } // pass get_detail_view_column() { return [ @@ -2218,8 +2228,8 @@ class ErrorsTab extends TabManager { ]; } - setup_actions() {} - set_creation_time_string() {} + setup_actions() { } + set_creation_time_string() { } refresh_data(data) { data = data.error_report; @@ -2476,17 +2486,17 @@ class FileGSTR1Dialog { ${description} ${format_currency( - liability.total_igst_amount - )} + liability.total_igst_amount + )} ${format_currency( - liability.total_cgst_amount - )} + liability.total_cgst_amount + )} ${format_currency( - liability.total_sgst_amount - )} + liability.total_sgst_amount + )} ${format_currency( - liability.total_cess_amount - )} + liability.total_cess_amount + )} `; } @@ -2858,12 +2868,12 @@ function is_gstr1_api_enabled() { return ( india_compliance.is_api_enabled() && !gst_settings.sandbox_mode && - gst_settings.compare_gstr_1_data + gst_settings.enable_gstr_1_api ); } function patch_set_indicator(frm) { - frm.toolbar.set_indicator = function () {}; + frm.toolbar.set_indicator = function () { }; } async function set_default_company_gstin(frm) { diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py index e4f4277763..2df7c1654f 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py @@ -56,7 +56,9 @@ def mark_as_filed(self): @frappe.whitelist() @otp_handler - def generate_gstr1(self, sync_for=None, recompute_books=False, message=None): + def generate_gstr1( + self, sync_for=None, recompute_books=False, only_books_data=None, message=None + ): period = get_period(self.month_or_quarter, self.year) # get gstr1 log @@ -98,7 +100,12 @@ def generate_gstr1(self, sync_for=None, recompute_books=False, message=None): if recompute_books: gstr1_log.remove_json_for("books") - # files are already present + # failed while downloading gov data + if only_books_data: + data = gstr1_log.load_data("books", "books_summary") + data["status"] = gstr1_log.filing_status or "Not Filed" + return data + if gstr1_log.has_all_files(settings): data = gstr1_log.get_gstr1_data() @@ -147,7 +154,7 @@ def _generate_gstr1(self): raise e - def on_generate(self, filters=None): + def on_generate(self, filters=None, error_log=None): """ Once data is generated, update the status and publish the data """ @@ -161,7 +168,7 @@ def on_generate(self, filters=None): frappe.publish_realtime( "gstr1_data_prepared", - message={"filters": filters}, + message={"filters": filters, "error_log": error_log}, user=frappe.session.user, doctype=self.doctype, ) diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py index 44336725cc..848a91e003 100644 --- a/india_compliance/gst_india/setup/__init__.py +++ b/india_compliance/gst_india/setup/__init__.py @@ -226,7 +226,8 @@ def set_default_gst_settings(): "reconcile_for_b2b": 1, "reconcile_for_cdnr": 1, # GSTR-1 - "compare_gstr_1_data": 1, + "enable_gstr_1_api": 1, + "compare_unfiled_data": 1, "freeze_transactions": 1, "filing_frequency": "Monthly", } diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index f40bf933b2..d667a4c541 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -54,6 +54,7 @@ india_compliance.patches.v14.set_item_details_from_purchase_invoice_to_bill_of_e india_compliance.patches.v14.update_item_gst_details_and_gst_trearment_in_bill_of_entry india_compliance.patches.v14.update_default_auto_reconciliation_settings india_compliance.patches.v14.update_default_gstr1_settings +india_compliance.patches.v14.rename_gstr1_settings india_compliance.patches.v14.stop_unnecessary_scheduled_jobs india_compliance.patches.v14.set_reconciliation_status_for_manual_match india_compliance.patches.v14.add_match_found_in_purchase_reconciliation_status diff --git a/india_compliance/patches/v14/rename_gstr1_settings.py b/india_compliance/patches/v14/rename_gstr1_settings.py new file mode 100644 index 0000000000..d27295fe5b --- /dev/null +++ b/india_compliance/patches/v14/rename_gstr1_settings.py @@ -0,0 +1,12 @@ +import frappe +from frappe.utils import sbool + + +def execute(): + settings = frappe.get_cached_doc("GST Settings") + if not sbool(settings.get("compare_gstr_1_data")): + return + + frappe.db.set_single_value( + "GST Settings", {"enable_gstr_1_api": 1, "compare_unfiled_data": 1} + ) diff --git a/india_compliance/patches/v14/update_default_gstr1_settings.py b/india_compliance/patches/v14/update_default_gstr1_settings.py index e8f4ef9865..032fa3d345 100644 --- a/india_compliance/patches/v14/update_default_gstr1_settings.py +++ b/india_compliance/patches/v14/update_default_gstr1_settings.py @@ -5,7 +5,7 @@ def execute(): frappe.db.set_single_value( "GST Settings", { - "compare_gstr_1_data": 1, + "enable_gstr_1_api": 1, "freeze_transactions": 1, "filing_frequency": "Monthly", }, From fc9e366f02e0768ad88dcb59a470c5756911f22e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 8 Jan 2025 12:38:03 +0530 Subject: [PATCH 12/38] fix: support purchases from tax collector in purchae reco tool --- .../doctype/purchase_reconciliation_tool/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py index 5849065e3d..cb28516ceb 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py @@ -376,7 +376,12 @@ def get_all(self, additional_fields=None, names=None, only_names=False): def get_unmatched(self, category): gst_category = ( - ("Registered Regular", "Tax Deductor", "Input Service Distributor") + ( + "Registered Regular", + "Tax Deductor", + "Tax Collector", + "Input Service Distributor", + ) if category in ("B2B", "CDNR", "ISD") else ("SEZ", "Overseas", "UIN Holders") ) @@ -412,7 +417,7 @@ def get_query(self, additional_fields=None, is_return=False): .on(self.PI_ITEM.parent == self.PI.name) .where(self.PI.docstatus == 1) .where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable") - .where(self.PI.is_opening == "NO") + .where(self.PI.is_opening == "No") .where(self.PI_ITEM.parenttype == "Purchase Invoice") .groupby(self.PI.name) .select( @@ -1227,6 +1232,7 @@ def guess_classification(doc): "Overseas": "IMPG", "UIN Holders": "B2B", "Tax Deductor": "B2B", + "Tax Collector": "B2B", "Input Service Distributor": "B2B", } From cc0e7e53efd81c26605bd68bf1a7702596b4ab7e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 8 Jan 2025 12:57:03 +0530 Subject: [PATCH 13/38] fix: selectively download sections in GSTR-1 based on summary information --- .../gst_india/utils/gstr_1/gstr_1_download.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index d6366e9648..5833535a3c 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -10,7 +10,7 @@ convert_to_internal_data_format, ) -UNFILED_ACTIONS = [ +ACTIONS = [ "B2B", "B2CL", "B2CS", @@ -25,8 +25,6 @@ "DOCISS", ] -FILED_ACTIONS = [*UNFILED_ACTIONS, "RETSUM"] - def download_gstr1_json_data(gstr1_log): """ @@ -39,16 +37,19 @@ def download_gstr1_json_data(gstr1_log): json_data = frappe._dict() api = GSTR1API(gstr1_log) + summary = api.get_gstr_1_data("RETSUM", return_period) + if gstr1_log.filing_status == "Filed": return_type = "GSTR1" - actions = FILED_ACTIONS data_field = "filed" + json_data.update(summary) else: return_type = "Unfiled GSTR1" - actions = UNFILED_ACTIONS data_field = "unfiled" + actions = get_sections_to_download(summary) + # download data for action in actions: response = api.get_gstr_1_data(action, return_period) @@ -119,3 +120,7 @@ def save_gstr_1_filed_data(gstin, return_period, json_data): def save_gstr_1_unfiled_data(gstin, return_period, json_data): save_gstr_1(gstin, return_period, json_data, "Unfiled GSTR1") + + +def get_sections_to_download(summary): + return ACTIONS From dc0be5dfebb995bec7cecc4b29113f552dd74e4b Mon Sep 17 00:00:00 2001 From: Ninad Parikh <109862100+Ninad1306@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:41:16 +0530 Subject: [PATCH 14/38] fix: unique name for events and doctype parameter alone is redundant (#2903) Huly®: IC-3030 --- .../doctype/gst_return_log/gst_return_log.py | 5 +-- .../doctype/gstr_1_beta/gstr_1_beta.js | 4 +-- .../doctype/gstr_1_beta/gstr_1_beta.py | 2 -- .../purchase_reconciliation_tool.js | 22 ++++++------- .../purchase_reconciliation_tool.py | 10 ++++-- .../gst_india/utils/gstr_1/gstr_1_download.py | 1 - .../gst_india/utils/gstr_2/__init__.py | 32 ++++++++++--------- .../gst_india/utils/gstr_2/gstr.py | 3 +- 8 files changed, 39 insertions(+), 40 deletions(-) diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py index 6e32d29d7e..a0f45e861e 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -134,7 +134,7 @@ def is_gstr1_api_enabled(self, settings=None, warn_for_missing_credentials=False if not settings.has_valid_credentials(self.gstin, "Returns"): if warn_for_missing_credentials: frappe.publish_realtime( - "show_message", + "show_missing_gst_credentials_message", dict( message=_( "Credentials are missing for GSTIN {0} for service" @@ -308,9 +308,10 @@ def update_is_not_latest_gstr1_data(posting_date, company_gstin): ) frappe.publish_realtime( - "is_not_latest_data", + "is_not_latest_gstr1_data", message={"filters": {"company_gstin": company_gstin, "period": period}}, doctype="GSTR-1 Beta", + docname="GSTR-1 Beta", ) diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 378fe8adc9..42aa5de9d5 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -118,7 +118,7 @@ frappe.ui.form.on(DOCTYPE, { frm.__setup_complete = true; // Setup Listeners - frappe.realtime.on("is_not_latest_data", message => { + frappe.realtime.on("is_not_latest_gstr1_data", message => { const { filters } = message; const [month_or_quarter, year] = @@ -143,7 +143,7 @@ frappe.ui.form.on(DOCTYPE, { ); }); - frappe.realtime.on("show_message", message => { + frappe.realtime.on("show_missing_gst_credentials_message", message => { frappe.msgprint(message); }); diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py index 2df7c1654f..6c2c8f1e59 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py @@ -149,7 +149,6 @@ def _generate_gstr1(self): "gstr1_generation_failed", message={"error": str(e), "filters": filters}, user=frappe.session.user, - doctype=self.doctype, ) raise e @@ -170,7 +169,6 @@ def on_generate(self, filters=None, error_log=None): "gstr1_data_prepared", message={"filters": filters, "error_log": error_log}, user=frappe.session.user, - doctype=self.doctype, ) diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js index b8b37a27f7..2536225abb 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js @@ -72,7 +72,7 @@ frappe.ui.form.on(DOCTYPE, { frm.trigger("company"); frm.purchase_reconciliation_tool = new PurchaseReconciliationTool(frm); - frm.events.handle_download_failure(frm); + frm.events.handle_download_message(frm); }, onload(frm) { @@ -189,11 +189,11 @@ frappe.ui.form.on(DOCTYPE, { show_progress(frm, type) { if (type == "download") { frappe.run_serially([ - () => frm.events.update_progress(frm, "update_api_progress"), - () => frm.events.update_progress(frm, "update_transactions_progress"), + () => frm.events.update_progress(frm, "update_2a_2b_api_progress"), + () => frm.events.update_progress(frm, "update_2a_2b_transactions_progress"), ]); } else if (type == "upload") { - frm.events.update_progress(frm, "update_transactions_progress"); + frm.events.update_progress(frm, "update_2a_2b_transactions_progress"); } }, @@ -201,7 +201,7 @@ frappe.ui.form.on(DOCTYPE, { frappe.realtime.on(method, data => { const { current_progress } = data; const message = - method == "update_api_progress" + method == "update_2a_2b_api_progress" ? __("Fetching data from GSTN") : __("Updating Inward Supply for Return Period {0}", [ data.return_period, @@ -217,7 +217,7 @@ frappe.ui.form.on(DOCTYPE, { } if ( current_progress === 100 && - method != "update_api_progress" && + method != "update_2a_2b_api_progress" && frm.flag_last_return_period == data.return_period ) { setTimeout(() => { @@ -233,14 +233,10 @@ frappe.ui.form.on(DOCTYPE, { }); }, - handle_download_failure(frm) { - frappe.realtime.on("gstr_2a_2b_download_failed", message => { + handle_download_message(frm) { + frappe.realtime.on("gstr_2a_2b_download_message", message => { frm.dashboard.hide(); - frappe.msgprint({ - title: __("2A/2B Download Failed"), - message: message.error, - indicator: "red", - }); + frappe.msgprint(message); }); }, }); diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py index 3dc69007f1..68505eb0d1 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -6,6 +6,7 @@ from typing import List import frappe +from frappe import _ from frappe.model.document import Document from frappe.query_builder.functions import IfNull from frappe.utils import add_to_date, cint, now_datetime @@ -475,10 +476,13 @@ def download_gstr( except Exception as e: frappe.publish_realtime( - "gstr_2a_2b_download_failed", - {"error": str(e)}, + "gstr_2a_2b_download_message", + { + "title": _("2A/2B Download Failed"), + "message": str(e), + "indicator": "red", + }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index d6366e9648..e46c211296 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -84,7 +84,6 @@ def download_gstr1_json_data(gstr1_log): "gstr1_queued", message={"gstin": gstin, "return_period": return_period}, user=frappe.session.user, - doctype="GSTR-1 Beta", ) return mapped_data, is_queued diff --git a/india_compliance/gst_india/utils/gstr_2/__init__.py b/india_compliance/gst_india/utils/gstr_2/__init__.py index 6efdf64a4b..01ad6d012b 100644 --- a/india_compliance/gst_india/utils/gstr_2/__init__.py +++ b/india_compliance/gst_india/utils/gstr_2/__init__.py @@ -59,14 +59,13 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None): requests_made += 1 frappe.publish_realtime( - "update_api_progress", + "update_2a_2b_api_progress", { "current_progress": requests_made * 100 / total_expected_requests, "return_period": return_period, "is_last_period": is_last_period, }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) if gst_categories and category.value not in gst_categories: @@ -116,7 +115,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None): save_gstr_2a(gstin, return_period, json_data) if queued_message: - show_queued_message() + publish_queued_message() if not has_data: end_transaction_progress(return_period) @@ -133,14 +132,13 @@ def download_gstr_2b(gstin, return_periods): is_last_period = return_periods[-1] == return_period requests_made += 1 frappe.publish_realtime( - "update_api_progress", + "update_2a_2b_api_progress", { "current_progress": requests_made * 100 / total_expected_requests, "return_period": return_period, "is_last_period": is_last_period, }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) response = api.get_data(return_period) @@ -185,7 +183,7 @@ def download_gstr_2b(gstin, return_periods): save_gstr_2b(gstin, return_period, response) if queued_message: - show_queued_message() + publish_queued_message() if not has_data: end_transaction_progress(return_period) @@ -302,13 +300,18 @@ def _download_gstr_2a(gstin, return_period, json_data): save_gstr_2a(gstin, return_period, json_data) -def show_queued_message(): - frappe.msgprint( - _( - "Some returns are queued for download at GSTN as there may be large data." - " We will retry download every few minutes until it succeeds.

" - "You can track download status from download dialog." - ) +def publish_queued_message(): + frappe.publish_realtime( + "gstr_2a_2b_download_message", + { + "title": _("2A/2B Download Queued"), + "message": _( + "Some returns are queued for download at GSTN as there may be large data." + " We will retry download every few minutes until it succeeds.

" + "You can track download status from download dialog." + ), + }, + user=frappe.session.user, ) @@ -319,12 +322,11 @@ def end_transaction_progress(return_period): """ frappe.publish_realtime( - "update_transactions_progress", + "update_2a_2b_transactions_progress", { "current_progress": 100, "return_period": return_period, "is_last_period": True, }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) diff --git a/india_compliance/gst_india/utils/gstr_2/gstr.py b/india_compliance/gst_india/utils/gstr_2/gstr.py index 7677e0a767..ce284940b1 100644 --- a/india_compliance/gst_india/utils/gstr_2/gstr.py +++ b/india_compliance/gst_india/utils/gstr_2/gstr.py @@ -62,13 +62,12 @@ def create_transactions(self, category, suppliers): current_transaction += 1 frappe.publish_realtime( - "update_transactions_progress", + "update_2a_2b_transactions_progress", { "current_progress": current_transaction * 100 / total_transactions, "return_period": self.return_period, }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) if transaction.get("unique_key") in self.existing_transaction: From 6549244b082f7def125ab48632388489bd10295f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 10:14:46 +0530 Subject: [PATCH 15/38] ci: correct mariadb-client version --- .github/helper/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index cb45c35d4f..e261b700f8 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -16,7 +16,7 @@ echo "Setting Up System Dependencies..." sudo apt update sudo apt remove mysql-server mysql-client -sudo apt install libcups2-dev redis-server mariadb-client-10.6 +sudo apt install libcups2-dev redis-server mariadb-client install_whktml() { if [ "$(lsb_release -rs)" = "22.04" ]; then From ab04e691099db3c17e33f37f33432c9e11511f12 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 10:18:37 +0530 Subject: [PATCH 16/38] ci: always install wkhtmltopdf --- .github/helper/install.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index e261b700f8..bccc61cea7 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -19,13 +19,8 @@ sudo apt remove mysql-server mysql-client sudo apt install libcups2-dev redis-server mariadb-client install_whktml() { - if [ "$(lsb_release -rs)" = "22.04" ]; then - wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb - sudo apt install /tmp/wkhtmltox.deb - else - echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)" - exit 1 - fi + wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb + sudo apt install /tmp/wkhtmltox.deb } install_whktml & wkpid=$! From a197267b40380dd9410bab1016878b7f0f2b2911 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 12:16:12 +0530 Subject: [PATCH 17/38] fix: only download sections with records --- .../gst_india/utils/gstr_1/gstr_1_download.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index 5833535a3c..0b41236822 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -6,6 +6,7 @@ from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import ( create_import_log, ) +from india_compliance.gst_india.utils.gstr_1 import GovJsonKey from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import ( convert_to_internal_data_format, ) @@ -123,4 +124,33 @@ def save_gstr_1_unfiled_data(gstin, return_period, json_data): def get_sections_to_download(summary): - return ACTIONS + if summary.isnil: + return [] + + SECTION_ACTION_MAP = { + "B2B": "B2B", + "B2CL": "B2CL", + "B2CS": "B2CS", + "CDNR": "CDNR", + "CDNUR": "CDNUR", + "EXP": "EXP", + "NIL": "NIL", + "AT": "AT", + "TXPD": "TXP", + "HSN": "HSNSUM", + "DOC_ISSUE": "DOCISS", + } + + actions = set() + + for row in summary.get(GovJsonKey.RET_SUM.value): + section = row.get("sec_nm") + + # total no of records + if row.get("ttl_rec") == 0: + continue + + if section in SECTION_ACTION_MAP: + actions.add(SECTION_ACTION_MAP[section]) + + return list(actions) From 117151ab2588477f1623ed8f8728ac8d53bc4f1f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 15:28:32 +0530 Subject: [PATCH 18/38] fix: suggest default return period correct when in first period --- .../gst_india/doctype/gstr_1_beta/gstr_1_beta.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 42aa5de9d5..8b2a11f906 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -174,7 +174,7 @@ frappe.ui.form.on(DOCTYPE, { [`error log`]), title: "GSTR-1 Download Failed", indicator: "red", - }) + }); } frm.taxpayer_api_call("generate_gstr1", { only_books_data }).then(r => { @@ -2894,12 +2894,19 @@ async function set_default_company_gstin(frm) { function set_options_for_year(frm) { const today = new Date(); - const current_year = today.getFullYear(); + let current_year = today.getFullYear(); + const current_month_idx = today.getMonth(); const start_year = 2017; const year_range = current_year - start_year + 1; let options = Array.from({ length: year_range }, (_, index) => start_year + index); options = options.reverse().map(year => year.toString()); + if ( + (frm.filing_frequency === "Monthly" && current_month_idx === 0) || + (frm.filing_frequency === "Quarterly" && current_month_idx < 3) + ) + current_year--; + frm.get_field("year").set_data(options); frm.set_value("year", current_year.toString()); } @@ -2945,7 +2952,7 @@ function set_options_for_month_or_quarter(frm) { } set_field_options("month_or_quarter", options); - if (frm.doc.year === current_year) + if (frm.doc.year === current_year && options.length > 1) // set second last option as default frm.set_value("month_or_quarter", options[options.length - 2]); // set last option as default From 7813727d48e3d528d053b2fb0b34d364ecb59d7e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 09:45:09 +0530 Subject: [PATCH 19/38] fix: save nil filing information in gst return log --- .../doctype/gst_return_log/generate_gstr_1.py | 2 ++ .../gst_return_log/gst_return_log.json | 10 +++++++++- .../doctype/gstr_1_beta/gstr_1_beta.js | 20 ++++++++++++++----- .../gst_india/utils/gstr_1/gstr_1_download.py | 3 +++ .../js/components/data_table_manager.js | 5 ++++- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index cb71b83bb5..86f5667aae 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -513,6 +513,8 @@ def get_gstr1_data(self): data = data data["status"] = self.filing_status or "Not Filed" + data["is_nil"] = self.is_nil + if error_data := self.get_json_for("upload_error"): data["errors"] = error_data diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json index e43a0084a2..40cf63b699 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json @@ -12,6 +12,7 @@ "return_period", "company", "return_type", + "is_nil", "column_break_sqwh", "filing_date", "acknowledgement_number", @@ -214,6 +215,13 @@ "label": "Actions", "options": "GSTR Action", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_nil", + "fieldtype": "Check", + "label": "Is Nil Return", + "read_only": 1 } ], "in_create": 1, @@ -224,7 +232,7 @@ "link_fieldname": "reference_docname" } ], - "modified": "2024-10-02 12:16:04.271962", + "modified": "2025-01-10 08:19:45.848389", "modified_by": "Administrator", "module": "GST India", "name": "GST Return Log", diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 8b2a11f906..2865a29512 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -873,7 +873,7 @@ class TabManager { this.status = status; this.remove_tab_custom_buttons(); this.setup_actions(); - this.datatable.refresh(this.summary); + this.datatable.refresh(this.summary, null, this.get_no_data_message()); this.set_default_title(); this.set_creation_time_string(); } @@ -989,7 +989,7 @@ class TabManager { showTotalRow: true, checkboxColumn: false, treeView: treeView, - noDataMessage: this.DEFAULT_NO_DATA_MESSAGE, + noDataMessage: this.get_no_data_message(), headerDropdown: [ { label: "Collapse All Node", @@ -1033,7 +1033,6 @@ class TabManager { }, }, }, - no_data_message: __("No data found"), }); this.setup_datatable_listeners(treeView); @@ -1186,6 +1185,10 @@ class TabManager { `; } + + get_no_data_message() { + return this.DEFAULT_NO_DATA_MESSAGE; + } } class GSTR1_TabManager extends TabManager { @@ -2108,6 +2111,11 @@ class FiledTab extends GSTR1_TabManager { }, ]; } + + get_no_data_message() { + if (this.instance.data?.is_nil) + return __("You have filed a Nil GSTR-1 for this period"); + } } class UnfiledTab extends FiledTab { @@ -2126,8 +2134,6 @@ class UnfiledTab extends FiledTab { } class ReconcileTab extends FiledTab { - DEFAULT_NO_DATA_MESSAGE = __("No differences found"); - set_default_title() { if (this.instance.data.status === "Filed") this.DEFAULT_TITLE = "Books vs Filed"; @@ -2182,6 +2188,10 @@ class ReconcileTab extends FiledTab { }, ]; } + + get_no_data_message() { + return __("No differences found"); + } } class ErrorsTab extends TabManager { diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index 6b0eb0c327..3ff25c38e7 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -76,6 +76,9 @@ def download_gstr1_json_data(gstr1_log): json_data.update(response) + if json_data.isnil == "Y": + gstr1_log.db_set("is_nil", 1) + mapped_data = convert_to_internal_data_format(json_data) gstr1_log.update_json_for(data_field, mapped_data, reset_reconcile=True) diff --git a/india_compliance/public/js/components/data_table_manager.js b/india_compliance/public/js/components/data_table_manager.js index bcd375e13a..8150de9d3b 100644 --- a/india_compliance/public/js/components/data_table_manager.js +++ b/india_compliance/public/js/components/data_table_manager.js @@ -23,8 +23,11 @@ india_compliance.DataTableManager = class DataTableManager { } } - refresh(data, columns) { + refresh(data, columns, noDataMessage) { this.data = data; + if (noDataMessage) + this.datatable.noDataMessage = noDataMessage; + this.datatable.refresh(data, columns); } From 4d4d6ec6b3a45528c4869f3476c45760978e7dd2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 10 Jan 2025 18:49:31 +0530 Subject: [PATCH 20/38] fix: selective download supported only for filled information, fixes related to updating description --- .../gst_india/doctype/gstr_1_beta/gstr_1_beta.js | 2 ++ .../gst_india/utils/gstr_1/gstr_1_download.py | 10 +++++----- .../public/js/components/data_table_manager.js | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 2865a29512..6f1844f9ee 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -2115,6 +2115,8 @@ class FiledTab extends GSTR1_TabManager { get_no_data_message() { if (this.instance.data?.is_nil) return __("You have filed a Nil GSTR-1 for this period"); + + return this.DEFAULT_NO_DATA_MESSAGE; } } diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index 3ff25c38e7..79563fbc21 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -38,18 +38,18 @@ def download_gstr1_json_data(gstr1_log): json_data = frappe._dict() api = GSTR1API(gstr1_log) - summary = api.get_gstr_1_data("RETSUM", return_period) - if gstr1_log.filing_status == "Filed": return_type = "GSTR1" data_field = "filed" + + summary = api.get_gstr_1_data("RETSUM", return_period) + actions = get_sections_to_download(summary) json_data.update(summary) else: return_type = "Unfiled GSTR1" data_field = "unfiled" - - actions = get_sections_to_download(summary) + actions = ACTIONS # download data for action in actions: @@ -145,7 +145,7 @@ def get_sections_to_download(summary): actions = set() - for row in summary.get(GovJsonKey.RET_SUM.value): + for row in summary.get(GovJsonKey.RET_SUM.value, []): section = row.get("sec_nm") # total no of records diff --git a/india_compliance/public/js/components/data_table_manager.js b/india_compliance/public/js/components/data_table_manager.js index 8150de9d3b..b56f13f699 100644 --- a/india_compliance/public/js/components/data_table_manager.js +++ b/india_compliance/public/js/components/data_table_manager.js @@ -26,7 +26,7 @@ india_compliance.DataTableManager = class DataTableManager { refresh(data, columns, noDataMessage) { this.data = data; if (noDataMessage) - this.datatable.noDataMessage = noDataMessage; + this.datatable.options.noDataMessage = noDataMessage; this.datatable.refresh(data, columns); } From 00ec2cad30c2d8eb5e36bb4f30bbad2eb17ffead Mon Sep 17 00:00:00 2001 From: Sanket Shah <113279972+Sanket322@users.noreply.github.com> Date: Sat, 11 Jan 2025 14:50:20 +0530 Subject: [PATCH 21/38] feat: allow filing of Nil return for GSTR-1 (#2906) - closes: #2880 ![image](https://github.com/user-attachments/assets/7f577ef2-e7bf-4a47-804c-2d7143f42c4c) Show a checkbox to file NIL return if there is no data in books. Huly®: IC-3033 --- .../gst_india/api_classes/taxpayer_returns.py | 18 ++--- .../doctype/gst_return_log/generate_gstr_1.py | 23 +++++-- .../doctype/gstr_1_beta/gstr_1_beta.js | 67 ++++++++++++++++--- .../doctype/gstr_1_beta/gstr_1_beta.json | 10 ++- .../doctype/gstr_1_beta/gstr_1_beta.py | 3 + .../gst_india/utils/gstr_1/gstr_1_download.py | 3 +- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/india_compliance/gst_india/api_classes/taxpayer_returns.py b/india_compliance/gst_india/api_classes/taxpayer_returns.py index fb0df061bb..26807a075e 100644 --- a/india_compliance/gst_india/api_classes/taxpayer_returns.py +++ b/india_compliance/gst_india/api_classes/taxpayer_returns.py @@ -37,17 +37,19 @@ def get_return_status(self, return_period, reference_id, otp=None): otp=otp, ) - def proceed_to_file(self, return_type, return_period, otp=None): + def proceed_to_file(self, return_type, return_period, is_nil_return, otp=None): + data = { + "gstin": self.company_gstin, + "ret_period": return_period, + } + + if is_nil_return: + data["isnil"] = "Y" + return self.post( return_type=return_type, return_period=return_period, - json={ - "action": "RETNEWPTF", - "data": { - "gstin": self.company_gstin, - "ret_period": return_period, - }, # "isnil": "N" / "Y" - }, + json={"action": "RETNEWPTF", "data": data}, endpoint="returns/gstrptf", otp=otp, ) diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index 86f5667aae..245d8320fc 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -4,7 +4,7 @@ import frappe from frappe import _, unscrub -from frappe.utils import flt +from frappe.utils import flt, sbool from india_compliance.gst_india.api_classes.taxpayer_returns import GSTR1API from india_compliance.gst_india.utils.gstr_1 import GovJsonKey, GSTR1_SubCategory @@ -832,14 +832,23 @@ def process_upload_gstr1(self): return response - def proceed_to_file_gstr1(self, force): + def proceed_to_file_gstr1(self, is_nil_return, force): verify_request_in_progress(self, force) + is_nil_return = sbool(is_nil_return) + api = GSTR1API(self) - response = api.proceed_to_file("GSTR1", self.return_period) + response = api.proceed_to_file("GSTR1", self.return_period, is_nil_return) # Return Form already ready to be filed - if response.error and response.error.error_cd == "RET00003": + if response.error and response.error.error_cd == "RET00003" or is_nil_return: + set_gstr1_actions( + self, + "proceed_to_file", + response.get("reference_id"), + api.request_id, + status="Processed", + ) return self.fetch_and_compare_summary(api) set_gstr1_actions( @@ -872,13 +881,15 @@ def fetch_and_compare_summary(self, api, response=None): response = {} summary = api.get_gstr_1_data("RETSUM", self.return_period) + self.db_set("is_nil", summary.isnil == "Y") + if summary.error: return self.update_json_for("authenticated_summary", summary) mapped_summary = self.get_json_for("books_summary") - gov_summary = convert_to_internal_data_format(summary).get("summary") + gov_summary = convert_to_internal_data_format(summary).get("summary", {}) gov_summary = summarize_retsum_data(gov_summary.values()) differing_categories = get_differing_categories(mapped_summary, gov_summary) @@ -940,7 +951,7 @@ def file_gstr1(self, pan, otp, force): def get_amendment_data(self): authenticated_summary = convert_to_internal_data_format( self.get_json_for("authenticated_summary") - ).get("summary") + ).get("summary", {}) authenticated_summary = summarize_retsum_data(authenticated_summary.values()) non_amended_entries = { diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 6f1844f9ee..de1e9b6bc1 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -170,8 +170,12 @@ frappe.ui.form.on(DOCTYPE, { const only_books_data = error_log != undefined; if (error_log) { frappe.msgprint({ - message: __("Error while preparing GSTR-1 data, please check {0} for more deatils.", - [`error log`]), + message: __( + "Error while preparing GSTR-1 data, please check {0} for more deatils.", + [ + `error log`, + ] + ), title: "GSTR-1 Download Failed", indicator: "red", }); @@ -195,6 +199,10 @@ frappe.ui.form.on(DOCTYPE, { company_gstin: render_empty_state, + file_nil_gstr1(frm) { + frm.gstr1.render_form_actions(); + }, + month_or_quarter(frm) { render_empty_state(frm); }, @@ -221,6 +229,8 @@ frappe.ui.form.on(DOCTYPE, { const data = frm.doc.__gst_data; if (!data?.status) return; + frm.doc.file_nil_gstr1 = data.is_nil + // Toggle HTML fields frm.refresh(); @@ -300,6 +310,7 @@ class GSTR1 { } this.set_output_gst_balances(); + this.toggle_file_nil_gstr1(); // refresh tabs this.TABS.forEach(_tab => { @@ -356,6 +367,10 @@ class GSTR1 { }); } + refresh_no_data_message() { + this.tabs.filed_tab.tabmanager.refresh_no_data_message(); + } + // RENDER render() { @@ -477,14 +492,20 @@ class GSTR1 { File: this.gstr1_action.file_gstr1_data, }; + // No need to upload if nil gstr1 + const status = + this.frm.doc.file_nil_gstr1 && this.status == "Not Filed" + ? "Uploaded" + : this.status; + let primary_button_label = { "Not Filed": "Upload", Uploaded: "Proceed to File", "Ready to File": "File", - }[this.status] || "Generate"; + }[status] || "Generate"; - if (this.status === "Ready to File") { + if (status === "Ready to File") { this.frm.add_custom_button(__("Mark as Unfiled"), () => { this.gstr1_action.mark_as_unfiled(); }); @@ -687,6 +708,16 @@ class GSTR1 { else this.$wrapper.find(".filter-selector").hide(); } + toggle_file_nil_gstr1() { + if (!this.data || !is_gstr1_api_enabled()) return; + + const has_records = this.data.books_summary?.some(row => row.no_of_records > 0); + + if (!has_records && this.data.status != "Filed") + this.frm.set_df_property("file_nil_gstr1", "hidden", 0); + else this.frm.set_df_property("file_nil_gstr1", "hidden", 1); + } + async set_output_gst_balances() { //Checks if gst-ledger-difference element is there and removes if already present const gst_liability = await get_net_gst_liability(this.frm); @@ -743,7 +774,7 @@ class GSTR1 { args: { month_or_quarter, year, company }, }); - if (!je_details || !je_details.data) return; + if (!je_details) return; this.create_journal_entry_dialog(je_details); } @@ -878,6 +909,10 @@ class TabManager { this.set_creation_time_string(); } + refresh_no_data_message() { + this.datatable.refresh(null, null, this.get_no_data_message()); + } + refresh_view(view, category, filters) { if (!category && view === "Detailed") return; @@ -2114,7 +2149,10 @@ class FiledTab extends GSTR1_TabManager { get_no_data_message() { if (this.instance.data?.is_nil) - return __("You have filed a Nil GSTR-1 for this period"); + if (this.status === "Filed") + return __("You have filed a Nil GSTR-1 for this period"); + else + return __("You are filing a Nil GSTR-1 for this period"); return this.DEFAULT_NO_DATA_MESSAGE; } @@ -2645,11 +2683,18 @@ class GSTR1Action extends FileGSTR1Dialog { proceed_to_file() { const action = "proceed_to_file"; - this.perform_gstr1_action(action, r => { - // already proceed to file - if (r.message) this.handle_proceed_to_file_response(r.message); - else this.check_action_status_with_retry(action); - }); + this.frm.gstr1.data.is_nil = this.frm.doc.file_nil_gstr1; + this.frm.gstr1.refresh_no_data_message(); + + this.perform_gstr1_action( + action, + r => { + // already proceed to file + if (r.message) this.handle_proceed_to_file_response(r.message); + else this.check_action_status_with_retry(action); + }, + { is_nil_return: this.frm.doc.file_nil_gstr1 } + ); } async mark_as_unfiled() { diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json index ee26f09558..3aec4f6c73 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json @@ -7,6 +7,7 @@ "field_order": [ "form", "company", + "file_nil_gstr1", "column_break_ejve", "company_gstin", "column_break_ldkv", @@ -81,13 +82,20 @@ "fieldtype": "Select", "label": "Month/Quarter", "reqd": 1 + }, + { + "default": "0", + "fieldname": "file_nil_gstr1", + "fieldtype": "Check", + "hidden": 1, + "label": "File Nil GSTR-1" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-05-27 19:30:01.074149", + "modified": "2024-12-26 12:43:21.979607", "modified_by": "Administrator", "module": "GST India", "name": "GSTR-1 Beta", diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py index 6c2c8f1e59..32e6e7d101 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py @@ -287,6 +287,9 @@ def get_journal_entries(month_or_quarter, year, company): .run(as_dict=True) ) + if not data: + return + return {"data": data, "posting_date": to_date} diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py index 79563fbc21..307716a794 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -76,8 +76,7 @@ def download_gstr1_json_data(gstr1_log): json_data.update(response) - if json_data.isnil == "Y": - gstr1_log.db_set("is_nil", 1) + gstr1_log.db_set("is_nil", json_data.isnil == "Y") mapped_data = convert_to_internal_data_format(json_data) gstr1_log.update_json_for(data_field, mapped_data, reset_reconcile=True) From 94c11771d332ef04519131605544e76fcfd6a9d6 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 11 Jan 2025 19:06:57 +0530 Subject: [PATCH 22/38] fix: ignore permissions for saving gstin doc --- .../gst_india/doctype/gst_return_log/gst_return_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py index a0f45e861e..44242e0206 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -249,7 +249,7 @@ def _update_gstr_1_filed_upto(filing_date): gstin_doc.gstr_1_filed_upto ): gstin_doc.gstr_1_filed_upto = filing_date - gstin_doc.save() + gstin_doc.save(ignore_permissions=True) # create or update filed logs for key, info in return_info.items(): From 3ef5ae07b293151b4b6576d2a0d8974be7823d53 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 11 Jan 2025 19:09:28 +0530 Subject: [PATCH 23/38] fix: round gst values before upload for accurate comparision with govt summary --- .../gst_india/utils/gstr_1/gstr_1_json_map.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py index b79694b9fa..aa53b96aac 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py @@ -2332,6 +2332,45 @@ def get_invoice_values(self, invoice): GSTR1_DataField.CESS.value: invoice.total_cess_amount, } + def round_values(self, data): + """ + Progressively round off the values in the data + to ensure that the total values match the sum of the individual values + """ + + if isinstance(data[0], list): + for row in data: + self.round_values(row) + + fields = ( + GSTR1_DataField.TAXABLE_VALUE.value, + GSTR1_DataField.IGST.value, + GSTR1_DataField.CGST.value, + GSTR1_DataField.SGST.value, + GSTR1_DataField.CESS.value, + ) + + for field in fields: + if field not in data[0]: + continue + + differece = 0 + last_row_with_value = None + + for row in data: + if not row[field]: # zero values + continue + + rounded = flt(row[field], 2) + differece += row[field] - rounded + + row[field] = flt(rounded, 2) + + last_row_with_value = row + + if flt(differece, 2) != 0: + last_row_with_value[field] += differece + class GSTR1BooksData(BooksDataMapper): def __init__(self, filters): @@ -2372,6 +2411,12 @@ def prepare_mapped_data(self): if data: prepared_data[category] = data + for data in prepared_data.values(): + if not isinstance(data, dict): + continue + + self.round_values(list(data.values())) + return prepared_data def prepare_document_issued_data(self): From a35a098341a4e75e7884f3703c6edb3618491069 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 11 Jan 2025 19:18:38 +0530 Subject: [PATCH 24/38] fix: trim OTP values before sending --- india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js | 2 +- india_compliance/public/js/gst_api_handler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index de1e9b6bc1..9ea8dc4a1a 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -2556,7 +2556,7 @@ class FileGSTR1Dialog { this.perform_gstr1_action( "file", r => this.handle_filing_response(r.message), - { pan: pan, otp: this.filing_dialog.get_value("otp") } + { pan: pan, otp: this.filing_dialog.get_value("otp").trim() } ); this.toggle_actions(true); diff --git a/india_compliance/public/js/gst_api_handler.js b/india_compliance/public/js/gst_api_handler.js index 47a7e2eb9a..54c2f02236 100644 --- a/india_compliance/public/js/gst_api_handler.js +++ b/india_compliance/public/js/gst_api_handler.js @@ -30,7 +30,7 @@ Object.assign(india_compliance, { ], primary_action_label: __("Submit"), primary_action(values) { - resolve(values.otp); + resolve(values.otp.trim()); prompt.hide(); }, secondary_action_label: __("Resend OTP"), From ac77ccef66f0c21ab36f41be68b1f97d762cd86a Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 13 Jan 2025 15:56:22 +0530 Subject: [PATCH 25/38] fix: primary action to reset on change of `is_nil` if already uploaded info --- .../doctype/gst_return_log/generate_gstr_1.py | 4 ++- .../doctype/gstr_1_beta/gstr_1_beta.js | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index 245d8320fc..67bbd380c9 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -728,11 +728,13 @@ def normalize_data(data): class FileGSTR1: - def reset_gstr1(self, force): + + def reset_gstr1(self, is_nil_return, force): verify_request_in_progress(self, force) # reset called after proceed to file self.db_set({"filing_status": "Not Filed"}) + self.db_set({"is_nil": sbool(is_nil_return)}) api = GSTR1API(self) response = api.reset_gstr_1_data(self.return_period) diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js index 9ea8dc4a1a..2867ad727f 100644 --- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js +++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js @@ -229,7 +229,7 @@ frappe.ui.form.on(DOCTYPE, { const data = frm.doc.__gst_data; if (!data?.status) return; - frm.doc.file_nil_gstr1 = data.is_nil + frm.doc.file_nil_gstr1 = data.is_nil; // Toggle HTML fields frm.refresh(); @@ -486,26 +486,32 @@ class GSTR1 { // Primary Button const actions = { + Reset: this.gstr1_action.reset_gstr1_data, Generate: this.gstr1_action.generate_gstr1_data, Upload: this.gstr1_action.upload_gstr1_data, "Proceed to File": this.gstr1_action.proceed_to_file, File: this.gstr1_action.file_gstr1_data, }; - // No need to upload if nil gstr1 - const status = - this.frm.doc.file_nil_gstr1 && this.status == "Not Filed" - ? "Uploaded" - : this.status; - let primary_button_label = { "Not Filed": "Upload", Uploaded: "Proceed to File", "Ready to File": "File", - }[status] || "Generate"; + }[this.status] || "Generate"; + + // No need to upload if nil gstr1 + if (this.frm.doc.__gst_data) { + if (this.frm.doc.file_nil_gstr1 != this.frm.doc.__gst_data.is_nil) + primary_button_label = "Reset"; + + if (this.status == "Not Filed") + if (this.frm.doc.file_nil_gstr1) primary_button_label = "Proceed to File"; + else primary_button_label = "Upload"; + + } - if (status === "Ready to File") { + if (this.status === "Ready to File") { this.frm.add_custom_button(__("Mark as Unfiled"), () => { this.gstr1_action.mark_as_unfiled(); }); @@ -2151,8 +2157,7 @@ class FiledTab extends GSTR1_TabManager { if (this.instance.data?.is_nil) if (this.status === "Filed") return __("You have filed a Nil GSTR-1 for this period"); - else - return __("You are filing a Nil GSTR-1 for this period"); + else return __("You are filing a Nil GSTR-1 for this period"); return this.DEFAULT_NO_DATA_MESSAGE; } @@ -2674,8 +2679,10 @@ class GSTR1Action extends FileGSTR1Dialog { ), () => { frappe.show_alert(__("Resetting GSTR-1 data")); - this.perform_gstr1_action(action, () => - this.check_action_status_with_retry(action) + this.perform_gstr1_action( + action, + () => this.check_action_status_with_retry(action), + { is_nil_return: this.frm.doc.file_nil_gstr1 } ); } ); From ba8a7ae73a18458ca167b530d60c24bdc73e7c6c Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 15 Jan 2025 11:31:59 +0530 Subject: [PATCH 26/38] fix: `ignore_permissions` when handeling missing gstr2a 2b transactions --- india_compliance/gst_india/utils/gstr_2/gstr_2a.py | 4 +++- india_compliance/gst_india/utils/gstr_2/gstr_2b.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/india_compliance/gst_india/utils/gstr_2/gstr_2a.py b/india_compliance/gst_india/utils/gstr_2/gstr_2a.py index c96ffc58ff..e76fd8990c 100644 --- a/india_compliance/gst_india/utils/gstr_2/gstr_2a.py +++ b/india_compliance/gst_india/utils/gstr_2/gstr_2a.py @@ -47,7 +47,9 @@ def handle_missing_transactions(self): if self.existing_transaction: for inward_supply_name in self.existing_transaction.values(): - frappe.delete_doc("GST Inward Supply", inward_supply_name) + frappe.delete_doc( + "GST Inward Supply", inward_supply_name, ignore_permissions=True + ) def get_supplier_details(self, supplier): supplier_details = { diff --git a/india_compliance/gst_india/utils/gstr_2/gstr_2b.py b/india_compliance/gst_india/utils/gstr_2/gstr_2b.py index bc8eeff44e..9d898aa8aa 100644 --- a/india_compliance/gst_india/utils/gstr_2/gstr_2b.py +++ b/india_compliance/gst_india/utils/gstr_2/gstr_2b.py @@ -58,7 +58,9 @@ def handle_missing_transactions(self): ) for transaction_name in unmatched_transactions: - frappe.delete_doc("GST Inward Supply", transaction_name) + frappe.delete_doc( + "GST Inward Supply", transaction_name, ignore_permissions=True + ) def get_transaction(self, category, supplier, invoice): transaction = super().get_transaction(category, supplier, invoice) From 7e8d515993ccd33b9c791abd6a12c6d6f47cb644 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 15 Jan 2025 12:25:21 +0530 Subject: [PATCH 27/38] fix: do not migrate old passwords if decryption fails --- .../migrate_e_invoice_settings_to_gst_settings.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py b/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py index 244fe7f3d8..0b4223ad34 100644 --- a/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py +++ b/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py @@ -1,6 +1,7 @@ import click import frappe +from frappe import _ from frappe.utils import sbool from frappe.utils.password import decrypt @@ -63,7 +64,18 @@ def get_credentials_from_e_invoice_user(): ) for credential in old_credentials: - credential.password = credential.password and decrypt(credential.password) + try: + password = credential.password and decrypt(credential.password) + except Exception as e: + password = None + frappe.log_error( + title=_( + "Failed to decrypt password for E Invoice User {0} - {1}" + ).format(credential.company, credential.gstin), + message=e, + ) + + credential.password = password credential.service = "e-Waybill / e-Invoice" return old_credentials From d59fb57d438dee139aecfb53a5301a9da725ae2e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 15 Jan 2025 14:01:40 +0530 Subject: [PATCH 28/38] fix: enqueue only once for transactions created in bulk --- india_compliance/gst_india/doctype/gstin/gstin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/india_compliance/gst_india/doctype/gstin/gstin.py b/india_compliance/gst_india/doctype/gstin/gstin.py index 283ae0ff09..c9c602ee4e 100644 --- a/india_compliance/gst_india/doctype/gstin/gstin.py +++ b/india_compliance/gst_india/doctype/gstin/gstin.py @@ -107,6 +107,8 @@ def get_and_validate_gstin_status(gstin, transaction_date): validate_gstin_status(doc, transaction_date, throw=True) else: + now = get_datetime() + # Don't delay the response if API is required frappe.enqueue( create_or_update_gstin_status, @@ -115,6 +117,7 @@ def get_and_validate_gstin_status(gstin, transaction_date): gstin=gstin, transaction_date=transaction_date, callback=validate_gstin_status, + job_id=f"create_or_update_gstin_status_{gstin}_{now.date()}_{now.hour}", ) From 4a20e41d0956c7531b27897d87423ae819f8e0f1 Mon Sep 17 00:00:00 2001 From: Ninad Parikh <109862100+Ninad1306@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:25:52 +0530 Subject: [PATCH 29/38] fix: Improved Data Flow in Purchase Reconciliation Tool (#2953) --- .../purchase_reconciliation_tool/__init__.py | 2 +- ...n.html => purchase_detail_comparison.html} | 0 .../purchase_reconciliation_tool.js | 335 +++++++++++------- .../purchase_reconciliation_tool.json | 32 +- .../purchase_reconciliation_tool.py | 15 +- india_compliance/public/js/gst_api_handler.js | 35 +- 6 files changed, 237 insertions(+), 182 deletions(-) rename india_compliance/gst_india/doctype/purchase_reconciliation_tool/{purchase_detail_comparision.html => purchase_detail_comparison.html} (100%) diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py index cb28516ceb..fd8bd3a64a 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py @@ -978,7 +978,7 @@ def get_consolidated_data( def get_manually_matched_data(self, purchase_name: str, inward_supply_name: str): """ Get manually matched data for given purchase invoice and inward supply. - This can be used to show comparision of matched values. + This can be used to show comparison of matched values. """ inward_supplies = self.get_all_inward_supply( names=[inward_supply_name], only_names=True diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparison.html similarity index 100% rename from india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html rename to india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparison.html diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js index 2536225abb..be3295606c 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js @@ -76,92 +76,43 @@ frappe.ui.form.on(DOCTYPE, { }, onload(frm) { - if (frm.doc.is_modified) frm.doc.reconciliation_data = null; add_gstr2b_alert(frm); frm.trigger("purchase_period"); frm.trigger("inward_supply_period"); }, + refresh(frm) { + frm.reco_tool_actions = new PurchaseReconciliationToolAction(frm); + frm.reco_tool_actions.setup_actions(); + }, + async company(frm) { + render_empty_state(frm); if (!frm.doc.company) return; const options = await india_compliance.set_gstin_options(frm, true); if (!frm.doc.company_gstin) frm.set_value("company_gstin", options[0]); }, - refresh(frm) { - // Primary Action - frm.disable_save(); - frm.page.set_primary_action(__("Reconcile"), () => { - if (!frm.doc.company && !frm.doc.company_gstin) { - frappe.throw( - __("Please provide either a Company name or Company GSTIN.") - ); - } - frm.save(); - }); - - const action_group = __("Actions"); - - // add custom buttons - api_enabled - ? frm.add_custom_button(__("Download 2A/2B"), () => new ImportDialog(frm)) - : frm.add_custom_button( - __("Upload 2A/2B"), - () => new ImportDialog(frm, false) - ); - - if (!frm.purchase_reconciliation_tool?.data?.length) return; - if (frm.get_active_tab()?.df.fieldname == "invoice_tab") { - frm.add_custom_button( - __("Unlink"), - () => unlink_documents(frm), - action_group - ); - frm.add_custom_button(__("dropdown-divider"), () => {}, action_group); - } - ["Accept", "Pending", "Ignore"].forEach(action => - frm.add_custom_button( - __(action), - () => apply_action(frm, action), - action_group - ) - ); - frm.$wrapper - .find("[data-label='dropdown-divider']") - .addClass("dropdown-divider"); - - // Export button - frm.add_custom_button(__("Export"), () => - frm.purchase_reconciliation_tool.export_data() + async company_gstin(frm) { + render_empty_state(frm); + await fetch_date_range( + frm, + "inward_supply", + "get_date_range_and_check_missing_documents" ); - - // move actions button next to filters - for (const group_div of $(".custom-actions .inner-group-button")) { - const btn_label = group_div.querySelector("button").innerText?.trim(); - if (btn_label != action_group) continue; - - $(".custom-button-group .inner-group-button").remove(); - - // to hide `Actions` button group on smaller screens - $(group_div).addClass("hidden-md"); - - $(group_div).appendTo($(".custom-button-group")); - } - }, - - before_save(frm) { - frm.doc.__unsaved = true; - frm.doc.reconciliation_data = null; + add_gstr2b_alert(frm); }, async purchase_period(frm) { + render_empty_state(frm); await fetch_date_range(frm, "purchase"); set_date_range_description(frm, "purchase"); }, async inward_supply_period(frm) { + render_empty_state(frm); await fetch_date_range( frm, "inward_supply", @@ -171,26 +122,19 @@ frappe.ui.form.on(DOCTYPE, { add_gstr2b_alert(frm); }, - async company_gstin(frm) { - await fetch_date_range( - frm, - "inward_supply", - "get_date_range_and_check_missing_documents" - ); - add_gstr2b_alert(frm); - }, + gst_return: render_empty_state, - after_save(frm) { - frm.purchase_reconciliation_tool.refresh( - frm.doc.reconciliation_data ? JSON.parse(frm.doc.reconciliation_data) : [] - ); - }, + include_ignored: render_empty_state, show_progress(frm, type) { if (type == "download") { frappe.run_serially([ () => frm.events.update_progress(frm, "update_2a_2b_api_progress"), - () => frm.events.update_progress(frm, "update_2a_2b_transactions_progress"), + () => + frm.events.update_progress( + frm, + "update_2a_2b_transactions_progress" + ), ]); } else if (type == "upload") { frm.events.update_progress(frm, "update_2a_2b_transactions_progress"); @@ -246,19 +190,24 @@ class PurchaseReconciliationTool { this.init(frm); this.render_tab_group(); this.setup_filter_button(); - this.render_data_tables(); } init(frm) { this.frm = frm; - this.data = frm.doc.reconciliation_data - ? JSON.parse(frm.doc.reconciliation_data) - : []; - this.filtered_data = this.data; + this.data = []; this.$wrapper = this.frm.get_field("reconciliation_html").$wrapper; this._tabs = ["invoice", "supplier", "summary"]; } + generate_data() { + this.data = this.frm.__reconciliation_data; + this.filtered_data = this.frm.__reconciliation_data; + + // clear filters + this.filter_group.filter_x_button.click(); + this.render_data_tables(); + } + refresh(data) { if (data) { this.data = data; @@ -455,24 +404,36 @@ class PurchaseReconciliationTool { set_listeners() { const me = this; - this.tabs.invoice_tab.datatable.$datatable.on("click", ".btn.eye", function (e) { - const row = me.mapped_invoice_data[$(this).attr("data-name")]; - me.dm = new DetailViewDialog(me.frm, row); - }); + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".btn.eye", + function (e) { + const row = me.mapped_invoice_data[$(this).attr("data-name")]; + me.dm = new DetailViewDialog(me.frm, row); + } + ); - this.tabs.supplier_tab.datatable.$datatable.on("click", ".btn.download", function (e) { - const row = me.tabs.supplier_tab.datatable.data.find( - r => r.supplier_gstin === $(this).attr("data-name") - ); - me.export_data(row); - }); + this.tabs.supplier_tab.datatable.$datatable.on( + "click", + ".btn.download", + function (e) { + const row = me.tabs.supplier_tab.datatable.data.find( + r => r.supplier_gstin === $(this).attr("data-name") + ); + me.frm.reco_tool_actions.export_data(row); + } + ); - this.tabs.supplier_tab.datatable.$datatable.on("click", ".btn.envelope", function (e) { - const row = me.tabs.supplier_tab.datatable.data.find( - r => r.supplier_gstin === $(this).attr("data-name") - ); - me.dm = new EmailDialog(me.frm, row); - }); + this.tabs.supplier_tab.datatable.$datatable.on( + "click", + ".btn.envelope", + function (e) { + const row = me.tabs.supplier_tab.datatable.data.find( + r => r.supplier_gstin === $(this).attr("data-name") + ); + me.dm = new EmailDialog(me.frm, row); + } + ); const filter_map = { // TAB: { SELECTOR: FIELDNAME } @@ -506,20 +467,6 @@ class PurchaseReconciliationTool { }); } - export_data(selected_row) { - this.data_to_export = this.get_filtered_data(selected_row); - if (selected_row) delete this.data_to_export.supplier_summary; - - const url = - "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.download_excel_report"; - - open_url_post(`/api/method/${url}`, { - data: JSON.stringify(this.data_to_export), - doc: JSON.stringify(this.frm.doc), - is_supplier_specific: !!selected_row, - }); - } - get_filtered_data(selected_row = null) { let supplier_filter = null; @@ -818,6 +765,113 @@ class PurchaseReconciliationTool { } } +class PurchaseReconciliationToolAction { + constructor(frm) { + this.frm = frm; + } + + setup_actions() { + this.setup_document_actions(); + this.setup_row_actions(); + } + + setup_document_actions() { + // Primary Action + this.frm.disable_save(); + this.frm.page.set_primary_action(__("Generate"), async () => { + if (!this.frm.doc.company && !this.frm.doc.company_gstin) { + frappe.throw( + __("Please provide either a Company name or Company GSTIN.") + ); + } + + this.get_reconciliation_data(this.frm); + }); + + // Download Button + api_enabled + ? this.frm.add_custom_button( + __("Download 2A/2B"), + () => new ImportDialog(this.frm) + ) + : this.frm.add_custom_button( + __("Upload 2A/2B"), + () => new ImportDialog(this.frm, false) + ); + + // Export button + this.frm.add_custom_button(__("Export"), () => this.export_data()); + } + + setup_row_actions() { + const action_group = __("Actions"); + + if (!this.frm.purchase_reconciliation_tool?.data?.length) return; + if (this.frm.get_active_tab()?.df.fieldname == "invoice_tab") { + this.frm.add_custom_button( + __("Unlink"), + () => unlink_documents(this.frm), + action_group + ); + this.frm.add_custom_button(__("dropdown-divider"), () => {}, action_group); + } + + // Setup Actions + ["Accept", "Pending", "Ignore"].forEach(action => + this.frm.add_custom_button( + __(action), + () => apply_action(this.frm, action), + action_group + ) + ); + + // Add Dropdown Divider to differentiate between Actions + this.frm.$wrapper + .find("[data-label='dropdown-divider']") + .addClass("dropdown-divider"); + + // move actions button next to filters + for (const group_div of $(".custom-actions .inner-group-button")) { + const btn_label = group_div.querySelector("button").innerText?.trim(); + if (btn_label != action_group) continue; + + $(".custom-button-group .inner-group-button").remove(); + + // to hide `Actions` button group on smaller screens + $(group_div).addClass("hidden-md"); + + $(group_div).appendTo($(".custom-button-group")); + } + } + + async get_reconciliation_data(frm) { + const { message } = await frm._call("reconcile_and_generate_data"); + + frm.__reconciliation_data = message; + + frm.purchase_reconciliation_tool.generate_data(); + frm.doc.data_state = message.length ? "available" : "unavailable"; + + // Toggle HTML fields + frm.refresh(); + } + + export_data(selected_row) { + const data_to_export = + this.frm.purchase_reconciliation_tool.get_filtered_data(selected_row); + if (selected_row) delete data_to_export.supplier_summary; + + const url = + "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.download_excel_report"; + + open_url_post(`/api/method/${url}`, { + data: JSON.stringify(data_to_export), + doc: JSON.stringify(this.frm.doc), + is_supplier_specific: !!selected_row, + }); + } +} + class DetailViewDialog { table_fields = [ "name", @@ -848,7 +902,7 @@ class DetailViewDialog { } async get_invoice_details() { - const { message } = await this.frm.call("get_invoice_details", { + const { message } = await this.frm._call("get_invoice_details", { purchase_name: this.row.purchase_invoice_name, inward_supply_name: this.row.inward_supply_name, }); @@ -977,7 +1031,7 @@ class DetailViewDialog { purchase_doctype: this.data.purchase_doctype, }; - const { message } = await this.frm.call("get_link_options", { + const { message } = await this.frm._call("get_link_options", { doctype: this.dialog.get_value("doctype"), filters: this.filters, }); @@ -1106,7 +1160,7 @@ class DetailViewDialog { const detail_table = this.dialog.fields_dict.detail_table; detail_table.html( - frappe.render_template("purchase_detail_comparision", { + frappe.render_template("purchase_detail_comparison", { purchase: this.data._purchase_invoice, inward_supply: this.data._inward_supply, }) @@ -1277,7 +1331,7 @@ class ImportDialog { if (!this.company_gstin) return; // fetch history - const { message } = await this.frm.call("get_import_history", { + const { message } = await this.frm._call("get_import_history", { company_gstin: this.company_gstin, return_type: this.return_type, date_range: this.date_range, @@ -1294,7 +1348,7 @@ class ImportDialog { ); let download_history = { - columns: ["Period", "Downloded On"], + columns: ["Period", "Downloaded On"], data: message.download_history, }; let html = @@ -1310,7 +1364,7 @@ class ImportDialog { async update_return_period() { const file_path = this.dialog.get_value("attach_file"); - const { message } = await this.frm.call("get_return_period_from_file", { + const { message } = await this.frm._call("get_return_period_from_file", { return_type: this.return_type, file_path, }); @@ -1331,7 +1385,7 @@ class ImportDialog { upload_gstr(period, file_path) { this.frm.events.show_progress(this.frm, "upload"); - this.frm.call("upload_gstr", { + this.frm._call("upload_gstr", { return_type: this.return_type, period, file_path, @@ -1383,13 +1437,14 @@ class ImportDialog { fieldtype: "Select", options: this.frm.get_field("inward_supply_period").df.options, default: this.frm.doc.inward_supply_period, - onchange: () => { + onchange: async () => { const period = this.dialog.get_value("period"); - this.frm.call("get_date_range", { period }).then(({ message }) => { - this.date_range = - message || this.dialog.get_value("date_range"); - this.fetch_import_history(); + const { message } = await this.frm._call("get_date_range", { + period, }); + + this.date_range = message || this.dialog.get_value("date_range"); + this.fetch_import_history(); }, }, { @@ -1570,17 +1625,17 @@ async function fetch_date_range(frm, field_prefix, method) { const period = frm.doc[field_prefix + "_period"]; if (!period || period == "Custom") return; - const { message } = await frm.call(method || "get_date_range", { period }); + const { message } = await frm._call(method || "get_date_range", { period }); frm.set_value(from_date_field, message[0]); frm.set_value(to_date_field, message[1]); } -function set_date_range_description(frm, field_prefixs) { - if (!field_prefixs) field_prefixs = ["inward_supply", "purchase"]; - else field_prefixs = [field_prefixs]; +function set_date_range_description(frm, field_prefixes) { + if (!field_prefixes) field_prefixes = ["inward_supply", "purchase"]; + else field_prefixes = [field_prefixes]; - field_prefixs.forEach(prefix => { + field_prefixes.forEach(prefix => { const period_field = prefix + "_period"; const period = frm.doc[period_field]; @@ -1632,11 +1687,12 @@ purchase_reconciliation_tool.link_documents = async function ( if (frm.get_active_tab()?.df.fieldname != "invoice_tab") return; // link documents & update data. - const { message: r } = await frm.call("link_documents", { + const { message: r } = await frm._call("link_documents", { purchase_invoice_name, inward_supply_name, link_doctype, }); + const reco_tool = frm.purchase_reconciliation_tool; const new_data = reco_tool.data.filter( row => @@ -1674,7 +1730,8 @@ async function unlink_documents(frm, selected_rows) { }); // unlink documents & update table - const { message: r } = await frm.call("unlink_documents", selected_rows); + const { message: r } = await frm.call("unlink_documents", { data: selected_rows }); + const unlinked_docs = get_unlinked_docs(selected_rows); const reco_tool = frm.purchase_reconciliation_tool; @@ -1757,7 +1814,8 @@ function apply_action(frm, action, selected_rows) { } // update affected rows to backend and frontend - frm.call("apply_action", { data: affected_rows, action }); + frm._call("apply_action", { data: affected_rows, action }); + const new_data = data.filter(row => { if (has_matching_row(row, affected_rows)) row.action = action; return true; @@ -1800,9 +1858,7 @@ async function create_new_purchase_invoice(row, company, company_gstin) { const { message: supplier } = await frappe.call({ method: "india_compliance.gst_india.utils.get_party_for_gstin", - args: { - gstin: row.supplier_gstin, - }, + args: { gstin: row.supplier_gstin }, }); let company_address; @@ -1854,3 +1910,10 @@ async function create_new_purchase_invoice(row, company, company_gstin) { frappe.new_doc("Purchase Invoice"); } + +function render_empty_state(frm) { + frm.__reconciliation_data = null; + frm.doc.data_state = null; + + frm.refresh(); +} diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.json b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.json index 59ca56ed3d..83b4c270a8 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.json +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.json @@ -22,10 +22,7 @@ "section_break_11", "reconciliation_html", "not_reconciled", - "no_reconciliation_data", - "is_modified", - "section_break_cmfa", - "reconciliation_data" + "no_reconciliation_data" ], "fields": [ { @@ -98,12 +95,12 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: doc.reconciliation_data?.length", + "depends_on": "eval: doc.data_state === \"available\"", "fieldname": "reconciliation_html", "fieldtype": "HTML" }, { - "depends_on": "eval: doc.reconciliation_data && !doc.reconciliation_data.length", + "depends_on": "eval: doc.data_state === \"unavailable\"", "fieldname": "no_reconciliation_data", "fieldtype": "HTML", "options": "\"No\n\t

{{ __(\"No data available for selected filters\") }}

" @@ -119,27 +116,10 @@ "options": "GSTR 2B\nBoth GSTR 2A & 2B" }, { - "depends_on": "eval: !doc.reconciliation_data", + "depends_on": "eval: !doc.data_state", "fieldname": "not_reconciled", "fieldtype": "HTML", - "options": "\"No\n\t

{{ __(\"Reconcile to view the data\") }}

" - }, - { - "fieldname": "section_break_cmfa", - "fieldtype": "Section Break", - "hidden": 1 - }, - { - "fieldname": "reconciliation_data", - "fieldtype": "JSON", - "label": "Reconciliation Data" - }, - { - "default": "0", - "fieldname": "is_modified", - "fieldtype": "Check", - "hidden": 1, - "label": "Is Modified" + "options": "\"No\n\t

{{ __(\"Generate to view the data\") }}

" }, { "default": "0", @@ -152,7 +132,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-08-09 17:57:55.801686", + "modified": "2025-01-16 11:43:52.167256", "modified_by": "Administrator", "module": "GST India", "name": "Purchase Reconciliation Tool", diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py index 68505eb0d1..2d341ae764 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -1,6 +1,5 @@ # Copyright (c) 2022, Resilient Tech and contributors # For license information, please see license.txt -import json import re from collections import defaultdict from typing import List @@ -10,7 +9,6 @@ from frappe.model.document import Document from frappe.query_builder.functions import IfNull from frappe.utils import add_to_date, cint, now_datetime -from frappe.utils.response import json_handler from india_compliance.gst_india.api_classes.taxpayer_base import ( TaxpayerBaseAPI, @@ -79,7 +77,8 @@ def onload(self): ), ) - def validate(self): + @frappe.whitelist() + def reconcile_and_generate_data(self): # reconcile purchases and inward supplies if frappe.flags.in_install or frappe.flags.in_migrate: return @@ -89,11 +88,8 @@ def validate(self): _Reconciler.reconcile(row["original"], row["amended"]) self.ReconciledData = ReconciledData(**self.get_reco_doc()) - self.reconciliation_data = json.dumps( - self.ReconciledData.get(), default=json_handler - ) - self.db_set("is_modified", 0) + return self.ReconciledData.get() @frappe.whitelist() def upload_gstr(self, return_type, period, file_path): @@ -270,7 +266,6 @@ def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype purchases.append(purchase_invoice_name) inward_supplies.append(inward_supply_name) - self.db_set("is_modified", 1) self.set_reconciliation_status( link_doctype, (purchase_invoice_name,), "Match Found" ) @@ -300,8 +295,6 @@ def unlink_documents(self, data): self.set_reconciliation_status("Bill of Entry", boe, "Unreconciled") self._unlink_documents(inward_supplies) - self.db_set("is_modified", 1) - return self.ReconciledData.get(purchases.union(boe), inward_supplies) def set_reconciliation_status(self, doctype, names, status): @@ -371,8 +364,6 @@ def apply_action(self, data, action): self.set_reconciliation_status("Purchase Invoice", purchases, status) self.set_reconciliation_status("Bill of Entry", boe, status) - self.db_set("is_modified", 1) - @frappe.whitelist() def get_link_options(self, doctype, filters): frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) diff --git a/india_compliance/public/js/gst_api_handler.js b/india_compliance/public/js/gst_api_handler.js index 54c2f02236..2f28be1ca5 100644 --- a/india_compliance/public/js/gst_api_handler.js +++ b/india_compliance/public/js/gst_api_handler.js @@ -4,11 +4,12 @@ frappe.provide("india_compliance"); taxpayer_api.call = async function (...args) { const response = await frappe.call(...args); const { message } = response; - if (!["otp_requested", "invalid_otp"].includes(message?.error_type)) return response; + if (!["otp_requested", "invalid_otp"].includes(message?.error_type)) + return response; await india_compliance.authenticate_otp(message.gstin, message.error_type); return taxpayer_api.call(...args); -} +}; Object.assign(india_compliance, { get_gstin_otp(company_gstin, error_type) { @@ -97,17 +98,37 @@ class IndiaComplianceForm extends frappe.ui.form.Form { method: method, doc: this.doc, args: args, - callback: callback - } + callback: callback, + }; opts.original_callback = opts.callback; - opts.callback = (r) => { + opts.callback = r => { if (!r.exc) this.refresh_fields(); opts.original_callback && opts.original_callback(r); - } + }; return taxpayer_api.call(opts); } + + _call(method, args, callback) { + const data_state = this.doc.data_state; + + // similar to frappe.ui.form.Form.prototype.call + const opts = { + method: method, + doc: this.doc, + args: args, + callback: callback, + }; + + opts.original_callback = opts.callback; + opts.callback = r => { + if (!r.exc) this.doc.data_state = data_state; + opts.original_callback && opts.original_callback(r); + }; + + return frappe.call(opts); + } } -frappe.ui.form.Form = IndiaComplianceForm; \ No newline at end of file +frappe.ui.form.Form = IndiaComplianceForm; From 3f1153118fa17d1dd0cf35d625540dd9585bfeb6 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Thu, 16 Jan 2025 18:56:33 +0530 Subject: [PATCH 30/38] fix: make e-waybill-detailed print format --- .../e_waybill_detailed/__init__.py | 0 .../e_waybill_detailed/e_waybill_detailed.css | 125 +++++++ .../e_waybill_detailed.html | 317 ++++++++++++++++++ .../e_waybill_detailed.json | 30 ++ 4 files changed, 472 insertions(+) create mode 100644 india_compliance/gst_india/print_format/e_waybill_detailed/__init__.py create mode 100644 india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css create mode 100644 india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html create mode 100644 india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/__init__.py b/india_compliance/gst_india/print_format/e_waybill_detailed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css new file mode 100644 index 0000000000..3bdfccda60 --- /dev/null +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css @@ -0,0 +1,125 @@ +.align-middle{ + vertical-align: middle; +} + +.print-format { + padding: 0.25in; + color: black; +} + +.qr-code{ + +} + +.attribute{ + +} + +.barcode{ + +} + +.no-border{ + border: none !important; +} + +.no-border > *{ + border: none; +} + +.attribute-value{ + font-weight: bold; +} + +.bold{ + font-weight: bold; +} + +.table-element{ + font-size: 10px; +} + +.table-element > .table-heading{ + font-size: 13px; + font-weight: bold; +} + +.table-heading{ + font-size: 13px; + font-weight: bold; +} + +thead > th{ + background-color: white !important; +} + +table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + +@media print { + .print-format { + padding: 0; + margin-top: 10mm; + margin-bottom: 10mm; + } +} + +.print-format tbody>tr>td, +.print-format tbody>tr>th { + padding: 6px !important; + border-top: none; + color: black; +} + +.print-format h1.title { + font-size: 18px; + font-weight: bold; + margin: 0; +} + +.print-format .e-waybill-number { + font-size: 16px; +} + +.print-format .page-layout { + font-size: 12px; + line-height: 1.4286; + box-sizing: border-box; + transform-origin: top center; + width: 95%; + margin: 0 auto; +} + +.print-format .vehicle-info { + margin: 0 !important; + font-size: 9px; + border-collapse: collapse; +} + +.print-format .vehicle-info th { + font-weight: bold; +} + +.print-format .vehicle-info td, +.print-format .vehicle-info th { + border: 0 !important; + border-top: 1px solid black !important; +} + +.print-format .ewb-no-span { + font-size: 13px; +} + +.print-format .section-separator { + border-top: 2px solid #bbb; + margin: 8px 0 16px; +} + +.print-format table td img.barcode, +.print-format table td img.qr-code { + margin-top: 5px; + width: 95px !important; + image-rendering: -webkit-optimize-contrast; +} \ No newline at end of file diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html new file mode 100644 index 0000000000..10c57dd548 --- /dev/null +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html @@ -0,0 +1,317 @@ + + +{% set data = _dict(json.loads(doc.data)) %} +{%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} +{%- set irn = data.irn -%} {#temp#} + +{% if data.supplyType == "O" %} + {% set generated_by = data.fromTrdName %} +{% else %} + {% set generated_by = data.toTrdName %} +{% endif %} + + + +
+ + + + + + + +
+

e-Waybill

+
+ +
+ +
E-Way Bill Details
+ + + + + + + + + + + + + + + + + + + + + +
+ eWay Bill No: + 2818 6444 2513 + + Generated Date: + 26/11/2024 12:00 PM + + Generated By: + 27AAG CP577 4K1ZT +
+ Mode: + Road + + Approx Distance: + 20km + + Valid Upto: + 27/11/2024 +
+ Type: + Outward - Export + + Document Details: + {{data.DocDtls.Typ}} - {{data.DocDtls.No}} - {{data.DocDtls.Dt}} + + Transaction type: + Regular +
+ Irn: + {{data.Irn}} +
+ +
Address Details
+ + + + + + + + +
+ {% set from_state = get_state(data.ShipDtls.Stcd) %} + +
From
+
+
+
GSTIN: {{ data.ShipDtls.Gstin }}
+
{{data.ShipDtls.LglNm}}
+
{{ from_state }}
{# get_state_by_state_code #} +
+
+ :: Dispatch From :: +
{{data.ShipDtls.Addr1}}
+
{{data.ShipDtls.Addr2}}
+
{{data.ShipDtls.Loc}}, {{from_state}}-{{ data.ShipDtls.Pin }}
+
+
+
+ {% set to_state = get_state(data.SellerDtls.Stcd) %} +
To
+
+
+
GSTIN: {{ data.SellerDtls.Gstin }}
+
{{data.SellerDtls.LglNm}}
+
{{ to_state }}
{# get_state_by_state_code #} +
+
+ :: Ship To :: +
{{data.SellerDtls.Addr1}}
+
{{data.SellerDtls.Addr2}}
+
{{data.SellerDtls.Loc}}, {{to_state}}-{{ data.SellerDtls.Pin }}
+
+
+
+ +
Goods Details
+ + + + + + + + + + + {% for item in data.ItemList %} + + + + + + + + + {% endfor %} + +
+ HSN Code + + Product Name & Desc. + + Quantity + + Taxable Amount Rs. + + Tax Rate (C + S + I + Cess + Cess Non. Advol) +
+ {{item.HsnCd}} + + {{item.PrdDesc}} + + {{item.Qty}} {{item.Unit}} + + temp value {# taxable amt. #} + + temp value {# tax rate #} +
+ + + + + + + + + + + + + + + + + + +
+ Tot. Taxable Amt. + + CGST Amt. + + SGST Amt. + + IGST Amt. + + CESS Amt. + + CESS Non. Advol Amt. + + Other Amt. + + Total Inv. Amt. +
+ +
Transportation Details
+ + + + + + + + +
+ Transporter ID & Name: + {{data.EwbDtls.TransName}} + + Transporter Doc. No & Date: + where?? +
+ +
Vehicle Details
+ + + + + + + + + + + + + + + + + + + + + + + +
+ Mode + + Vehicle / Trans Doc. No. & Dt. + + From + + Entered Date + + Entered By + + CEWB No. (If any) + + Multi Veh. Info (If any) +
+ where?? + + {{data.EwbDtls.VehNo}} maybe? + + {{from_state}} maybe? + + where?? + + where?? + + where?? + + where?? +
+ + + + + + + +
+
+ +
+
+
+ + + + + + + + + {#
+ +

e-Waybill

+
+ + + +
+ #} + +
+ + diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json new file mode 100644 index 0000000000..d028983b81 --- /dev/null +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json @@ -0,0 +1,30 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2025-01-16 11:59:28.744326", + "custom_format": 0, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Print-E waybill", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "idx": 0, + "line_breaks": 0, + "margin_bottom": 0.0, + "margin_left": 0.0, + "margin_right": 0.0, + "margin_top": 0.0, + "modified": "2025-01-16 12:01:14.556660", + "modified_by": "admin@example.com", + "module": "GST India", + "name": "e-Waybill Detailed", + "owner": "admin@example.com", + "page_number": "Hide", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file From ebeb4658955d2334667eacc68135f7f46222c0b0 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 20 Jan 2025 12:49:19 +0530 Subject: [PATCH 31/38] fix: update fieldnames --- .../e_waybill_detailed/e_waybill_detailed.css | 7 + .../e_waybill_detailed.html | 186 ++++++++++++------ 2 files changed, 137 insertions(+), 56 deletions(-) diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css index 3bdfccda60..889990203f 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css @@ -58,6 +58,13 @@ table, th, td { border-collapse: collapse; } +.table-element .border div{ + border: 1px solid black !important; + width: 100%; + padding: 5px; + +} + @media print { .print-format { padding: 0; diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html index 10c57dd548..5f2eb5fbd9 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html @@ -37,49 +37,49 @@

e-Waybill

eWay Bill No: - 2818 6444 2513 + {{ add_spacing(data.ewbNo,4) }} Generated Date: - 26/11/2024 12:00 PM + {{ data.ewayBillDate }} Generated By: - 27AAG CP577 4K1ZT + {{ add_spacing(data.userGstin,5) }} Mode: - Road + where?? Approx Distance: - 20km + {{ data.actualDist }}kms Valid Upto: - 27/11/2024 + {{ data.validUpto }} Type: - Outward - Export + {{ get_supply_type(data.supplyType) }} - {{ get_sub_supply_type(data.subSupplyType) }} Document Details: - {{data.DocDtls.Typ}} - {{data.DocDtls.No}} - {{data.DocDtls.Dt}} + {{ data.docType }} - {{ data.docNo }} - {{ data.docDate }} Transaction type: - Regular + {{ get_transport_type(data.transactionType) }} Irn: - {{data.Irn}} + {{data.irn}} @@ -90,37 +90,37 @@

e-Waybill

- {% set from_state = get_state(data.ShipDtls.Stcd) %} + {% set from_state = get_state(data.fromStateCode) %}
From
-
GSTIN: {{ data.ShipDtls.Gstin }}
-
{{data.ShipDtls.LglNm}}
+
GSTIN: {{ add_spacing(data.fromGstin,5) }}
+
{{data.fromTrdName}}
{{ from_state }}
{# get_state_by_state_code #}
:: Dispatch From :: -
{{data.ShipDtls.Addr1}}
-
{{data.ShipDtls.Addr2}}
-
{{data.ShipDtls.Loc}}, {{from_state}}-{{ data.ShipDtls.Pin }}
+
{{ data.fromAddr1 }}
+
{{ data.fromAddr2 }}
+
{{ data.fromPlace }}, {{ from_state }}-{{ data.fromPincode }}
- {% set to_state = get_state(data.SellerDtls.Stcd) %} + {% set to_state = get_state(data.toStateCode) %}
To
-
GSTIN: {{ data.SellerDtls.Gstin }}
-
{{data.SellerDtls.LglNm}}
+
GSTIN: {{ add_spacing(data.toGstin,5) }}
+
{{data.toTrdName}}
{{ to_state }}
{# get_state_by_state_code #}
- :: Ship To :: -
{{data.SellerDtls.Addr1}}
-
{{data.SellerDtls.Addr2}}
-
{{data.SellerDtls.Loc}}, {{to_state}}-{{ data.SellerDtls.Pin }}
+ :: Ship to :: +
{{ data.toAddr1 }}
+
{{ data.toAddr2 }}
+
{{ data.toPlace }}, {{ to_state }}-{{ data.toPincode }}
@@ -150,22 +150,61 @@

e-Waybill

Tax Rate (C + S + I + Cess + Cess Non. Advol) - {% for item in data.ItemList %} + {% for item in data.itemList %} - {{item.HsnCd}} + {{item.hsnCode}} - {{item.PrdDesc}} + {{item.productDesc}} - {{item.Qty}} {{item.Unit}} + {{item.qtyUnit}} {{item.qtyUnit}} - temp value {# taxable amt. #} + {{ item.taxableAmount }} {# taxable amt. #} - temp value {# tax rate #} + + {% if item.cgstRate is defined %} + {{ item.cgstRate }} + {% else %} + NE + {% endif %} + + + + + {% if item.sgstRate is defined %} + {{ item.sgstRate }} + {% else %} + NE + {% endif %} + + + + + {% if item.igstRate is defined %} + {{ item.igstRate }} + {% else %} + NE + {% endif %} + + + + + {% if item.cessRate is defined %} + {{ item.cessRate }} + {% else %} + NE + {% endif %} + + + + + {% if item.cessNonAdvol is defined %} + {{ item.cessNonAdvol }} + {% else %} + NE + {% endif %} + + @@ -201,8 +240,31 @@

e-Waybill

Total Inv. Amt. - - + + +
{{ data.totalValue }}
+ + +
{{ data.cgstValue }}
+ + +
{{ data.sgstValue }}
+ + +
{{ data.igstValue }}
+ + +
{{ data.cessValue }}
+ + +
{{ data.cessNonAdvolValue }}
+ + +
{{ data.otherValue }}
+ + +
{{ data.totInvValue }}
+ @@ -213,11 +275,11 @@

e-Waybill

- - @@ -253,28 +315,40 @@

e-Waybill

- - - - - - - - + {% for item in data.VehiclListDetails %} + + + + + + + + + + + {% endfor %}
+ Transporter ID & Name: - {{data.EwbDtls.TransName}} + {{ data.transporterId }} & {{ data.transporterName }} + Transporter Doc. No & Date: where??
- where?? - - {{data.EwbDtls.VehNo}} maybe? - - {{from_state}} maybe? - - where?? - - where?? - - where?? - - where?? -
+ {{ get_transport_mode(item.transMode) }} + + {{ item.vehicleNo }} + + {{ item.fromPlace }} + + {{ item.enteredDate }} + + {{ item.userGSTINTransin }} + + {% if item.cewbNo is defined %} + {{ item.cewbNo }} + {% else %} + - + {% endif %} + + {% if item.multiVehInfo is defined %} + {{ item.multiVehInfo }} + {% else %} + - + {% endif %} +
From 7f7b4f8da86be4ca8c1e28ac5d64d24d2c55e348 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 20 Jan 2025 16:39:07 +0530 Subject: [PATCH 32/38] fix: add more fields --- .../e_waybill_detailed.html | 103 +++++++----------- india_compliance/gst_india/utils/jinja.py | 7 ++ india_compliance/hooks.py | 1 + 3 files changed, 46 insertions(+), 65 deletions(-) diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html index 5f2eb5fbd9..d5663197d7 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html @@ -3,7 +3,7 @@ {% set data = _dict(json.loads(doc.data)) %} -{%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} +{%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} {%- set irn = data.irn -%} {#temp#} {% if data.supplyType == "O" %} @@ -23,13 +23,13 @@

e-Waybill

- +
E-Way Bill Details
@@ -50,8 +50,9 @@

e-Waybill

+ {% set lastVehicleDetails = data.VehiclListDetails | last %} Mode: - where?? + {{ get_transport_mode(lastVehicleDetails.transMode) }} Approx Distance: @@ -69,7 +70,7 @@

e-Waybill

Document Details: - {{ data.docType }} - {{ data.docNo }} - {{ data.docDate }} + {{ get_e_waybill_document_type(data.docType) }} - {{ data.docNo }} - {{ data.docDate }} Transaction type: @@ -84,17 +85,17 @@

e-Waybill

- +
Address Details
- - + +
{% set from_state = get_state(data.fromStateCode) %} - +
From
-
+
GSTIN: {{ add_spacing(data.fromGstin,5) }}
{{data.fromTrdName}}
{{ from_state }}
{# get_state_by_state_code #} @@ -111,7 +112,7 @@

e-Waybill

{% set to_state = get_state(data.toStateCode) %}
To
-
+
GSTIN: {{ add_spacing(data.toGstin,5) }}
{{data.toTrdName}}
{{ to_state }}
{# get_state_by_state_code #} @@ -124,8 +125,8 @@

e-Waybill

@@ -135,7 +136,7 @@

e-Waybill

- HSN Code + HSN Code Product Name & Desc. @@ -149,7 +150,7 @@

e-Waybill

Tax Rate (C + S + I + Cess + Cess Non. Advol) - + {% for item in data.itemList %} @@ -159,56 +160,36 @@

e-Waybill

{{item.productDesc}} - {{item.qtyUnit}} {{item.qtyUnit}} + {{item.quantity}} {{item.qtyUnit}} {{ item.taxableAmount }} {# taxable amt. #} - {% if item.cgstRate is defined %} - {{ item.cgstRate }} - {% else %} - NE - {% endif %} + {{ item.cgstRate | default('NE') }} + - {% if item.sgstRate is defined %} - {{ item.sgstRate }} - {% else %} - NE - {% endif %} + {{ item.sgstRate | default('NE') }} + - {% if item.igstRate is defined %} - {{ item.igstRate }} - {% else %} - NE - {% endif %} + {{ item.igstRate | default('NE') }} + - {% if item.cessRate is defined %} - {{ item.cessRate }} - {% else %} - NE - {% endif %} + {{ item.cessRate | default('NE') }} + - {% if item.cessNonAdvol is defined %} - {{ item.cessNonAdvol }} - {% else %} - NE - {% endif %} + {{ item.cessNonAdvol | default('NE') }} - + - - - {% endfor %} + + + {% endfor %} @@ -239,7 +220,7 @@

e-Waybill

Total Inv. Amt. - +
{{ data.totalValue }}
@@ -265,8 +246,8 @@

e-Waybill

{{ data.totInvValue }}
- - + + @@ -281,9 +262,9 @@

e-Waybill

Transporter Doc. No & Date: - where?? + {{ data.transDocNo|default('') }} - {{ data.transDocDate|default('') }} - + @@ -313,7 +294,7 @@

e-Waybill

Multi Veh. Info (If any) - + {% for item in data.VehiclListDetails %} @@ -333,21 +314,13 @@

e-Waybill

{{ item.userGSTINTransin }} - {% if item.cewbNo is defined %} - {{ item.cewbNo }} - {% else %} - - - {% endif %} + {{ item.cebwNo | default('-') }} - {% if item.multiVehInfo is defined %} - {{ item.multiVehInfo }} - {% else %} - - - {% endif %} + {{ item.multiVehInfo | default('-') }} - - + + {% endfor %} @@ -366,13 +339,13 @@

e-Waybill

- - - + + + {#

e-Waybill

@@ -383,7 +356,7 @@

e-Waybill

class="qr-code" />
-
+ #} diff --git a/india_compliance/gst_india/utils/jinja.py b/india_compliance/gst_india/utils/jinja.py index 592e602b1c..5658165e53 100644 --- a/india_compliance/gst_india/utils/jinja.py +++ b/india_compliance/gst_india/utils/jinja.py @@ -12,6 +12,7 @@ from frappe.utils import flt from india_compliance.gst_india.constants.e_waybill import ( + DOCUMENT_TYPES, SUB_SUPPLY_TYPES, SUPPLY_TYPES, TRANSPORT_MODES, @@ -81,6 +82,12 @@ def get_transport_type(code): return TRANSPORT_TYPES[int(code)] +def get_e_waybill_document_type(short_document_type): + for full_document_type, document_type in DOCUMENT_TYPES.items(): + if short_document_type == document_type: + return full_document_type + + def get_e_waybill_qr_code(e_waybill, gstin, ewaybill_date): e_waybill_date = as_ist(ewaybill_date) qr_text = "/".join( diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 96a87b35cf..ffbcfe9134 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -379,6 +379,7 @@ "india_compliance.gst_india.utils.jinja.get_qr_code", "india_compliance.gst_india.utils.jinja.get_transport_type", "india_compliance.gst_india.utils.jinja.get_transport_mode", + "india_compliance.gst_india.utils.jinja.get_e_waybill_document_type", "india_compliance.gst_india.utils.jinja.get_ewaybill_barcode", "india_compliance.gst_india.utils.jinja.get_e_invoice_item_fields", "india_compliance.gst_india.utils.jinja.get_e_invoice_amount_fields", From 9c12c22d53f75f9e9cc248b30ce715c13659e5e7 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Mon, 20 Jan 2025 16:53:09 +0530 Subject: [PATCH 33/38] fix: set irn from data json removed --- .../print_format/e_waybill_detailed/e_waybill_detailed.html | 1 - 1 file changed, 1 deletion(-) diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html index d5663197d7..c9e3b29614 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html @@ -4,7 +4,6 @@ {% set data = _dict(json.loads(doc.data)) %} {%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} -{%- set irn = data.irn -%} {#temp#} {% if data.supplyType == "O" %} {% set generated_by = data.fromTrdName %} From f8d4c06bf88e83c9df975f8903accdbaada65979 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 21 Jan 2025 11:16:33 +0530 Subject: [PATCH 34/38] fix: update address mapping; variable names; conditional IRN being set --- .../e_waybill_detailed/e_waybill_detailed.css | 45 ++++++--------- .../e_waybill_detailed.html | 56 ++++++------------- .../e_waybill_detailed.json | 8 +-- 3 files changed, 38 insertions(+), 71 deletions(-) diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css index 889990203f..1bca28f792 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css @@ -1,4 +1,4 @@ -.align-middle{ +.align-middle { vertical-align: middle; } @@ -7,62 +7,51 @@ color: black; } -.qr-code{ - -} - -.attribute{ - -} - -.barcode{ - -} - -.no-border{ +.no-border { border: none !important; } -.no-border > *{ +.no-border > * { border: none; } - -.attribute-value{ + +.attribute-value { font-weight: bold; } -.bold{ +.bold { font-weight: bold; } -.table-element{ +.table-element { font-size: 10px; } -.table-element > .table-heading{ +.table-element > .table-heading { font-size: 13px; font-weight: bold; } -.table-heading{ +.table-heading { font-size: 13px; font-weight: bold; } -thead > th{ +thead > th { background-color: white !important; } -table, th, td { +table, +th, +td { border: 1px solid black; border-collapse: collapse; } -.table-element .border div{ +.table-element .border div { border: 1px solid black !important; width: 100%; padding: 5px; - } @media print { @@ -73,8 +62,8 @@ table, th, td { } } -.print-format tbody>tr>td, -.print-format tbody>tr>th { +.print-format tbody > tr > td, +.print-format tbody > tr > th { padding: 6px !important; border-top: none; color: black; @@ -129,4 +118,4 @@ table, th, td { margin-top: 5px; width: 95px !important; image-rendering: -webkit-optimize-contrast; -} \ No newline at end of file +} diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html index c9e3b29614..54e890305d 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.html @@ -76,12 +76,14 @@

e-Waybill

{{ get_transport_type(data.transactionType) }} + {% if irn %} Irn: - {{data.irn}} + {{irn}} + {% endif %} @@ -90,37 +92,34 @@

e-Waybill

- {% set from_state = get_state(data.fromStateCode) %} -
From
GSTIN: {{ add_spacing(data.fromGstin,5) }}
{{data.fromTrdName}}
-
{{ from_state }}
{# get_state_by_state_code #} +
{{ get_state(data.fromStateCode) }}
{# get_state_by_state_code #}
:: Dispatch From ::
{{ data.fromAddr1 }}
{{ data.fromAddr2 }}
-
{{ data.fromPlace }}, {{ from_state }}-{{ data.fromPincode }}
+
{{ data.fromPlace }}, {{ get_state(data.actFromStateCode) }}-{{ data.fromPincode }}
- {% set to_state = get_state(data.toStateCode) %}
To
GSTIN: {{ add_spacing(data.toGstin,5) }}
{{data.toTrdName}}
-
{{ to_state }}
{# get_state_by_state_code #} +
{{ get_state(data.toStateCode) }}
{# get_state_by_state_code #}
:: Ship to ::
{{ data.toAddr1 }}
{{ data.toAddr2 }}
-
{{ data.toPlace }}, {{ to_state }}-{{ data.toPincode }}
+
{{ data.toPlace }}, {{ get_state(data.actToStateCode) }}-{{ data.toPincode }}
@@ -257,11 +256,11 @@

e-Waybill

Transporter ID & Name: - {{ data.transporterId }} & {{ data.transporterName }} + {{ add_spacing(data.transporterId, 5) or "None" }} & {{ data.transporterName or "None" }} Transporter Doc. No & Date: - {{ data.transDocNo|default('') }} - {{ data.transDocDate|default('') }} + {{ data.transDocNo or "None" }} & {{ data.transDocDate or "None" }} @@ -295,28 +294,28 @@

e-Waybill

- {% for item in data.VehiclListDetails %} + {% for detail in data.VehiclListDetails %} - {{ get_transport_mode(item.transMode) }} + {{ get_transport_mode(detail.transMode) }} - {{ item.vehicleNo }} + {{ detail.vehicleNo }} - {{ item.fromPlace }} + {{ detail.fromPlace }} - {{ item.enteredDate }} + {{ detail.enteredDate }} - {{ item.userGSTINTransin }} + {{ detail.userGSTINTransin }} - {{ item.cebwNo | default('-') }} + {{ detail.cebwNo | default('-') }} - {{ item.multiVehInfo | default('-') }} + {{ detail.multiVehInfo | default('-') }} @@ -337,27 +336,6 @@

e-Waybill

- - - - - - - - - {#
- -

e-Waybill

-
- - - -
- #} - diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json index d028983b81..971df3175a 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.json @@ -5,7 +5,7 @@ "custom_format": 0, "default_print_language": "en", "disabled": 0, - "doc_type": "Print-E waybill", + "doc_type": "e-Waybill Log", "docstatus": 0, "doctype": "Print Format", "font_size": 14, @@ -15,11 +15,11 @@ "margin_left": 0.0, "margin_right": 0.0, "margin_top": 0.0, - "modified": "2025-01-16 12:01:14.556660", - "modified_by": "admin@example.com", + "modified": "2025-01-21 10:42:52.756385", + "modified_by": "Administrator", "module": "GST India", "name": "e-Waybill Detailed", - "owner": "admin@example.com", + "owner": "Administrator", "page_number": "Hide", "print_format_builder": 0, "print_format_builder_beta": 0, From caebd7d5c74003ee513ce52a7983c122da23fa73 Mon Sep 17 00:00:00 2001 From: Karm Soni Date: Tue, 21 Jan 2025 13:26:58 +0530 Subject: [PATCH 35/38] fix: update UI of e-Waybill print-format --- .../print_format/e_waybill/e_waybill.css | 22 ++ .../print_format/e_waybill/e_waybill.html | 252 +++++++++--------- 2 files changed, 143 insertions(+), 131 deletions(-) diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css index 3bf9be7803..665559e849 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css @@ -1,8 +1,30 @@ + +.table-element { + font-size: 12px; +} + +.bold { + font-weight: bold; +} + +.no-border { + border: none !important; +} + +.no-border > * { + border: none; +} + .print-format { padding: 0.25in; color: black; } +table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + @media print { .print-format { padding: 0; diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html index 78a3abf4da..10f529d652 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html @@ -6,38 +6,38 @@ {%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} {% if data.supplyType == "O" %} - {% set generated_by = data.fromTrdName %} +{% set generated_by = data.fromTrdName %} {% else %} - {% set generated_by = data.toTrdName %} +{% set generated_by = data.toTrdName %} {% endif %} - -
- - - - - - - - - -
-

e-Waybill

-
- -
+ +
+ + + + + + + + + + +
+

e-Waybill

+
+ +
- - +
+
+ {% endif %} - - - + +
- e-Waybill No: + e-Waybill No @@ -47,17 +47,17 @@

e-Waybill

- e-Waybill Date: + e-Waybill Date - {{ data.ewayBillDate | replace(":00 ", "") }} + {{ data.ewayBillDate | replace("00 ", "") }}
- Generated By: + Generated By @@ -69,18 +69,17 @@

e-Waybill

- Valid From: + Valid From - {{ data.ewayBillDate | replace(":00 ", " ") }} - {{ " [" }}{{ data.actualDist }}{{ "Kms]" }} + {{ data.ewayBillDate | replace(":00 ", " ") }} {{ " [" }} {{ data.actualDist }}{{ "Kms]" }}
- Valid Until: + Valid Until @@ -91,7 +90,7 @@

e-Waybill

{% if irn %}
- IRN: + IRN @@ -100,17 +99,21 @@

e-Waybill

-
- Part - A -
+ + +
+
+ Part - A +
+ + @@ -118,9 +121,7 @@

e-Waybill

@@ -128,45 +129,41 @@

e-Waybill

@@ -174,8 +171,7 @@

e-Waybill

@@ -184,10 +180,10 @@

e-Waybill

@@ -203,84 +199,78 @@

e-Waybill

- {% if data.VehiclListDetails|length > 0 %} - - +
GSTIN of Supplier - {{ add_spacing(data.fromGstin, 5) }}-{{ data.fromTrdName }} + {{ add_spacing(data.fromGstin, 5) }} - {{ data.fromTrdName }}
Place of Dispatch - {{ data.fromPlace }}, - {{ get_state(data.actFromStateCode) }} - - {{ data.fromPincode }} + {{ data.fromPlace }}, {{ get_state(data.actFromStateCode) }} - {{ data.fromPincode }}
GSTIN of Recipient - {{ add_spacing(data.toGstin, 5) }}-{{ data.toTrdName }} - + {{ add_spacing(data.toGstin, 5) }} - {{ data.toTrdName }}
Place of Delivery - - {{ data.toPlace }}, - {{ get_state(data.actToStateCode) }} -{{ data.toPincode }} - + + {{ data.toPlace }}, {{ get_state(data.actToStateCode) }} -{{ data.toPincode }}
Document No. - - {{ data.docNo }} - + + {{ data.docNo }}
Document Date - - {{ data.docDate }} - + + {{ data.docDate }}
- Transaction Type: + Transaction Type - {{ get_transport_type(data.transactionType) }} + + {{ get_transport_type(data.transactionType) }}
Value of Goods - - {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }} + {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }}
{{ data.itemList[0].hsnCode }} - {%if data.itemList[0].productDesc%} - {{ " - " ~data.itemList[0].productDesc }} - {%endif%} - + {%if data.itemList[0].productDesc%} + {{ " - " ~data.itemList[0].productDesc }} + {%endif%} +
Transporter - {{ add_spacing(data.transporterId, 5) }}- - {{ data.transporterName }} + {{ add_spacing(data.transporterId, 5) }} - {{ data.transporterName }}
-
- Part - B +
+
+ +
+ {% if data.VehiclListDetails|length > 0 %} +
+ Part - B +
+ + + + + - - - + + + - - {% endif %} - - + + {% for detail in data.VehiclListDetails %} - + + + + + + + {% endfor%}
Mode + Vehicle / Trans
+ Doc No & Dt.
-
- - - - - - - - - - - - {% for detail in data.VehiclListDetails %} - - - - - - - - - - {% endfor%} - -
Mode - Vehicle / Trans
- Doc No & Dt. -
FromEntered DateEntered By - Cdata No.
- (If any) -
- Multi Veh.Info
- (If any) -
- {{ get_transport_mode(detail.transMode) }} - - {{ detail.vehicleNo }} / - {{ detail.transDocNo }} Dt: - {{ detail.transDocDate }} - {{ detail.fromPlace }} - {{ detail.enteredDate | - replace(":00 ", " ") }} - {{ detail.userGSTINTransin }}--
-
+
FromEntered DateEntered By + Cdata No.
+ (If any)
- + + Multi Veh.Info
+ (If any)
-
+
+ {{ get_transport_mode(detail.transMode) }} + + {{ detail.vehicleNo }} / + {{ detail.transDocNo }} Dt + {{ detail.transDocDate }} {{ detail.fromPlace }} + {{ detail.enteredDate | + replace(":00 ", " ") }} + {{ detail.userGSTINTransin }}--
-
- - + {% endif %} + + + + + + + + +
+
+ +
+
+
+
+ + + \ No newline at end of file From c6f62cfdd52efda0ea520a8db5721666cd76ef6a Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 21 Jan 2025 14:32:57 +0530 Subject: [PATCH 36/38] chore: plesant border colors --- .../print_format/e_waybill/e_waybill.css | 6 +- .../print_format/e_waybill/e_waybill.html | 73 +++++++++---------- .../e_waybill_detailed/e_waybill_detailed.css | 4 +- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css index 665559e849..3d03f5766c 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css @@ -21,7 +21,7 @@ } table, th, td { - border: 1px solid black; + border: 1px solid #d1d8dd; border-collapse: collapse; } @@ -75,10 +75,6 @@ table, th, td { border-top: 1px solid black !important; } -.print-format .ewb-no-span { - font-size: 13px; -} - .print-format .section-separator { border-top: 2px solid #bbb; margin: 8px 0 16px; diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html index 10f529d652..452b7533d8 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html @@ -40,8 +40,8 @@

e-Waybill

e-Waybill No - - {{ add_spacing(data.ewbNo, 4) }} + + {{ add_spacing(data.ewbNo, 4) }} @@ -50,8 +50,8 @@

e-Waybill

e-Waybill Date - - {{ data.ewayBillDate | replace("00 ", "") }} + + {{ data.ewayBillDate | replace("00 ", "") }} @@ -60,10 +60,8 @@

e-Waybill

Generated By - - - {{ add_spacing(data.userGstin, 5) }} - {{ generated_by }} - + + {{ add_spacing(data.userGstin, 5) }} - {{ generated_by }} @@ -72,8 +70,8 @@

e-Waybill

Valid From - - {{ data.ewayBillDate | replace(":00 ", " ") }} {{ " [" }} {{ data.actualDist }}{{ "Kms]" }} + + {{ data.ewayBillDate | replace(":00 ", " ") }} {{ " [" }} {{ data.actualDist }}{{ "Kms]" }} @@ -82,8 +80,8 @@

e-Waybill

Valid Until - - {{ data.validUpto[:10] }} + + {{ data.validUpto[:10] }} @@ -93,8 +91,8 @@

e-Waybill

IRN - - {{ irn }} + + {{ irn }} @@ -110,50 +108,50 @@

e-Waybill

- + @@ -162,44 +160,43 @@

e-Waybill

Transaction Type diff --git a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css index 1bca28f792..5332c0f943 100644 --- a/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css +++ b/india_compliance/gst_india/print_format/e_waybill_detailed/e_waybill_detailed.css @@ -44,12 +44,12 @@ thead > th { table, th, td { - border: 1px solid black; + border: 1px solid #d1d8dd; border-collapse: collapse; } .table-element .border div { - border: 1px solid black !important; + border: 1px solid #d1d8dd !important; width: 100%; padding: 5px; } From 01bbdf96d7bae59527b0cc8e98928af85ed9db74 Mon Sep 17 00:00:00 2001 From: Ninad Parikh <109862100+Ninad1306@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:09:24 +0530 Subject: [PATCH 37/38] feat: GST Invoice Management System (IMS) Implementation (#2638) Co-authored-by: Smit Vora --- .../gst_india/api_classes/taxpayer_base.py | 3 +- .../gst_india/api_classes/taxpayer_returns.py | 243 ++++- .../client_scripts/purchase_invoice.js | 11 +- .../gst_india/constants/__init__.py | 17 + india_compliance/gst_india/data/test_ims.json | 134 +++ .../gst_invoice_management_system/__init__.py | 246 +++++ .../gst_invoice_management_system.css | 109 ++ .../gst_invoice_management_system.js | 990 ++++++++++++++++++ .../gst_invoice_management_system.json | 97 ++ .../gst_invoice_management_system.py | 420 ++++++++ .../invoice_detail_comparison.html | 91 ++ .../test_gst_invoice_management_system.py | 29 + .../gst_inward_supply/gst_inward_supply.json | 61 +- .../gst_inward_supply/gst_inward_supply.py | 23 + .../doctype/gst_return_log/generate_gstr_1.py | 123 +-- .../doctype/gst_return_log/gst_return_log.py | 19 +- .../doctype/gstr_action/gstr_action.py | 38 +- .../gstr_import_log/gstr_import_log.json | 7 +- ...on.html => invoice_detail_comparison.html} | 2 +- .../purchase_reconciliation_tool.js | 662 ++---------- .../purchase_reconciliation_tool.py | 136 +-- .../purchase_reconciliation_utils.py | 124 +++ india_compliance/gst_india/utils/__init__.py | 27 + .../gst_india/utils/gstr_2/__init__.py | 120 ++- .../gst_india/utils/gstr_2/gstr.py | 10 +- .../gst_india/utils/gstr_2/ims.py | 319 ++++++ .../gst_india/utils/gstr_2/test_ims.py | 215 ++++ .../gst_india/utils/gstr_utils.py | 49 +- .../js/components/data_table_manager.js | 11 +- .../public/js/components/filter_group.js | 2 + india_compliance/public/js/ims.bundle.js | 5 + .../public/js/india_compliance.bundle.js | 1 + .../js/purchase_reconciliation_tool.bundle.js | 1 + .../js/reconciliation_components/actions.js | 155 +++ .../js/reconciliation_components/tabs.js | 412 ++++++++ 35 files changed, 4064 insertions(+), 848 deletions(-) create mode 100644 india_compliance/gst_india/data/test_ims.json create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.json create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html create mode 100644 india_compliance/gst_india/doctype/gst_invoice_management_system/test_gst_invoice_management_system.py rename india_compliance/gst_india/doctype/purchase_reconciliation_tool/{purchase_detail_comparison.html => invoice_detail_comparison.html} (98%) create mode 100644 india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py create mode 100644 india_compliance/gst_india/utils/gstr_2/ims.py create mode 100644 india_compliance/gst_india/utils/gstr_2/test_ims.py create mode 100644 india_compliance/public/js/ims.bundle.js create mode 100644 india_compliance/public/js/reconciliation_components/actions.js create mode 100644 india_compliance/public/js/reconciliation_components/tabs.js diff --git a/india_compliance/gst_india/api_classes/taxpayer_base.py b/india_compliance/gst_india/api_classes/taxpayer_base.py index 8f5ecb0316..da77b00582 100644 --- a/india_compliance/gst_india/api_classes/taxpayer_base.py +++ b/india_compliance/gst_india/api_classes/taxpayer_base.py @@ -466,13 +466,12 @@ def generate_app_key(self): return app_key - def get_files(self, return_period, token, action, endpoint, otp=None): + def get_files(self, return_period, token, action, endpoint): response = self.get( action=action, return_period=return_period, params={"ret_period": return_period, "token": token}, endpoint=endpoint, - otp=otp, ) if response.error_type == "queued": diff --git a/india_compliance/gst_india/api_classes/taxpayer_returns.py b/india_compliance/gst_india/api_classes/taxpayer_returns.py index 26807a075e..ac7e605b4e 100644 --- a/india_compliance/gst_india/api_classes/taxpayer_returns.py +++ b/india_compliance/gst_india/api_classes/taxpayer_returns.py @@ -1,7 +1,10 @@ import frappe from frappe import _ -from india_compliance.gst_india.api_classes.taxpayer_base import TaxpayerBaseAPI +from india_compliance.gst_india.api_classes.taxpayer_base import ( + FilesAPI, + TaxpayerBaseAPI, +) class ReturnsAPI(TaxpayerBaseAPI): @@ -23,9 +26,9 @@ class ReturnsAPI(TaxpayerBaseAPI): "RET2B1010": "authorization_failed", # API Authorization Failed for 2B } - def download_files(self, return_period, token, otp=None): + def download_files(self, return_period, token): return super().get_files( - return_period, token, action="FILEDET", endpoint="returns", otp=otp + return_period, token, action="FILEDET", endpoint="returns" ) def get_return_status(self, return_period, reference_id, otp=None): @@ -57,6 +60,7 @@ def proceed_to_file(self, return_type, return_period, is_nil_return, otp=None): class GSTR2bAPI(ReturnsAPI): API_NAME = "GSTR-2B" + END_POINT = "returns/gstr2b" def get_data(self, return_period, otp=None, file_num=None): params = {"rtnprd": return_period} @@ -67,10 +71,29 @@ def get_data(self, return_period, otp=None, file_num=None): action="GET2B", return_period=return_period, params=params, - endpoint="returns/gstr2b", + endpoint=self.END_POINT, otp=otp, ) + def regenerate_2b(self, return_period): + return self.put( + json={ + "action": "GEN2B", + "data": {"rtin": self.company_gstin, "itcprd": return_period}, + }, + endpoint=self.END_POINT, + ) + + def get_2b_gen_status(self, transaction_id): + return self.get( + action="GENSTS2B", + params={ + "gstin": self.company_gstin, + "int_tran_id": transaction_id, + }, + endpoint=self.END_POINT, + ) + class GSTR2aAPI(ReturnsAPI): API_NAME = "GSTR-2A" @@ -153,3 +176,215 @@ def file_gstr_1(self, return_period, summary_data, pan, evc_otp): }, endpoint="returns/gstr1", ) + + +class GSTR3bAPI(ReturnsAPI): + END_POINT = "returns/gstr3b" + + def setup(self, company_gstin, return_period): + self.return_period = return_period + super().setup(company_gstin=company_gstin) + + def get_data(self): + return self.get( + action="RETSUM", + return_period=self.return_period, + params={"gstin": self.company_gstin, "ret_period": self.return_period}, + endpoint=self.END_POINT, + ) + + def save_gstr3b(self, data): + return self.put( + return_period=self.return_period, + json={ + "action": "RETSAVE", + "data": data, + }, + endpoint=self.END_POINT, + ) + + def submit_gstr3b(self, data): + return self.post( + return_period=self.return_period, + json={ + "action": "RETSUBMIT", + "data": data, + }, + endpoint=self.END_POINT, + ) + + def save_offset_liability_gstr3b(self, data): + return self.put( + return_period=self.return_period, + json={ + "action": "RETOFFSET", + "data": data, + }, + endpoint=self.END_POINT, + ) + + def file_gstr_3b(self, data, pan, evc_otp): + return self.post( + return_period=self.return_period, + json={ + "action": "RETFILE", + "data": data, + "st": "EVC", + "sid": f"{pan}|{evc_otp}", + }, + endpoint=self.END_POINT, + ) + + def get_itc_liab_data(self): + return self.get( + action="AUTOLIAB", + return_period=self.return_period, + params={"gstin": self.company_gstin, "ret_period": self.return_period}, + endpoint=self.END_POINT, + ) + + def validate_3b_against_auto_calc(self, data): + return self.post( + return_period=self.return_period, + json={ + "action": "VALID", + "data": data, + }, + endpoint=self.END_POINT, + ) + + def get_system_calc_interest(self): + return self.get( + action="RETINT", + return_period=self.return_period, + params={"gstin": self.company_gstin, "ret_period": self.return_period}, + endpoint=self.END_POINT, + ) + + def recompute_interest(self): + return self.post( + return_period=self.return_period, + json={ + "action": "CMPINT", + "data": {"gstn": self.company_gstin, "ret_period": self.return_period}, + }, + endpoint=self.END_POINT, + ) + + def save_past_liab(self, data): + return self.put( + return_period=self.return_period, + json={"action": "RETBKP", "data": data}, + endpoint=self.END_POINT, + ) + + def get_itc_reversal_bal(self): + return self.get( + action="CLOSINGBAL", + return_period=self.return_period, + params={"gstin": self.company_gstin}, + endpoint=self.END_POINT, + ) + + def get_rcm_bal(self): + return self.get( + action="RCMCLOSINGBAL", + return_period=self.return_period, + params={"gstin": self.company_gstin}, + endpoint=self.END_POINT, + ) + + def get_opening_bal(self): + return self.get( + action="OPENINGBAL", + return_period=self.return_period, + params={"gstin": self.company_gstin}, + endpoint=self.END_POINT, + ) + + def get_rcm_opening_bal(self): + return self.get( + action="RCMOPNBAL", + return_period=self.return_period, + params={"gstin": self.company_gstin}, + endpoint=self.END_POINT, + ) + + def save_opening_bal(self, data): + return self.post( + return_period=self.return_period, + json={"action": "SAVEOB", "data": data}, + endpoint=self.END_POINT, + ) + + def submit_rcm_opening_bal(self, data): + return self.post( + return_period=self.return_period, + json={ + "action": "SAVERCMOPNBAL", + "data": data, + }, + endpoint=self.END_POINT, + ) + + +class IMSAPI(ReturnsAPI): + API_NAME = "IMS" + END_POINT = "returns/ims" + + def get_data(self, section): + return self.get( + action="GETINV", + params={ + "gstin": self.company_gstin, + "section": section, + }, + endpoint=self.END_POINT, + ) + + def download_files(self, return_period, token): + return self.get_files( + return_period, token, action="FILEDET", endpoint=self.END_POINT + ) + + def get_files(self, return_period, token, action, endpoint): + response = self.get( + action=action, + return_period=return_period, + params={"gstin": self.company_gstin, "token": token}, + endpoint=endpoint, + ) + + if response.error_type == "queued": + return response + + return FilesAPI().get_all(response) + + def save(self, data): + return self.put( + endpoint=self.END_POINT, + json={ + "action": "SAVE", + "data": {"rtin": self.company_gstin, "reqtyp": "SAVE", "invdata": data}, + }, + ) + + def reset(self, data): + return self.put( + endpoint=self.END_POINT, + json={ + "action": "RESETIMS", + "data": { + "rtin": self.company_gstin, + "reqtyp": "RESET", + "invdata": data, + }, + }, + ) + + def get_request_status(self, transaction_id): + return self.get( + action="REQSTS", + endpoint=self.END_POINT, + params={"gstin": self.company_gstin, "int_tran_id": transaction_id}, + ) diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js index dae0d69e32..bb4948ed16 100644 --- a/india_compliance/gst_india/client_scripts/purchase_invoice.js +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -74,20 +74,19 @@ frappe.ui.form.on(DOCTYPE, { on_submit: function (frm) { if (!frm._inward_supply) return; - // go back to previous page and match the invoice with the inward supply setTimeout(() => { - frappe.route_hooks.after_load = reco_frm => { - if (!reco_frm.purchase_reconciliation_tool) return; - purchase_reconciliation_tool.link_documents( - reco_frm, + frappe.route_hooks.after_load = source_frm => { + if (!source_frm.reconciliation_tabs) return; + reconciliation.link_documents( + source_frm, frm.doc.name, frm._inward_supply.name, "Purchase Invoice", false ); }; - frappe.set_route("Form", "Purchase Reconciliation Tool"); + frappe.set_route("Form", frm._inward_supply.source_doc); }, 2000); }, }); diff --git a/india_compliance/gst_india/constants/__init__.py b/india_compliance/gst_india/constants/__init__.py index 39f0996658..6086191907 100644 --- a/india_compliance/gst_india/constants/__init__.py +++ b/india_compliance/gst_india/constants/__init__.py @@ -36,6 +36,23 @@ "Input Service Distributor": "B2B", } +GST_CATEGORY_MAP = { + "R": "Regular", + "SEZWP": "SEZ supplies with payment of tax", + "SEZWOP": "SEZ supplies with out payment of tax", + "DE": "Deemed exports", + "CBW": "Intra-State Supplies attracting IGST", +} + +ACTION_MAP = {"A": "Accepted", "R": "Rejected", "P": "Pending", "N": "No Action"} + +STATUS_CODE_MAP = { + "P": "Processed", + "PE": "Processed with Errors", + "ER": "Error", + "IP": "In Progress", +} + EXPORT_TYPES = ( "WOP", # Without Payment of Tax [0] "WP", # With Payment of Tax [1] diff --git a/india_compliance/gst_india/data/test_ims.json b/india_compliance/gst_india/data/test_ims.json new file mode 100644 index 0000000000..214fee0d8e --- /dev/null +++ b/india_compliance/gst_india/data/test_ims.json @@ -0,0 +1,134 @@ +{ + "b2b": [ + { + "stin": "24MAYAS0100J1JD", + "inum": "b1", + "inv_typ": "R", + "action": "A", + "ispendactblocked": "N", + "srcform": "R1", + "rtnprd": "012023", + "srcfilstatus": "Not Filed", + "idt": "23-01-2023", + "val": 1000, + "pos": "24", + "txval": 100, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ], + "b2ba": [ + { + "oinum": "ab2", + "oidt": "24-02-2023", + "stin": "24MAYAS0100J1JD", + "rtnprd": "012023", + "inum": "b1a", + "action": "A", + "ispendactblocked": "N", + "inv_typ": "R", + "srcform": "R1", + "idt": "23-01-2023", + "val": 1000, + "pos": "07", + "txval": 100, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "srcfilstatus": "Not Filed", + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ], + "b2bdn": [ + { + "stin": "24MAYAS0100J1JD", + "nt_num": "dn2", + "action": "A", + "inv_typ": "R", + "srcform": "R1", + "ispendactblocked": "N", + "rtnprd": "012023", + "nt_dt": "24-02-2023", + "val": 1000.1, + "pos": "07", + "txval": 1000.1, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "srcfilstatus": "Filed", + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ], + "b2bdna": [ + { + "stin": "24MAYAS0100J1JD", + "ont_num": "dn2", + "ont_dt": "24-02-2023", + "nt_num": "dna2", + "action": "A", + "inv_typ": "R", + "srcform": "R1", + "ispendactblocked": "N", + "rtnprd": "012023", + "nt_dt": "24-02-2023", + "val": 1000.1, + "pos": "07", + "txval": 1000.1, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "srcfilstatus": "Filed", + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ], + "b2bcn": [ + { + "stin": "24MAYAS0100J1JD", + "nt_num": "cn2", + "action": "A", + "inv_typ": "R", + "srcform": "R1", + "rtnprd": "012023", + "ispendactblocked": "N", + "nt_dt": "24-02-2023", + "val": 1000.1, + "pos": "07", + "txval": 1000.1, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "srcfilstatus": "Not Filed", + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ], + "b2bcna": [ + { + "stin": "24MAYAS0100J1JD", + "ont_num": "cn2", + "ont_dt": "24-02-2023", + "nt_num": "cna2", + "action": "A", + "inv_typ": "R", + "srcform": "R1", + "ispendactblocked": "N", + "rtnprd": "012023", + "nt_dt": "24-02-2023", + "val": 1000.1, + "pos": "07", + "txval": 1000.1, + "iamt": 20, + "camt": 20, + "samt": 20, + "cess": 0, + "srcfilstatus": "Not Filed", + "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1" + } + ] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py b/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py new file mode 100644 index 0000000000..dde5bb557a --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py @@ -0,0 +1,246 @@ +import frappe +from frappe.query_builder import Case +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Abs, IfNull, Sum + +from india_compliance.gst_india.constants import GST_TAX_TYPES +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + GSTIN_RULES, + PAN_RULES, + BaseUtil, + Reconciler, +) + + +class IMSReconciler(Reconciler): + CATEGORIES = ( + {"doc_type": "Invoice", "category": "B2B"}, + {"doc_type": "Debit Note", "category": "CDNR"}, + {"doc_type": "Credit Note", "category": "CDNR"}, + ) + + def reconcile(self, filters): + """ + Reconcile purchases and inward supplies. + """ + for row in self.CATEGORIES: + filters["doc_type"], self.category = row.values() + + purchases = PurchaseInvoice().get_unmatched(filters) + inward_supplies = InwardSupply().get_unmatched(filters) + + # GSTIN Level matching + self.reconcile_for_rules(GSTIN_RULES, purchases, inward_supplies) + + # PAN Level matching + purchases = self.get_pan_level_data(purchases) + inward_supplies = self.get_pan_level_data(inward_supplies) + self.reconcile_for_rules(PAN_RULES, purchases, inward_supplies) + + +class InwardSupply: + def __init__(self): + self.IMS = frappe.qb.DocType("GST Inward Supply") + + def get_all(self, company_gstin, names=None): + query = self.get_query(company_gstin, ["action", "doc_type"]) + + if names: + query = query.where(self.IMS.name.isin(names)) + + return query.run(as_dict=True) + + def get_for_save(self, company_gstin): + return ( + self.get_query_for_upload(company_gstin) + .where(self.IMS.ims_action != "No Action") + .run(as_dict=True) + ) + + def get_for_reset(self, company_gstin): + return ( + self.get_query_for_upload(company_gstin) + .where(self.IMS.ims_action == "No Action") + .run(as_dict=True) + ) + + def get_query_for_upload(self, company_gstin): + return self.get_query( + company_gstin, + additional_fields=[ + "doc_type", + "is_amended", + "sup_return_period", + "document_value", + ], + ).where(self.IMS.ims_action != self.IMS.previous_ims_action) + + def get_unmatched(self, filters): + query = self.get_query(filters.company_gstin) + data = ( + query.where(IfNull(self.IMS.match_status, "") == "") + .where(self.IMS.doc_type == filters.doc_type) + .run(as_dict=True) + ) + + for doc in data: + doc.fy = BaseUtil.get_fy(doc.bill_date) + + return BaseUtil.get_dict_for_key("supplier_gstin", data) + + def get_query(self, company_gstin, additional_fields=None): + fields = self.get_fields(additional_fields=additional_fields) + + return ( + frappe.qb.from_(self.IMS) + .select( + *fields, + ConstantColumn("GST Inward Supply").as_("doctype"), + Case() + .when( + (self.IMS.ims_action == self.IMS.previous_ims_action), + False, + ) + .else_(True) + .as_("pending_upload"), + ) + .where(IfNull(self.IMS.previous_ims_action, "") != "") + .where(self.IMS.company_gstin == company_gstin) + ) + + def get_fields(self, additional_fields=None): + fields = [ + "supplier_gstin", + "supplier_name", + "company_gstin", + "bill_no", + "bill_date", + "name", + "is_reverse_charge", + "place_of_supply", + "link_name", + "link_doctype", + "match_status", + "ims_action", + "previous_ims_action", + "supply_type", + "classification", + "is_pending_action_allowed", + "supplier_return_form", + "is_supplier_return_filed", + ] + + if additional_fields: + fields += additional_fields + + fields = [self.IMS[field] for field in fields] + fields += self.get_tax_fields() + + return fields + + def get_tax_fields(self): + fields = GST_TAX_TYPES[:-1] + ("taxable_value",) + return [self.IMS[field] for field in fields] + + +class PurchaseInvoice: + def __init__(self): + self.PI = frappe.qb.DocType("Purchase Invoice") + self.PI_ITEM = frappe.qb.DocType("Purchase Invoice Item") + + def get_all(self, names=None, filters=None): + query = self.get_query(filters=filters) + + if names: + query = query.where(self.PI.name.isin(names)) + + purchases = query.run(as_dict=True) + + return {doc.name: doc for doc in purchases} + + def get_unmatched(self, filters): + gst_category = ( + "Registered Regular", + "Tax Deductor", + "Tax Collector", + "Input Service Distributor", + ) + is_return = 1 if filters.doc_type == "Credit Note" else 0 + + data = ( + self.get_query(filters=filters) + .where(self.PI.gst_category.isin(gst_category)) + .where(self.PI.reconciliation_status == "Unreconciled") + .where(self.PI.is_return == is_return) + .where(self.PI.ineligibility_reason != "ITC restricted due to PoS rules") + .run(as_dict=True) + ) + + for doc in data: + doc.fy = BaseUtil.get_fy(doc.bill_date) + + return BaseUtil.get_dict_for_key("supplier_gstin", data) + + def get_query(self, filters=None, additional_fields=None): + fields = self.get_fields(additional_fields) + + query = ( + frappe.qb.from_(self.PI) + .left_join(self.PI_ITEM) + .on(self.PI_ITEM.parent == self.PI.name) + .select( + Abs(Sum(self.PI_ITEM.taxable_value)).as_("taxable_value"), + *fields, + ConstantColumn("Purchase Invoice").as_("doctype"), + ) + .where(self.PI.docstatus == 1) + .where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable") + .where(self.PI.is_opening == "No") + .where(self.PI_ITEM.parenttype == "Purchase Invoice") + .where(self.PI.is_reverse_charge == 0) # for IMS + .groupby(self.PI.name) + ) + + if filters: + query = self.apply_filters(query, filters) + + return query + + def apply_filters(self, query, filters): + if filters.get("company"): + query = query.where(self.PI.company == filters.company) + + if filters.get("company_gstin"): + query = query.where(self.PI.company_gstin == filters.company_gstin) + + return query + + def get_fields(self, additional_fields=None): + fields = [ + "supplier_gstin", + "supplier_name", + "bill_no", + "bill_date", + "name", + "company", + "company_gstin", + "is_reverse_charge", + "place_of_supply", + ] + + if additional_fields: + fields += additional_fields + + fields = [self.PI[field] for field in fields] + fields += self.get_tax_fields() + + return fields + + def get_tax_fields(self): + return [ + self.query_tax_amount(f"{tax_type}_amount").as_(tax_type) + for tax_type in GST_TAX_TYPES + ] + + def query_tax_amount(self, field): + return Abs(Sum(getattr(self.PI_ITEM, field))) diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css new file mode 100644 index 0000000000..003527b27c --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css @@ -0,0 +1,109 @@ +div[data-page-route="GST Invoice Management System"] { + --dt-row-height: 34px; +} + +div[data-page-route="GST Invoice Management System"] .section-body { + max-width: 100% !important; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_html"] + .section-body { + margin-top: 0; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_html"] + .form-tabs-list { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 0 solid black; + padding-right: var(--padding-lg); + position: inherit; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_html"] + .form-tabs-list + .custom-button-group { + display: flex; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_html"] + .form-tabs-list + .inner-group-button, +.filter-selector { + margin-bottom: 8px; + margin-left: 8px; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_html"] + .form-tabs-list + .custom-button-group + .btn { + padding: 5px 10px; +} + +div[data-page-route="GST Invoice Management System"] .title-area .indicator-pill { + display: none; +} + +div[data-page-route="GST Invoice Management System"] .datatable .dt-scrollable { + overflow-y: auto !important; + margin-bottom: 2em; + min-height: calc(100vh - 450px); +} + +div[data-page-route="GST Invoice Management System"] .datatable .dt-row { + height: unset; +} + +div[data-page-route="GST Invoice Management System"] .datatable .dt-row-filter { + height: var(--dt-row-height); +} + +div[data-page-route="GST Invoice Management System"] + .datatable + .dt-row-filter + .dt-cell { + max-height: var(--dt-row-height); +} + +div[data-page-route="GST Invoice Management System"] [data-fieldname="no_invoice_data"], +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_empty_state"] { + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +div[data-page-route="GST Invoice Management System"] + [data-fieldname="no_invoice_data"] + > img, +div[data-page-route="GST Invoice Management System"] + [data-fieldname="invoice_empty_state"] + > img { + margin-bottom: var(--margin-md); + max-height: 100px; +} + +div[data-page-route="GST Invoice Management System"] .dropdown-divider { + height: 0; + margin: 5px 0; + border-top: 1px solid var(--border-color); + padding: 0; +} + +.modal-dialog div[data-fieldname="detail_table"] .table > tbody > tr > td { + text-align: center; + width: 30%; +} + +div[data-page-route="GST Invoice Management System"] .action-summary { + text-decoration: none; +} diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js new file mode 100644 index 0000000000..d693e43489 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js @@ -0,0 +1,990 @@ +// Copyright (c) 2024, Resilient Tech and contributors +// For license information, please see license.txt + +const api_enabled = india_compliance.is_api_enabled(); +const DOCTYPE = "GST Invoice Management System"; +const DOC_PATH = + "india_compliance.gst_india.doctype.gst_invoice_management_system.gst_invoice_management_system"; + +const category_map = { + "B2B-Invoices": "Invoice", + "B2B-Credit Notes": "Credit Note", + "B2B-Debit Notes": "Debit Note", +}; + +const ACTION_MAP = { + "No Action": "No Action", + Accept: "Accepted", + Pending: "Pending", + Reject: "Rejected", +}; + +frappe.ui.form.on(DOCTYPE, { + async setup(frm) { + await frappe.require("ims.bundle.js"); + + frm.reconciliation_tabs = new IMS( + frm, + ["invoice", "match_summary", "action_summary"], + "invoice_html" + ); + + frm.trigger("company"); + + // Setup Listeners + + // Download Queued + frappe.realtime.on("ims_download_queued", message => { + frappe.msgprint(message["message"]); + }); + + // Downloaded and Reconciled Invoices + frappe.realtime.on("ims_download_completed", message => { + frm.ims_actions.get_ims_data(); + frappe.show_alert({ message: message["message"], indicator: "green" }); + }); + + // Upload and Check Status + frappe.realtime.on("upload_data_and_check_status", async message => { + await frm.ims_actions.get_ims_data(); + frm.ims_actions.upload_ims_data(); + }); + }, + + async company(frm) { + render_empty_state(frm); + if (!frm.doc.company) return; + const options = await india_compliance.set_gstin_options(frm); + + frm.set_value("company_gstin", options[0]); + }, + + company_gstin: render_empty_state, + + refresh(frm) { + show_download_invoices_message(frm); + + frm.ims_actions = new IMSAction(frm); + frm.ims_actions.setup_actions(); + }, +}); + +class IMS extends reconciliation.reconciliation_tabs { + refresh(data) { + super.refresh(data); + this.set_actions_summary(); + } + + get_tab_group_fields() { + return [ + { + //hack: for the FieldGroup(Layout) to avoid rendering default "details" tab + fieldtype: "Section Break", + }, + { + label: "Match Summary", + fieldtype: "Tab Break", + fieldname: "match_summary_tab", + active: 1, + }, + { + fieldtype: "HTML", + fieldname: "match_summary_data", + }, + { + label: "Actions Summary", + fieldtype: "Tab Break", + fieldname: "action_summary_tab", + }, + { + fieldtype: "HTML", + fieldname: "action_summary_data", + }, + { + label: "Document View", + fieldtype: "Tab Break", + fieldname: "invoice_tab", + }, + { + fieldtype: "HTML", + fieldname: "invoice_data", + }, + ]; + } + + get_filter_fields() { + const fields = [ + { + label: "Supplier Name", + fieldname: "supplier_name", + fieldtype: "Autocomplete", + options: this.get_autocomplete_options("supplier_name"), + }, + { + label: "Supplier GSTIN", + fieldname: "supplier_gstin", + fieldtype: "Autocomplete", + options: this.get_autocomplete_options("supplier_gstin"), + }, + { + label: "Match Status", + fieldname: "match_status", + fieldtype: "Select", + options: [ + "Exact Match", + "Suggested Match", + "Mismatch", + "Manual Match", + "Missing in PI", + ], + }, + { + label: "Action", + fieldname: "ims_action", + fieldtype: "Select", + options: ["No Action", "Accepted", "Rejected", "Pending"], + }, + { + label: "Document Type", + fieldname: "doc_type", + fieldtype: "Select", + options: ["Invoice", "Credit Note", "Debit Note"], + }, + { + label: "Upload Pending", + fieldname: "pending_upload", + fieldtype: "Check", + }, + { + label: "Is Pending Action Allowed", + fieldname: "is_pending_action_allowed", + fieldtype: "Check", + }, + { + label: "Classification", + fieldname: "classification", + fieldtype: "Select", + options: ["B2B", "B2BA", "CDNR", "CDNRA"], + }, + { + label: "Is Supplier Return Filed", + fieldname: "is_supplier_return_filed", + fieldtype: "Check", + }, + ]; + + fields.forEach(field => (field.parent = DOCTYPE)); + return fields; + } + + set_listeners() { + const me = this; + + // TODO: Refactor like purchase_reconciliation.js + + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".supplier-gstin", + function (e) { + me.update_filter(e, "supplier_gstin", $(this).text().trim(), me); + } + ); + + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".match-status", + function (e) { + me.update_filter(e, "match_status", $(this).text(), me); + } + ); + + this.tabs.match_summary_tab.datatable.$datatable.on( + "click", + ".match-status", + function (e) { + me.update_filter(e, "match_status", $(this).text(), me); + } + ); + + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".ims-action", + function (e) { + me.update_filter(e, "ims_action", $(this).text(), me); + } + ); + + this.tabs.action_summary_tab.datatable.$datatable.on( + "click", + ".invoice-category", + function (e) { + me.update_filter(e, "doc_type", category_map[$(this).text()], me); + } + ); + + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".classification", + function (e) { + me.update_filter(e, "classification", $(this).text(), me); + } + ); + + this.tabs.invoice_tab.datatable.$datatable.on( + "click", + ".btn.eye", + function (e) { + const row = me.mapped_invoice_data[$(this).attr("data-name")]; + me.dm = new DetailViewDialog(me.frm, row); + } + ); + } + + async update_filter(e, field, field_value, me) { + e.preventDefault(); + + await me.filter_group.add_or_remove_filter([DOCTYPE, field, "=", field_value]); + me.filter_group.apply(); + } + + get_match_summary_columns() { + return [ + { + label: "Match Status", + fieldname: "match_status", + width: 200, + _value: (...args) => `${args[0]}`, + }, + { + label: "Count
2A/2B Docs", + fieldname: "inward_supply_count", + width: 120, + align: "center", + }, + { + label: "Count
Purchase Docs", + fieldname: "purchase_count", + width: 120, + align: "center", + }, + { + label: "Taxable Amount Diff
2A/2B - Purchase", + fieldname: "taxable_value_difference", + width: 180, + align: "center", + _value: (...args) => format_number(args[0]), + }, + { + label: "Tax Difference
2A/2B - Purchase", + fieldname: "tax_difference", + width: 180, + align: "center", + _value: (...args) => format_number(args[0]), + }, + { + label: "% Action Taken", + fieldname: "action_taken", + width: 120, + align: "center", + _value: (...args) => { + return ( + roundNumber( + (args[2].action_taken_count / args[2].total_docs) * 100, + 2 + ) + " %" + ); + }, + }, + ]; + } + + get_match_summary_data() { + if (!this.data.length) return []; + + const data = {}; + this.filtered_data.forEach(row => { + let new_row = data[row.match_status]; + if (!new_row) { + new_row = data[row.match_status] = { + match_status: row.match_status, + inward_supply_count: 0, + purchase_count: 0, + action_taken_count: 0, + total_docs: 0, + tax_difference: 0, + taxable_value_difference: 0, + }; + } + if (row.inward_supply_name) new_row.inward_supply_count += 1; + if (row.purchase_invoice_name) new_row.purchase_count += 1; + if (row.ims_action != "No Action") new_row.action_taken_count += 1; + new_row.total_docs += 1; + new_row.tax_difference += row.tax_difference || 0; + new_row.taxable_value_difference += row.taxable_value_difference || 0; + }); + + return Object.values(data); + } + + get_invoice_columns() { + return [ + { + fieldname: "view", + fieldtype: "html", + width: 60, + align: "center", + _value: (...args) => get_icon(...args), + }, + { + label: "Supplier Name", + fieldname: "supplier_name_gstin", + align: "center", + width: 200, + }, + { + label: "Bill No.", + fieldname: "bill_no", + align: "center", + width: 120, + }, + { + label: "Match Status", + fieldname: "match_status", + align: "center", + width: 120, + _value: (...args) => `${args[0]}`, + }, + { + label: "Action", + fieldname: "ims_action", + align: "center", + width: 100, + _value: (...args) => `${args[0]}`, + }, + { + label: "GST Inward
Supply", + fieldname: "inward_supply_name", + align: "center", + fieldtype: "Link", + options: "GST Inward Supply", + width: 150, + _after_format: (...args) => get_value_with_indicator(...args), + }, + { + label: "Linked Voucher", + fieldname: "linked_doc", + align: "center", + width: 150, + fieldtype: "Dynamic Link", + options: "linked_voucher_type", + }, + { + label: "Tax Difference
2A/2B - Purchase", + fieldname: "tax_difference", + align: "center", + width: 150, + _value: (...args) => format_number(args[0]), + }, + { + label: "Taxable Amount Diff
2A/2B - Purchase", + fieldname: "taxable_value_difference", + align: "center", + width: 160, + _value: (...args) => format_number(args[0]), + }, + { + label: "Classification", + fieldname: "classification", + align: "center", + width: 100, + _value: (...args) => + `${args[0]}`, + }, + ]; + } + + get_invoice_data() { + if (!this.data.length) return []; + + const data = []; + this.mapped_invoice_data = {}; + + this.filtered_data.forEach(row => { + this.mapped_invoice_data[row.inward_supply_name] = row; + + data.push({ + supplier_name_gstin: this.get_supplier_name_gstin(row), + bill_no: row.bill_no, + classification: row._inward_supply.classification, + ims_action: row.ims_action || "", + match_status: row.match_status, + linked_doc: row.purchase_invoice_name, + tax_difference: row.tax_difference, + taxable_value_difference: row.taxable_value_difference, + inward_supply_name: row.inward_supply_name, + pending_upload: row.pending_upload, + is_supplier_return_filed: row.is_supplier_return_filed, + }); + }); + + return data; + } + + get_action_summary_columns() { + return [ + { + label: "Category", + fieldname: "category", + width: 200, + _value: (...args) => + `${args[0]}`, + }, + { + label: "No Action", + fieldname: "no_action", + width: 200, + }, + { + label: "Accepted", + fieldname: "accepted", + width: 200, + }, + { + label: "Pending", + fieldname: "pending", + width: 200, + }, + { + label: "Rejected", + fieldname: "rejected", + width: 200, + }, + ]; + } + + get_action_summary_data(data) { + const category_map = { + Invoice: "B2B-Invoices", + "Credit Note": "B2B-Credit Notes", + "Debit Note": "B2B-Debit Notes", + }; + let summary_data = {}; + if (!data) data = this.filtered_data; + + data.forEach(row => { + const action = frappe.scrub(row.ims_action); + const category = category_map[row.doc_type]; + if (!summary_data[category]) { + summary_data[category] = { + category, + no_action: 0, + accepted: 0, + rejected: 0, + pending: 0, + }; + } + summary_data[category][action] += 1; + }); + + return Object.values(summary_data); + } + + async set_actions_summary() { + const actions_data = this.get_action_summary_data(this.data); + + if ($(".action-performed-summary").length) { + $(".action-performed-summary").remove(); + } + + $(function () { + $('[data-toggle="tooltip"]').tooltip(); + }); + + const actions_summary = { + no_action: { count: 0, color: "#7c7c7c" }, + accepted: { count: 0, color: "#28a745" }, + pending: { count: 0, color: "#ffc107" }, + rejected: { count: 0, color: "#e03636" }, + }; + + actions_data.forEach(row => { + actions_summary.accepted.count += row.accepted; + actions_summary.pending.count += row.pending; + actions_summary.rejected.count += row.rejected; + actions_summary.no_action.count += row.no_action; + }); + + const action_performed_cards = Object.entries(actions_summary) + .map(([value, data]) => { + const action = frappe.unscrub(value); + return `
+
${action}
+
+ +

+ ${data.count} +

+
+
`; + }) + .join(""); + + const action_performed_html = ` +
+ ${action_performed_cards} +
+ `; + + let element = $('[data-fieldname="data_section"]'); + element.prepend(action_performed_html); + + const me = this; + this.frm.$wrapper.find(".action-summary").click(async function (e) { + const [action, action_count] = $(this).attr("data-name").split("-"); + + if (action_count === "0") return; + + const fg = me.filter_group; + const filter = [DOCTYPE, "ims_action", "=", action]; + + if (fg.filter_exists(filter.slice(0, 2)) && !fg.filter_exists(filter)) + await me.filter_group.remove_filter([DOCTYPE, "ims_action"]); + + me.update_filter(e, "ims_action", action, me); + }); + } +} + +class IMSAction { + RETRY_INTERVALS = [2000, 3000, 15000, 30000, 60000, 120000, 300000, 600000, 720000]; // 5 second, 15 second, 30 second, 1 min, 2 min, 5 min, 10 min, 12 min + + constructor(frm) { + this.frm = frm; + } + + setup_actions() { + this.setup_document_actions(); + this.setup_row_actions(); + } + + setup_document_actions() { + // Primary Action + this.frm.disable_save(); + if (!this.frm.doc.data_state) { + this.frm.page.set_primary_action(__("Show Invoices"), () => + this.get_ims_data() + ); + } else { + this.frm.page.set_primary_action(__("Upload Invoices"), () => + this.upload_ims_data() + ); + } + + this.frm.add_custom_button(__("Download Invoices"), () => { + render_empty_state(this.frm); + this.download_ims_data(); + }); + } + + setup_row_actions() { + // Setup Custom Buttons + if (!this.frm.reconciliation_tabs?.data?.length) return; + if (this.frm.get_active_tab()?.df.fieldname == "invoice_tab") { + this.frm.add_custom_button( + __("Unlink"), + () => reconciliation.unlink_documents(this.frm), + __("Actions") + ); + this.frm.add_custom_button(__("dropdown-divider"), () => {}, __("Actions")); + } + + // Setup Bulk Actions + ["No Action", "Accept", "Pending", "Reject"].forEach(action => + this.frm.add_custom_button( + __(action), + () => apply_bulk_action(this.frm, ACTION_MAP[action]), + __("Actions") + ) + ); + + // Add Dropdown Divider to differentiate between IMS and Reconciliation Actions + this.frm.$wrapper + .find("[data-label='dropdown-divider']") + .addClass("dropdown-divider"); + + // move actions button next to filters + for (let button of this.frm.$wrapper.find( + ".custom-actions .inner-group-button" + )) { + if (button.innerText?.trim() != __("Actions")) continue; + this.frm.$wrapper.find(".custom-button-group .inner-group-button").remove(); + $(button).appendTo(this.frm.$wrapper.find(".custom-button-group")); + } + } + + async download_ims_data() { + await taxpayer_api.call({ + method: `${DOC_PATH}.download_invoices`, + args: { company_gstin: this.frm.doc.company_gstin }, + }); + + frappe.show_alert({ + message: __("Downloading Invoices"), + }); + } + + async get_ims_data() { + const { message } = await this.frm.call("autoreconcile_and_get_data"); + this.frm.__invoice_data = message.invoice_data; + + this.frm.reconciliation_tabs.render_data(this.frm.__invoice_data); + this.frm.doc.data_state = this.frm.__invoice_data.length + ? "available" + : "unavailable"; + + if (message.pending_actions.length) { + this.handle_upload_status(); + } + + // Toggle HTML fields + this.frm.refresh(); + } + + async upload_ims_data() { + if (!this.filter_invoices_to_upload().length) { + frappe.msgprint({ + title: __("No Data Found"), + message: __("No Invoices to Upload"), + indicator: "red", + }); + return; + } + + frappe.show_alert(__("Checking Upload Status")); + + const save_status = await this.upload_and_check_status("save"); + const reset_status = await this.upload_and_check_status("reset"); + + this.handle_upload_status(save_status, reset_status); + } + + async upload_and_check_status(action) { + await taxpayer_api.call({ + method: `${DOC_PATH}.${action}_invoices`, + args: { company_gstin: this.frm.doc.company_gstin }, + }); + + return this.get_upload_status_with_retry(action); + } + + async handle_upload_status(save_status, reset_status) { + if (!save_status) save_status = await this.get_upload_status_with_retry("save"); + + if (!reset_status) + reset_status = await this.get_upload_status_with_retry("reset"); + + const error_statuses = ["ER", "PE"]; + if ( + error_statuses.includes(save_status.status_cd) || + error_statuses.includes(reset_status.status_cd) + ) + return this.on_failed_upload(); + + return this.on_successful_upload(); + } + + get_upload_status_with_retry(action, retries = 0, now = false) { + return new Promise(resolve => { + setTimeout( + async () => { + const { message } = await taxpayer_api.call({ + method: `${DOC_PATH}.check_action_status`, + args: { company_gstin: this.frm.doc.company_gstin, action }, + }); + + if (!message.status_cd) { + resolve({ status_cd: "ER" }); + return; + } + + if ( + message.status_cd === "IP" && + retries < this.RETRY_INTERVALS.length + ) { + resolve( + await this.get_upload_status_with_retry(action, retries + 1) + ); + return; + } + + // Not IP + resolve(message); + }, + now ? 0 : this.RETRY_INTERVALS[retries] + ); + }); + } + + filter_invoices_to_upload() { + return this.frm.reconciliation_tabs.data.filter(row => row.pending_upload); + } + + on_failed_upload() { + frappe.msgprint({ + message: + "An error occurred while uploading the data. Please try downloading the data again and re-uploading it.", + indicator: "red", + title: __("GSTN Sync Required"), + primary_action: { + label: __("Sync and Reupload"), + action: () => { + frappe.hide_msgprint(); + render_empty_state(this.frm); + + taxpayer_api.call({ + method: `${DOC_PATH}.sync_with_gstn_and_reupload`, + args: { company_gstin: this.frm.doc.company_gstin }, + }); + }, + }, + }); + } + + on_successful_upload() { + // refresh existing data + const data = this.frm.reconciliation_tabs.data; + data.forEach(row => { + if (!row.pending_upload) return; + + row.pending_upload = false; + row.previous_ims_action = row.ims_action; + }); + + this.frm.reconciliation_tabs.refresh(data); + + frappe.show_alert({ + message: __("Uploaded Invoices Successfully"), + indicator: "green", + }); + } +} + +class DetailViewDialog extends reconciliation.detail_view_dialog { + _get_custom_actions() { + // setup actions + let actions = ["No Action", "Reject"].filter( + action => ACTION_MAP[action] != this.row.ims_action + ); + + if ( + this.row.match_status !== "Missing in PI" && + this.row.ims_action != "Accepted" + ) + actions.push("Accept"); + + if (this.row.is_pending_action_allowed && this.row.ims_action != "Pending") + actions.push("Pending"); + + if (this.row.match_status == "Missing in PI") actions.push("Create", "Link"); + else actions.push("Unlink"); + + return actions; + } + + _apply_custom_action(action) { + if (action == "Unlink") { + reconciliation.unlink_documents(this.frm, [this.row]); + } else if (action == "Link") { + reconciliation.link_documents( + this.frm, + this.data.purchase_invoice_name, + this.data.inward_supply_name, + this.dialog.get_value("doctype"), + true + ); + } else if (action == "Create") { + reconciliation.create_new_purchase_invoice( + this.data, + this.frm.doc.company, + this.frm.doc.company_gstin, + DOCTYPE + ); + } else { + apply_action(this.frm, ACTION_MAP[action], [this.row.inward_supply_name]); + } + } + + _get_button_css(action) { + if (action == "No Action") return "btn-secondary"; + if (action == "Accept") return "btn-success not-grey"; + if (action == "Reject") return "btn-danger not-grey"; + if (action == "Pending") return "btn-warning not-grey"; + if (action == "Create") return "btn-primary not-grey"; + if (action == "Link") return "btn-primary not-grey btn-link disabled"; + } + + _set_missing_doctype() { + if (this.row.match_status == "Missing in PI") + this.missing_doctype = "Purchase Invoice"; + else return; + + this.doctype_options = ["Purchase Invoice"]; + } +} + +function render_empty_state(frm) { + frm.__invoice_data = null; + frm.doc.data_state = null; + + $(".action-performed-summary").remove(); + + frm.refresh(); +} + +function apply_bulk_action(frm, action) { + const active_tab = frm.get_active_tab()?.df.fieldname; + if (!active_tab) return; + + const tab = frm.reconciliation_tabs.tabs[active_tab]; + + // from current tab + const selected_rows = tab.datatable.get_checked_items(); + if (!selected_rows.length) { + frappe.show_alert({ message: __("Please select invoices"), indicator: "red" }); + return; + } + + // summary => invoice + const affected_rows = get_affected_rows( + active_tab, + selected_rows, + frm.reconciliation_tabs.filtered_data + ); + + apply_action(frm, action, affected_rows); + + if (tab) tab.datatable.clear_checked_items(); +} + +async function apply_action(frm, action, invoice_names) { + // Validate and Update JS + let pending_not_allowed = []; + let accept_not_allowed = []; + let new_data = []; + frm.reconciliation_tabs.data.forEach(row => { + if (invoice_names.includes(row.inward_supply_name)) { + if (!is_pending_allowed(row, action)) { + pending_not_allowed.push(row.inward_supply_name); + } else if (!is_accept_allowed(row, action)) { + accept_not_allowed.push(row.inward_supply_name); + } else { + row.ims_action = action; + + // Update pending upload status + if (row.ims_action !== row.previous_ims_action) + row.pending_upload = true; + else row.pending_upload = false; + } + } + + new_data.push({ ...row }); + }); + + invoice_names = invoice_names.filter( + name => + !(pending_not_allowed.includes(name) || accept_not_allowed.includes(name)) + ); + + if (pending_not_allowed.length) { + frappe.msgprint({ + message: __( + "Some invoices are not allowed to be marked as Pending." + ), + indicator: "red", + }); + } else if (accept_not_allowed.length) { + frappe.msgprint({ + message: __( + "Some invoices cannot be Accepted. Please ensure they are linked to a purchase." + ), + indicator: "red", + }); + } + + if (!invoice_names.length) return; + + // Update + frm._call("update_action", { invoice_names, action }); + + frm.reconciliation_tabs.refresh(new_data); + frappe.show_alert({ message: "Action applied successfully", indicator: "green" }); +} + +function is_pending_allowed(row, action) { + if (action === "Pending" && !row.is_pending_action_allowed) return false; + return true; +} + +function is_accept_allowed(row, action) { + // "Accept" not allowed for Missing in PI + if (action === "Accepted" && row.match_status === "Missing in PI") return false; + return true; +} + +function get_icon(value, column, data) { + return ``; +} + +function get_value_with_indicator(value, column, data) { + let color = "green"; + let title = "Supplier Return: Filed"; + + if (!data.is_supplier_return_filed) { + color = "red"; + title = "Supplier Return: Not Filed"; + } + + value = $(value) + .addClass(`indicator ${color}`) + .attr("title", title) + .prop("outerHTML"); + + return value; +} + +function get_affected_rows(tab, selection, data) { + let invoices = []; + if (tab == "invoice_tab") invoices = selection; + + if (tab == "match_summary_tab") + invoices = data.filter( + inv => selection.filter(row => row.match_status == inv.match_status).length + ); + + if (tab == "action_summary_tab") + invoices = data.filter( + inv => + selection.filter(row => category_map[row.category] == inv.doc_type) + .length + ); + + return invoices.map(row => row.inward_supply_name); +} + +function show_download_invoices_message(frm) { + if (!api_enabled) return; + + const msg_tag = frm + .get_field("no_invoice_data") + .$wrapper.find("#download-invoices-alert"); + + // show alert + msg_tag.removeClass("hidden"); + + // setup listener + msg_tag.on("click", () => { + frm.ims_actions.download_ims_data(); + }); +} diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.json b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.json new file mode 100644 index 0000000000..728e7ca6cf --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.json @@ -0,0 +1,97 @@ +{ + "actions": [], + "creation": "2024-10-23 12:09:30.335663", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_2", + "company_gstin", + "data_section", + "invoice_html", + "invoice_empty_state", + "no_invoice_data" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_gstin", + "fieldtype": "Autocomplete", + "label": "Company GSTIN" + }, + { + "depends_on": "eval: doc.data_state === \"available\"", + "fieldname": "invoice_html", + "fieldtype": "HTML" + }, + { + "depends_on": "eval: !doc.data_state", + "fieldname": "invoice_empty_state", + "fieldtype": "HTML", + "options": "\"No\n\t

{{ __(\"Generate to view the data\") }}

" + }, + { + "depends_on": "eval: doc.data_state === \"unavailable\"", + "fieldname": "no_invoice_data", + "fieldtype": "HTML", + "options": "\"No\n\t

{{ __(\"No data available for selected filters.\") }}

\n{{ __(\"Download Invoices\") }}" + }, + { + "fieldname": "data_section", + "fieldtype": "Section Break" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2025-01-17 12:19:24.001794", + "modified_by": "Administrator", + "module": "GST India", + "name": "GST Invoice Management System", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts User", + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py new file mode 100644 index 0000000000..ed8bc1ad9c --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py @@ -0,0 +1,420 @@ +# Copyright (c) 2024, Resilient Tech and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +from india_compliance.gst_india.api_classes.taxpayer_base import ( + TaxpayerBaseAPI, + otp_handler, +) +from india_compliance.gst_india.api_classes.taxpayer_returns import IMSAPI +from india_compliance.gst_india.constants import STATUS_CODE_MAP +from india_compliance.gst_india.doctype.gst_invoice_management_system import ( + IMSReconciler, + InwardSupply, + PurchaseInvoice, +) +from india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1 import ( + verify_request_in_progress, +) +from india_compliance.gst_india.doctype.gstr_action.gstr_action import set_gstr_actions +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + ReconciledData, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + get_formatted_options, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + link_documents as _link_documents, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + unlink_documents as _unlink_documents, +) +from india_compliance.gst_india.utils.gstr_2 import ( + GSTRCategory, + ReturnType, + download_ims_invoices, + get_data_handler, +) +from india_compliance.gst_india.utils.gstr_utils import ( + publish_action_status_notification, +) + +CATEGORY_MAP = { + "Invoice_0": GSTRCategory.B2B.value, + "Invoice_1": GSTRCategory.B2BA.value, + "Debit Note_0": GSTRCategory.B2BDN.value, + "Debit Note_1": GSTRCategory.B2BDNA.value, + "Credit Note_0": GSTRCategory.B2BCN.value, + "Credit Note_1": GSTRCategory.B2BCNA.value, +} + + +class GSTInvoiceManagementSystem(Document): + @frappe.whitelist() + def autoreconcile_and_get_data(self): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + filters = frappe._dict( + { + "company": self.company, + "company_gstin": self.company_gstin, + } + ) + + # Auto-Reconcile invoices + IMSReconciler().reconcile(filters) + + return { + "invoice_data": self.get_invoice_data(filters=filters), + "pending_actions": self.get_pending_actions(), + } + + def get_invoice_data(self, inward_supply=None, purchase=None, filters=None): + if not filters: + filters = frappe._dict( + { + "company": self.company, + "company_gstin": self.company_gstin, + } + ) + + inward_supplies = InwardSupply().get_all( + company_gstin=self.company_gstin, names=inward_supply + ) + + if not purchase: + purchase = [doc.link_name for doc in inward_supplies] + + purchases = PurchaseInvoice().get_all(names=purchase, filters=filters) + + invoice_data = [] + for doc in inward_supplies: + invoice_data.append( + frappe._dict( + { + "ims_action": doc.ims_action, + "pending_upload": doc.pending_upload, + "previous_ims_action": doc.previous_ims_action, + "is_pending_action_allowed": doc.is_pending_action_allowed, + "is_supplier_return_filed": doc.is_supplier_return_filed, + "doc_type": doc.doc_type, + "_inward_supply": doc, + "_purchase_invoice": purchases.pop( + doc.link_name, frappe._dict() + ), + } + ) + ) + + # Missing in 2A/2B is ignored for IMS + + ReconciledData().process_data(invoice_data, retain_doc=True) + + return invoice_data + + def get_pending_actions(self): + return frappe.get_all( + "GSTR Action", + { + "parent": f"IMS-ALL-{self.company_gstin}", + "parenttype": "GST Return Log", + "status": ["is", "not set"], + "token": ["is", "set"], + }, + pluck="request_type", + ) + + @frappe.whitelist() + def update_action(self, invoice_names, action): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + invoice_names = frappe.parse_json(invoice_names) + + frappe.db.set_value( + "GST Inward Supply", + {"name": ("in", invoice_names)}, + "ims_action", + action, + ) + + @frappe.whitelist() + def get_invoice_details(self, purchase_name, inward_supply_name): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + inward_supply = InwardSupply().get_all( + self.company_gstin, names=[inward_supply_name] + ) + purchases = PurchaseInvoice().get_all(names=[purchase_name]) + + reconciliation_data = [ + frappe._dict( + { + "_inward_supply": ( + inward_supply[0] if inward_supply else frappe._dict() + ), + "_purchase_invoice": purchases.get(purchase_name, frappe._dict()), + } + ) + ] + + ReconciledData().process_data(reconciliation_data, retain_doc=True) + + return reconciliation_data[0] + + @frappe.whitelist() + def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + purchases, inward_supplies = _link_documents( + purchase_invoice_name, inward_supply_name, link_doctype + ) + + return self.get_invoice_data(inward_supplies, purchases) + + @frappe.whitelist() + def unlink_documents(self, data): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + purchases, inward_supplies = _unlink_documents(data) + + return self.get_invoice_data(inward_supplies, purchases) + + @frappe.whitelist() + def get_link_options(self, doctype, filters): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + if isinstance(filters, dict): + filters = frappe._dict(filters) + + PI = frappe.qb.DocType("Purchase Invoice") + query = ( + PurchaseInvoice() + .get_query(additional_fields=["gst_category", "is_return"]) + .where(PI.supplier_gstin.like(f"%{filters.supplier_gstin}%")) + .where(PI.bill_date[filters.bill_from_date : filters.bill_to_date]) + ) + + if not filters.show_matched: + query = query.where(PI.reconciliation_status == "Unreconciled") + + return get_formatted_options(query.run(as_dict=True)) + + +@frappe.whitelist() +@otp_handler +def download_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + TaxpayerBaseAPI(company_gstin).validate_auth_token() + + frappe.enqueue(download_ims_invoices, queue="long", gstin=company_gstin) + + +@frappe.whitelist() +@otp_handler +def save_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + return save_ims_invoices(company_gstin) + + +@frappe.whitelist() +@otp_handler +def reset_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + return reset_ims_invoices(company_gstin) + + +@frappe.whitelist() +@otp_handler +def sync_with_gstn_and_reupload(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + TaxpayerBaseAPI(company_gstin).validate_auth_token() + + frappe.enqueue( + download_and_upload_ims_invoices, + queue="long", + company_gstin=company_gstin, + ) + + +@frappe.whitelist() +@otp_handler +def check_action_status(company_gstin, action): + frappe.has_permission("GST Return Log", "write", throw=True) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + return process_save_or_reset_ims(ims_log, action) + + +def download_and_upload_ims_invoices(company_gstin): + """ + 1. This function will download invoices from GST Portal, + and if there are some queued invoices then upload will be skipped. + + 2. If there are no queued invoices, then it will upload the invoices to GST Portal. + + 3. It will check the status regardless of whether any data was uploaded or not. + (To notify user that process is completed successfully). + """ + + has_queued_invoices = download_ims_invoices(company_gstin, for_upload=True) + + # TODO: flag for pending upload and cron job for queued invoices + if has_queued_invoices: + return + + frappe.publish_realtime( + "upload_data_and_check_status", + user=frappe.session.user, + ) + + +def save_ims_invoices(company_gstin): + if not frappe.db.exists("GST Return Log", f"IMS-ALL-{company_gstin}"): + frappe.throw(_("Please download invoices before uploading")) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + save_data = get_data_for_upload(company_gstin, "save") + + if not save_data: + return + + verify_request_in_progress(ims_log, False) + + api = IMSAPI(company_gstin) + + # Upload invoices where action in ["Accepted", "Rejected", "Pending"] + response = api.save(save_data) + set_gstr_actions(ims_log, "save", response.get("reference_id"), api.request_id) + + +def reset_ims_invoices(company_gstin): + if not frappe.db.exists("GST Return Log", f"IMS-ALL-{company_gstin}"): + frappe.throw(_("Please download invoices before uploading")) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + reset_data = get_data_for_upload(company_gstin, "reset") + + if not reset_data: + return + + verify_request_in_progress(ims_log, False) + + api = IMSAPI(company_gstin) + + # Reset invoices where action is "No Action" + response = api.reset(reset_data) + set_gstr_actions(ims_log, "reset", response.get("reference_id"), api.request_id) + + +def get_data_for_upload(company_gstin, request_type): + upload_data = {} + key_invoice_map = {} + + if request_type == "save": + gst_inward_supply_list = InwardSupply().get_for_save(company_gstin) + else: + gst_inward_supply_list = InwardSupply().get_for_reset(company_gstin) + + for invoice in gst_inward_supply_list: + key = f"{invoice.doc_type}_{invoice.is_amended}" + key_invoice_map.setdefault(key, []).append(invoice) + + for key, invoices in key_invoice_map.items(): + category = CATEGORY_MAP[key] + _class = get_data_handler(ReturnType.IMS.value, category)() + upload_invoices = [] + + for invoice in invoices: + upload_invoices.append( + { + **_class.convert_data_to_gov_format(invoice), + **_class.get_category_details(invoice), + } + ) + + if upload_invoices: + upload_data[category.lower()] = upload_invoices + + return upload_data + + +def process_save_or_reset_ims(return_log, action): + response = {"status_cd": "P"} # dummy_response + doc = return_log.get_unprocessed_action(action) + if not doc: + return response + + api = IMSAPI(return_log.gstin) + response = api.get_request_status(doc.token) + + status_cd = response.get("status_cd") + + if status_cd != "IP": + doc.db_set({"status": STATUS_CODE_MAP.get(status_cd)}) + publish_action_status_notification( + "IMS", + return_log.return_period, + doc.request_type, + status_cd, + return_log.gstin, + api.request_id if status_cd == "ER" else None, + ) + + if status_cd in ["P", "PE"]: + # Exclude erroneous invoices from previous IMS action update + # This is enqueued because linking of integration request is enqueued + # TODO: flag for re-upload? + frappe.enqueue( + update_previous_ims_action, + queue="long", + integration_request=doc.integration_request, + error_report=response.get("error_report") or dict(), + ) + + return response + + +def update_previous_ims_action(integration_request, error_report=None): + uploaded_invoices = get_uploaded_invoices(integration_request) + + for category, invoices in uploaded_invoices.items(): + _class = get_data_handler(ReturnType.IMS.value, category.upper()) + _class().update_previous_ims_action(invoices, error_report.get(category, [])) + + +def get_uploaded_invoices(integration_request): + request_data = frappe.parse_json( + frappe.db.get_value( + "Integration Request", {"name": integration_request}, "data" + ) + ) + + if not request_data: + return {} + + if isinstance(request_data, str): + request_data = frappe.parse_json(request_data) + + return request_data["body"]["data"]["invdata"] diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html b/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html new file mode 100644 index 0000000000..6735501a0d --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html @@ -0,0 +1,91 @@ +
+
GSTIN of SupplierGSTIN of Supplier - - {{ add_spacing(data.fromGstin, 5) }} - {{ data.fromTrdName }} + + {{ add_spacing(data.fromGstin, 5) }} - {{ data.fromTrdName }}
Place of Dispatch - - {{ data.fromPlace }}, {{ get_state(data.actFromStateCode) }} - {{ data.fromPincode }} + + {{ data.fromPlace }}, {{ get_state(data.actFromStateCode) }} - {{ data.fromPincode }}
GSTIN of Recipient - - {{ add_spacing(data.toGstin, 5) }} - {{ data.toTrdName }} + + {{ add_spacing(data.toGstin, 5) }} - {{ data.toTrdName }}
Place of Delivery - - {{ data.toPlace }}, {{ get_state(data.actToStateCode) }} -{{ data.toPincode }} + + {{ data.toPlace }}, {{ get_state(data.actToStateCode) }} -{{ data.toPincode }}
Document No. - - {{ data.docNo }} + + {{ data.docNo }}
Document Date - - {{ data.docDate }} + + {{ data.docDate }}
- - {{ get_transport_type(data.transactionType) }} + + {{ get_transport_type(data.transactionType) }}
Value of Goods - - {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }} + + {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }}
HSN Code - - {{ data.itemList[0].hsnCode }} + + {{ data.itemList[0].hsnCode }} {%if data.itemList[0].productDesc%} {{ " - " ~data.itemList[0].productDesc }} {%endif%} -
Reason for Transportation - - {{ get_supply_type(data.supplyType) }} - {{ get_sub_supply_type(data.subSupplyType) }} + + {{ get_supply_type(data.supplyType) }} - {{ get_sub_supply_type(data.subSupplyType) }}
Transporter - - {{ add_spacing(data.transporterId, 5) }} - {{ data.transporterName }} + + {{ add_spacing(data.transporterId, 5) }} - {{ data.transporterName }}
+ + + + + + + + + + + + + + + + {% else %} + + {% endif %} + + {% if purchase.name %} + + {% else %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + {% if purchase.cgst || inward_supply.cgst %} + + + + + {% endif %} + {% if purchase.sgst || inward_supply.sgst %} + + + + + {% endif %} + {% if purchase.igst || inward_supply.igst %} + + + + + {% endif %} + {% if purchase.cess || inward_supply.cess %} + + + + + {% endif %} + + + + + +
2A / 2BPurchase
Company GSTIN{{ inward_supply.company_gstin || '-' }}{{ purchase.company_gstin || '-' }}
Document Links + {% if inward_supply.name %} + {{ frappe.utils.get_form_link("GST Inward Supply", + inward_supply.name, true) }}-{{ frappe.utils.get_form_link(purchase.doctype, + purchase.name, true)}}-
Bill No + {{ inward_supply.bill_no || '-' }}{{ purchase.bill_no || '-' }}
Bill Date + + {{ frappe.format(inward_supply.bill_date, {'fieldtype': 'Date'}) || '-' }} + + {{ frappe.format(purchase.bill_date, {'fieldtype': 'Date'}) || '-' }} +
Place of Supply{{ inward_supply.place_of_supply || '-' }}{{ purchase.place_of_supply || '-' }}
Reverse Charge{{ inward_supply.is_reverse_charge || '-' }}{{ purchase.is_reverse_charge || '-' }}
CGST + {{ inward_supply.cgst || '-' }}{{ purchase.cgst || '-' }}
SGST + {{ inward_supply.sgst || '-' }}{{ purchase.sgst || '-' }}
IGST + {{ inward_supply.igst || '-' }}{{ purchase.igst || '-' }}
CESS + {{ inward_supply.cess || '-' }}{{ purchase.cess || '-' }}
Taxable Amount + {{ inward_supply.taxable_value || '-' }}{{ purchase.taxable_value || '-' }}
+ \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/test_gst_invoice_management_system.py b/india_compliance/gst_india/doctype/gst_invoice_management_system/test_gst_invoice_management_system.py new file mode 100644 index 0000000000..dece865045 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/test_gst_invoice_management_system.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, Resilient Tech and Contributors +# See license.txt + +# import frappe +# from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +# class TestGSTInvoiceManagementSystem(UnitTestCase): +# """ +# Unit tests for GSTInvoiceManagementSystem. +# Use this class for testing individual functions and methods. +# """ + +# pass + + +# class TestGSTInvoiceManagementSystem(IntegrationTestCase): +# """ +# Integration tests for GSTInvoiceManagementSystem. +# Use this class for testing interactions between multiple components. +# """ + +# pass diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json index 96091e8262..11dc8d019c 100644 --- a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json @@ -70,7 +70,15 @@ "gstr_3b_filled", "column_break_47", "gstr_1_filing_date", - "registration_cancel_date" + "registration_cancel_date", + "ims_details_section", + "ims_action", + "is_pending_action_allowed", + "previous_ims_action", + "column_break_ppww", + "supplier_return_form", + "is_supplier_return_filed", + "is_downloaded_from_ims" ], "fields": [ { @@ -397,6 +405,57 @@ "fieldtype": "Float", "label": "Taxable Value" }, + { + "fieldname": "ims_action", + "fieldtype": "Data", + "label": "IMS Action", + "read_only": 1 + }, + { + "default": "No Action", + "fieldname": "previous_ims_action", + "fieldtype": "Data", + "hidden": 1, + "label": "Previous IMS Action", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_pending_action_allowed", + "fieldtype": "Check", + "label": "Is Pending Action Allowed", + "read_only": 1 + }, + { + "fieldname": "column_break_ppww", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_supplier_return_filed", + "fieldtype": "Check", + "label": "Is Supplier Return Filed", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_downloaded_from_ims", + "fieldtype": "Check", + "hidden": 1, + "label": "Downloaded from IMS", + "read_only": 1 + }, + { + "fieldname": "ims_details_section", + "fieldtype": "Section Break", + "label": "IMS Details" + }, + { + "fieldname": "supplier_return_form", + "fieldtype": "Data", + "label": "Supplier Return Form", + "read_only": 1 + }, { "default": "0", "fieldname": "is_downloaded_from_2b", diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py index 354ebff4d9..03e15b135e 100644 --- a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py @@ -17,6 +17,9 @@ def before_save(self): if self.gstr_1_filing_date: self.gstr_1_filled = True + if self.previous_ims_action and not self.get("ims_action"): + self.ims_action = self.previous_ims_action + if self.match_status != "Amended" and ( self.other_return_period or self.is_amended ): @@ -49,6 +52,26 @@ def create_inward_supply(transaction): return gst_inward_supply.save(ignore_permissions=True) +def update_previous_ims_action(transaction): + """ + After successfull upload of IMS Invoices, + update the ims_action taken in previous_ims_action field. + """ + filters = { + "bill_no": transaction.bill_no, + "bill_date": transaction.bill_date, + "classification": transaction.classification, + "supplier_gstin": transaction.supplier_gstin, + } + + frappe.db.set_value( + "GST Inward Supply", + filters, + "previous_ims_action", + transaction.previous_ims_action or "No Action", + ) + + def update_docs_for_amendment(doc): fields = [ "name", diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index 67bbd380c9..a24305e62d 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -7,13 +7,16 @@ from frappe.utils import flt, sbool from india_compliance.gst_india.api_classes.taxpayer_returns import GSTR1API -from india_compliance.gst_india.utils.gstr_1 import GovJsonKey, GSTR1_SubCategory -from india_compliance.gst_india.utils.gstr_1.__init__ import ( +from india_compliance.gst_india.constants import STATUS_CODE_MAP +from india_compliance.gst_india.doctype.gstr_action.gstr_action import set_gstr_actions +from india_compliance.gst_india.utils.gstr_1 import ( CATEGORY_SUB_CATEGORY_MAPPING, SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX, SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE, + GovJsonKey, GSTR1_Category, GSTR1_DataField, + GSTR1_SubCategory, ) from india_compliance.gst_india.utils.gstr_1.gstr_1_download import ( download_gstr1_json_data, @@ -23,13 +26,10 @@ convert_to_internal_data_format, summarize_retsum_data, ) +from india_compliance.gst_india.utils.gstr_utils import ( + publish_action_status_notification, +) -status_code_map = { - "P": "Processed", - "PE": "Processed with Errors", - "ER": "Error", - "IP": "In Progress", -} MAXIMUM_UPLOAD_SIZE = 5200000 @@ -739,7 +739,7 @@ def reset_gstr1(self, is_nil_return, force): api = GSTR1API(self) response = api.reset_gstr_1_data(self.return_period) - set_gstr1_actions(self, "reset", response.get("reference_id"), api.request_id) + set_gstr_actions(self, "reset", response.get("reference_id"), api.request_id) def process_reset_gstr1(self): if not self.actions: @@ -756,8 +756,9 @@ def process_reset_gstr1(self): response = api.get_return_status(self.return_period, doc.token) if response.get("status_cd") != "IP": - doc.db_set({"status": status_code_map.get(response.get("status_cd"))}) - enqueue_notification( + doc.db_set({"status": STATUS_CODE_MAP.get(response.get("status_cd"))}) + publish_action_status_notification( + "GSTR-1", self.return_period, "reset", response.get("status_cd"), @@ -790,7 +791,7 @@ def upload_gstr1(self, json_data, force): api = GSTR1API(self) response = api.save_gstr_1_data(self.return_period, json_data) - set_gstr1_actions(self, "upload", response.get("reference_id"), api.request_id) + set_gstr_actions(self, "upload", response.get("reference_id"), api.request_id) def process_upload_gstr1(self): if not self.actions: @@ -808,8 +809,9 @@ def process_upload_gstr1(self): status_cd = response.get("status_cd") if status_cd != "IP": - doc.db_set({"status": status_code_map.get(status_cd)}) - enqueue_notification( + doc.db_set({"status": STATUS_CODE_MAP.get(status_cd)}) + publish_action_status_notification( + "GSTR-1", self.return_period, "upload", status_cd, @@ -844,7 +846,7 @@ def proceed_to_file_gstr1(self, is_nil_return, force): # Return Form already ready to be filed if response.error and response.error.error_cd == "RET00003" or is_nil_return: - set_gstr1_actions( + set_gstr_actions( self, "proceed_to_file", response.get("reference_id"), @@ -853,7 +855,7 @@ def proceed_to_file_gstr1(self, is_nil_return, force): ) return self.fetch_and_compare_summary(api) - set_gstr1_actions( + set_gstr_actions( self, "proceed_to_file", response.get("reference_id"), api.request_id ) @@ -874,7 +876,7 @@ def process_proceed_to_file_gstr1(self): if response.get("status_cd") == "IP": return response - doc.db_set({"status": status_code_map.get(response.get("status_cd"))}) + doc.db_set({"status": STATUS_CODE_MAP.get(response.get("status_cd"))}) return self.fetch_and_compare_summary(api, response) @@ -908,7 +910,8 @@ def fetch_and_compare_summary(self, api, response=None): "differing_categories": differing_categories, } ) - enqueue_notification( + publish_action_status_notification( + "GSTR-1", self.return_period, "proceed_to_file", response.get("status_cd"), @@ -940,7 +943,7 @@ def file_gstr1(self, pan, otp, force): } ) - set_gstr1_actions( + set_gstr_actions( self, "file", response.get("ack_num"), @@ -1061,85 +1064,3 @@ def get_differing_categories(mapped_summary, gov_summary): break return differing_categories - - -def set_gstr1_actions(doc, request_type, token, request_id, status=None): - if not token: - return - - row = { - "request_type": request_type, - "token": token, - "creation_time": frappe.utils.now_datetime(), - } - - if status: - row["status"] = status - - doc.append("actions", row) - doc.save() - enqueue_link_integration_request(token, request_id) - - -def enqueue_link_integration_request(token, request_id): - """ - Integration request is enqueued. Hence, it's name is not available immediately. - Hence, link it after the request is processed. - """ - frappe.enqueue( - link_integration_request, queue="long", token=token, request_id=request_id - ) - - -def link_integration_request(token, request_id): - doc_name = frappe.db.get_value("Integration Request", {"request_id": request_id}) - if doc_name: - frappe.db.set_value( - "GSTR Action", {"token": token}, {"integration_request": doc_name} - ) - - -def enqueue_notification( - return_period, request_type, status_cd, gstin, request_id=None -): - frappe.enqueue( - create_notification, - queue="long", - return_period=return_period, - request_type=request_type, - status_cd=status_cd, - gstin=gstin, - request_id=request_id, - ) - - -def create_notification(return_period, request_type, status_cd, gstin, request_id=None): - # request_id shows failure response - status_message_map = { - "P": f"Data {request_type} for GSTIN {gstin} and return period {return_period} has been successfully completed.", - "PE": f"Data {request_type} for GSTIN {gstin} and return period {return_period} is completed with errors", - "ER": f"Data {request_type} for GSTIN {gstin} and return period {return_period} has encountered errors", - } - - if request_id and ( - doc_name := frappe.db.get_value( - "Integration Request", {"request_id": request_id} - ) - ): - document_type = "Integration Request" - document_name = doc_name - else: - document_type = document_name = "GSTR-1 Beta" - - notification = frappe.get_doc( - { - "doctype": "Notification Log", - "for_user": frappe.session.user, - "type": "Alert", - "document_type": document_type, - "document_name": document_name, - "subject": f"Data {request_type} for GSTIN {gstin} and return period {return_period}", - "email_content": status_message_map.get(status_cd), - } - ) - notification.insert() diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py index 44242e0206..7796741e55 100644 --- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -19,7 +19,10 @@ FileGSTR1, GenerateGSTR1, ) -from india_compliance.gst_india.utils import is_production_api_enabled +from india_compliance.gst_india.utils import ( + get_party_for_gstin, + is_production_api_enabled, +) DOCTYPE = "GST Return Log" @@ -337,3 +340,17 @@ def get_compressed_data(json_data): def get_decompressed_data(content): return frappe.parse_json(frappe.safe_decode(gzip.decompress(content))) + + +def create_ims_return_log(company_gstin): + company = get_party_for_gstin(company_gstin, "Company") + + if frappe.db.exists("GST Return Log", f"IMS-ALL-{company_gstin}"): + return + + ims_log = frappe.new_doc("GST Return Log") + ims_log.return_period = "ALL" + ims_log.company = company + ims_log.gstin = company_gstin + ims_log.return_type = "IMS" + ims_log.insert() diff --git a/india_compliance/gst_india/doctype/gstr_action/gstr_action.py b/india_compliance/gst_india/doctype/gstr_action/gstr_action.py index f267f39ec4..5f99f6a0ac 100644 --- a/india_compliance/gst_india/doctype/gstr_action/gstr_action.py +++ b/india_compliance/gst_india/doctype/gstr_action/gstr_action.py @@ -1,9 +1,45 @@ # Copyright (c) 2024, Resilient Tech and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class GSTRAction(Document): pass + + +def set_gstr_actions(doc, request_type, token, request_id, status=None): + if not token: + return + + row = { + "request_type": request_type, + "token": token, + "creation_time": frappe.utils.now_datetime(), + } + + if status: + row["status"] = status + + doc.append("actions", row) + doc.save() + enqueue_link_integration_request(token, request_id) + + +def enqueue_link_integration_request(token, request_id): + """ + Integration request is enqueued. Hence, it's name is not available immediately. + Hence, link it after the request is processed. + """ + frappe.enqueue( + link_integration_request, queue="long", token=token, request_id=request_id + ) + + +def link_integration_request(token, request_id): + doc_name = frappe.db.get_value("Integration Request", {"request_id": request_id}) + if doc_name: + frappe.db.set_value( + "GSTR Action", {"token": token}, {"integration_request": doc_name} + ) diff --git a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json index da40c8e6cb..d464ba105d 100644 --- a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json +++ b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json @@ -24,10 +24,9 @@ }, { "fieldname": "classification", - "fieldtype": "Select", + "fieldtype": "Data", "in_standard_filter": 1, - "label": "Classification", - "options": "\nB2B\nB2BA\nCDNR\nCDNRA\nISD\nISDA\nIMPG\nIMPGSEZ" + "label": "Classification" }, { "fieldname": "return_period", @@ -68,7 +67,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-29 11:54:43.449587", + "modified": "2024-12-31 18:32:42.409478", "modified_by": "Administrator", "module": "GST India", "name": "GSTR Import Log", diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparison.html b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/invoice_detail_comparison.html similarity index 98% rename from india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparison.html rename to india_compliance/gst_india/doctype/purchase_reconciliation_tool/invoice_detail_comparison.html index 3bef8ae602..1e44a0e33d 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparison.html +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/invoice_detail_comparison.html @@ -15,7 +15,7 @@ Document Links - {% if inward_supply.name %} + {% if inward_supply.name %} {{ frappe.utils.get_form_link("GST Inward Supply", inward_supply.name, true) }} {% else %} diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js index be3295606c..157fc43e79 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js @@ -1,8 +1,6 @@ // Copyright (c) 2022, Resilient Tech and contributors // For license information, please see license.txt -frappe.provide("purchase_reconciliation_tool"); - const DOCTYPE = "Purchase Reconciliation Tool"; const tooltip_info = { purchase_period: "Returns purchases during this period where no match is found.", @@ -70,7 +68,11 @@ frappe.ui.form.on(DOCTYPE, { await frappe.require("purchase_reconciliation_tool.bundle.js"); frm.trigger("company"); - frm.purchase_reconciliation_tool = new PurchaseReconciliationTool(frm); + frm.reconciliation_tabs = new PurchaseReconciliationTool( + frm, + ["invoice", "supplier", "summary"], + "reconciliation_html" + ); frm.events.handle_download_message(frm); }, @@ -185,107 +187,42 @@ frappe.ui.form.on(DOCTYPE, { }, }); -class PurchaseReconciliationTool { - constructor(frm) { - this.init(frm); - this.render_tab_group(); - this.setup_filter_button(); - } - - init(frm) { - this.frm = frm; - this.data = []; - this.$wrapper = this.frm.get_field("reconciliation_html").$wrapper; - this._tabs = ["invoice", "supplier", "summary"]; - } - - generate_data() { - this.data = this.frm.__reconciliation_data; - this.filtered_data = this.frm.__reconciliation_data; - - // clear filters - this.filter_group.filter_x_button.click(); - this.render_data_tables(); - } - - refresh(data) { - if (data) { - this.data = data; - this.refresh_filter_fields(); - } - - this.apply_filters(!!data); - - // data unchanged! - if (this.rendered_data == this.filtered_data) return; - - this._tabs.forEach(tab => { - this.tabs[`${tab}_tab`].datatable?.refresh(this[`get_${tab}_data`]()); - }); - - this.rendered_data = this.filtered_data; - } - - render_tab_group() { - this.tab_group = new frappe.ui.FieldGroup({ - fields: [ - { - //hack: for the FieldGroup(Layout) to avoid rendering default "details" tab - fieldtype: "Section Break", - }, - { - label: "Match Summary", - fieldtype: "Tab Break", - fieldname: "summary_tab", - active: 1, - }, - { - fieldtype: "HTML", - fieldname: "summary_data", - }, - { - label: "Supplier View", - fieldtype: "Tab Break", - fieldname: "supplier_tab", - }, - { - fieldtype: "HTML", - fieldname: "supplier_data", - }, - { - label: "Document View", - fieldtype: "Tab Break", - fieldname: "invoice_tab", - }, - { - fieldtype: "HTML", - fieldname: "invoice_data", - }, - ], - body: this.$wrapper, - frm: this.frm, - }); - - this.tab_group.make(); - - // make tabs_dict for easy access - this.tabs = Object.fromEntries( - this.tab_group.tabs.map(tab => [tab.df.fieldname, tab]) - ); - } - - setup_filter_button() { - this.filter_group = new india_compliance.FilterGroup({ - doctype: DOCTYPE, - parent: this.$wrapper.find(".form-tabs-list"), - filter_options: { - fieldname: "supplier_name", - filter_fields: this.get_filter_fields(), +class PurchaseReconciliationTool extends reconciliation.reconciliation_tabs { + get_tab_group_fields() { + return [ + { + //hack: for the FieldGroup(Layout) to avoid rendering default "details" tab + fieldtype: "Section Break", }, - on_change: () => { - this.refresh(); + { + label: "Match Summary", + fieldtype: "Tab Break", + fieldname: "summary_tab", + active: 1, }, - }); + { + fieldtype: "HTML", + fieldname: "summary_data", + }, + { + label: "Supplier View", + fieldtype: "Tab Break", + fieldname: "supplier_tab", + }, + { + fieldtype: "HTML", + fieldname: "supplier_data", + }, + { + label: "Document View", + fieldtype: "Tab Break", + fieldname: "invoice_tab", + }, + { + fieldtype: "HTML", + fieldname: "invoice_data", + }, + ]; } get_filter_fields() { @@ -353,55 +290,6 @@ class PurchaseReconciliationTool { return fields; } - refresh_filter_fields() { - this.filter_group.filter_options.filter_fields = this.get_filter_fields(); - } - - get_autocomplete_options(field) { - const options = []; - this.data.forEach(row => { - if (row[field] && !options.includes(row[field])) options.push(row[field]); - }); - return options; - } - - apply_filters(force, supplier_filter) { - const has_filters = this.filter_group.filters.length > 0 || supplier_filter; - if (!has_filters) { - this.filters = null; - this.filtered_data = this.data; - return; - } - - let filters = this.filter_group.get_filters(); - if (supplier_filter) filters.push(supplier_filter); - if (!force && this.filters === filters) return; - - this.filters = filters; - this.filtered_data = this.data.filter(row => { - return filters.every(filter => - india_compliance.FILTER_OPERATORS[filter[2]]( - filter[3] || "", - row[filter[1]] || "" - ) - ); - }); - } - - render_data_tables() { - this._tabs.forEach(tab => { - this.tabs[`${tab}_tab`].datatable = new india_compliance.DataTableManager({ - $wrapper: this.tab_group.get_field(`${tab}_data`).$wrapper, - columns: this[`get_${tab}_columns`](), - data: this[`get_${tab}_data`](), - options: { - cellHeight: 55, - }, - }); - }); - this.set_listeners(); - } - set_listeners() { const me = this; this.tabs.invoice_tab.datatable.$datatable.on( @@ -753,16 +641,6 @@ class PurchaseReconciliationTool { }, ]; } - - get_supplier_name_gstin(row) { - return ` - ${row.supplier_name} -
- - ${row.supplier_gstin || ""} - - `; - } } class PurchaseReconciliationToolAction { @@ -806,11 +684,11 @@ class PurchaseReconciliationToolAction { setup_row_actions() { const action_group = __("Actions"); - if (!this.frm.purchase_reconciliation_tool?.data?.length) return; + if (!this.frm.reconciliation_tabs?.data?.length) return; if (this.frm.get_active_tab()?.df.fieldname == "invoice_tab") { this.frm.add_custom_button( __("Unlink"), - () => unlink_documents(this.frm), + () => reconciliation.unlink_documents(this.frm), action_group ); this.frm.add_custom_button(__("dropdown-divider"), () => {}, action_group); @@ -849,7 +727,7 @@ class PurchaseReconciliationToolAction { frm.__reconciliation_data = message; - frm.purchase_reconciliation_tool.generate_data(); + frm.reconciliation_tabs.render_data(frm.__reconciliation_data); frm.doc.data_state = message.length ? "available" : "unavailable"; // Toggle HTML fields @@ -858,7 +736,7 @@ class PurchaseReconciliationToolAction { export_data(selected_row) { const data_to_export = - this.frm.purchase_reconciliation_tool.get_filtered_data(selected_row); + this.frm.reconciliation_tabs.get_filtered_data(selected_row); if (selected_row) delete data_to_export.supplier_summary; const url = @@ -872,207 +750,22 @@ class PurchaseReconciliationToolAction { } } -class DetailViewDialog { - table_fields = [ - "name", - "bill_no", - "bill_date", - "taxable_value", - "cgst", - "sgst", - "igst", - "cess", - "is_reverse_charge", - "place_of_supply", - ]; - - constructor(frm, row) { - this.frm = frm; - this.row = row; - this.render_dialog(); - } - - async render_dialog() { - await this.get_invoice_details(); - this.process_data(); - this.init_dialog(); - this.setup_actions(); - this.render_html(); - this.dialog.show(); - } - - async get_invoice_details() { - const { message } = await this.frm._call("get_invoice_details", { - purchase_name: this.row.purchase_invoice_name, - inward_supply_name: this.row.inward_supply_name, - }); - - this.data = message; - } - - process_data() { - for (let key of ["_purchase_invoice", "_inward_supply"]) { - const doc = this.data[key]; - if (!doc) continue; - - this.table_fields.forEach(field => { - if (field == "is_reverse_charge" && doc[field] != undefined) - doc[field] = doc[field] ? "Yes" : "No"; - }); - } - } - - init_dialog() { - const supplier_details = ` -
${this.row.supplier_name} - ${this.row.supplier_gstin ? ` (${this.row.supplier_gstin})` : ""} -
- `; - - this.dialog = new frappe.ui.Dialog({ - title: `Detail View (${this.row.classification})`, - fields: [ - ...this._get_document_link_fields(), - { - fieldtype: "HTML", - fieldname: "supplier_details", - options: supplier_details, - }, - { - fieldtype: "HTML", - fieldname: "diff_cards", - }, - { - fieldtype: "HTML", - fieldname: "detail_table", - }, - ], - }); - this.set_link_options(); - } - - _get_document_link_fields() { - if (this.row.match_status == "Missing in 2A/2B") - this.missing_doctype = "GST Inward Supply"; - else if (this.row.match_status == "Missing in PI") - if (["IMPG", "IMPGSEZ"].includes(this.row.classification)) - this.missing_doctype = "Bill of Entry"; - else this.missing_doctype = "Purchase Invoice"; - else return []; - - return [ - { - label: "GSTIN", - fieldtype: "Data", - fieldname: "supplier_gstin", - default: this.row.supplier_gstin, - onchange: () => this.set_link_options(), - }, - { - label: "Date Range", - fieldtype: "DateRange", - fieldname: "date_range", - default: [ - this.frm.doc.purchase_from_date, - this.frm.doc.purchase_to_date, - ], - onchange: () => this.set_link_options(), - }, - { - fieldtype: "Column Break", - }, - { - label: "Document Type", - fieldtype: "Autocomplete", - fieldname: "doctype", - default: this.missing_doctype, - options: - this.missing_doctype == "GST Inward Supply" - ? ["GST Inward Supply"] - : ["Purchase Invoice", "Bill of Entry"], - - read_only_depends_on: `eval: ${ - this.missing_doctype == "GST Inward Supply" - }`, - - onchange: () => { - const doctype = this.dialog.get_value("doctype"); - this.dialog - .get_field("show_matched") - .set_label(`Show matched options for linking ${doctype}`); - }, - }, - { - label: `Document Name`, - fieldtype: "Autocomplete", - fieldname: "link_with", - onchange: () => this.refresh_data(), - }, - { - label: `Show matched options for linking ${this.missing_doctype}`, - fieldtype: "Check", - fieldname: "show_matched", - onchange: () => this.set_link_options(), - }, - { - fieldtype: "Section Break", - }, - ]; - } - - async set_link_options() { - if (!this.dialog.get_value("doctype")) return; - - this.filters = { - supplier_gstin: this.dialog.get_value("supplier_gstin"), - bill_from_date: this.dialog.get_value("date_range")[0], - bill_to_date: this.dialog.get_value("date_range")[1], - show_matched: this.dialog.get_value("show_matched"), - purchase_doctype: this.data.purchase_doctype, - }; - - const { message } = await this.frm._call("get_link_options", { - doctype: this.dialog.get_value("doctype"), - filters: this.filters, - }); - - this.dialog.get_field("link_with").set_data(message); - } - - setup_actions() { - // determine actions - let actions = []; +class DetailViewDialog extends reconciliation.detail_view_dialog { + _get_custom_actions() { const doctype = this.dialog.get_value("doctype"); - if (this.row.match_status == "Missing in 2A/2B") actions.push("Link", "Ignore"); + if (this.row.match_status == "Missing in 2A/2B") return ["Link", "Ignore"]; else if (this.row.match_status == "Missing in PI") if (doctype == "Purchase Invoice") - actions.push("Create", "Link", "Pending", "Ignore"); - else actions.push("Link", "Pending", "Ignore"); - else actions.push("Unlink", "Accept", "Pending"); - - // setup actions - actions.forEach(action => { - this.dialog.add_custom_action( - action, - () => { - this._apply_custom_action(action); - this.dialog.hide(); - }, - `mr-2 ${this._get_button_css(action)}` - ); - }); - - this.dialog.$wrapper - .find(".btn.btn-secondary.not-grey") - .removeClass("btn-secondary"); - this.dialog.$wrapper.find(".modal-footer").css("flex-direction", "inherit"); + return ["Create", "Link", "Pending", "Ignore"]; + else return ["Link", "Pending", "Ignore"]; + else return ["Unlink", "Accept", "Pending"]; } _apply_custom_action(action) { if (action == "Unlink") { - unlink_documents(this.frm, [this.row]); + reconciliation.unlink_documents(this.frm, [this.row]); } else if (action == "Link") { - purchase_reconciliation_tool.link_documents( + reconciliation.link_documents( this.frm, this.data.purchase_invoice_name, this.data.inward_supply_name, @@ -1080,10 +773,11 @@ class DetailViewDialog { true ); } else if (action == "Create") { - create_new_purchase_invoice( + reconciliation.create_new_purchase_invoice( this.data, this.frm.doc.company, - this.frm.doc.company_gstin + this.frm.doc.company_gstin, + DOCTYPE ); } else { apply_action(this.frm, action, [this.row]); @@ -1099,87 +793,22 @@ class DetailViewDialog { if (action == "Accept") return "btn-primary not-grey"; } - toggle_link_btn(disabled) { - const btn = this.dialog.$wrapper.find(".modal-footer .btn-link"); - if (disabled) btn.addClass("disabled"); - else btn.removeClass("disabled"); - } - - async refresh_data() { - this.toggle_link_btn(true); - const field = this.dialog.get_field("link_with"); - if (field.value) this.toggle_link_btn(false); + _set_missing_doctype() { + if (this.row.match_status == "Missing in 2A/2B") + this.missing_doctype = "GST Inward Supply"; + else if (this.row.match_status == "Missing in PI") + if (["IMPG", "IMPGSEZ"].includes(this.row.classification)) + this.missing_doctype = "Bill of Entry"; + else this.missing_doctype = "Purchase Invoice"; + else return; if (this.missing_doctype == "GST Inward Supply") - this.row.inward_supply_name = field.value; - else this.row.purchase_invoice_name = field.value; - - await this.get_invoice_details(); - this.process_data(); - - this.row = this.data; - this.render_html(); - } - - render_html() { - this.render_cards(); - this.render_table(); - } - - render_cards() { - let cards = [ - { - value: this.row.tax_difference, - label: "Tax Difference", - datatype: "Currency", - currency: frappe.boot.sysdefaults.currency, - indicator: - this.row.tax_difference === 0 ? "text-success" : "text-danger", - }, - { - value: this.row.taxable_value_difference, - label: "Taxable Amount Difference", - datatype: "Currency", - currency: frappe.boot.sysdefaults.currency, - indicator: - this.row.taxable_value_difference === 0 - ? "text-success" - : "text-danger", - }, - ]; - - if (!this.row.purchase_invoice_name || !this.row.inward_supply_name) cards = []; - - new india_compliance.NumberCardManager({ - $wrapper: this.dialog.fields_dict.diff_cards.$wrapper, - cards: cards, - }); + this.doctype_options = ["GST Inward Supply"]; + else this.doctype_options = ["Purchase Invoice", "Bill of Entry"]; } - render_table() { - const detail_table = this.dialog.fields_dict.detail_table; - - detail_table.html( - frappe.render_template("purchase_detail_comparison", { - purchase: this.data._purchase_invoice, - inward_supply: this.data._inward_supply, - }) - ); - detail_table.$wrapper.removeClass("not-matched"); - this._set_value_color(detail_table.$wrapper); - } - - _set_value_color(wrapper) { - if (!this.row.purchase_invoice_name || !this.row.inward_supply_name) return; - - ["place_of_supply", "is_reverse_charge"].forEach(field => { - if (this.data._purchase_invoice[field] == this.data._inward_supply[field]) - return; - - wrapper - .find(`[data-label='${field}'], [data-label='${field}']`) - .addClass("not-matched"); - }); + _get_default_date_range() { + return [this.frm.doc.purchase_from_date, this.frm.doc.purchase_to_date]; } } @@ -1553,9 +1182,7 @@ class EmailDialog { } get_attachment() { - const export_data = this.frm.purchase_reconciliation_tool.get_filtered_data( - this.data - ); + const export_data = this.frm.reconciliation_tabs.get_filtered_data(this.data); frappe.call({ method: "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.generate_excel_attachment", @@ -1677,86 +1304,6 @@ function patch_set_active_tab(frm) { }; } -purchase_reconciliation_tool.link_documents = async function ( - frm, - purchase_invoice_name, - inward_supply_name, - link_doctype, - alert = true -) { - if (frm.get_active_tab()?.df.fieldname != "invoice_tab") return; - - // link documents & update data. - const { message: r } = await frm._call("link_documents", { - purchase_invoice_name, - inward_supply_name, - link_doctype, - }); - - const reco_tool = frm.purchase_reconciliation_tool; - const new_data = reco_tool.data.filter( - row => - !( - row.purchase_invoice_name == purchase_invoice_name || - row.inward_supply_name == inward_supply_name - ) - ); - new_data.push(...r); - - reco_tool.refresh(new_data); - if (alert) - after_successful_action(frm.purchase_reconciliation_tool.tabs.invoice_tab); -}; - -async function unlink_documents(frm, selected_rows) { - if (frm.get_active_tab()?.df.fieldname != "invoice_tab") return; - const { invoice_tab } = frm.purchase_reconciliation_tool.tabs; - if (!selected_rows) selected_rows = invoice_tab.datatable.get_checked_items(); - - if (!selected_rows.length) - return frappe.show_alert({ - message: __("Please select rows to unlink"), - indicator: "red", - }); - - // validate selected rows - selected_rows.forEach(row => { - if (row.match_status.includes("Missing")) - frappe.throw( - __( - "You have selected rows where no match is available. Please remove them before unlinking." - ) - ); - }); - - // unlink documents & update table - const { message: r } = await frm.call("unlink_documents", { data: selected_rows }); - - const unlinked_docs = get_unlinked_docs(selected_rows); - - const reco_tool = frm.purchase_reconciliation_tool; - const new_data = reco_tool.data.filter( - row => - !( - unlinked_docs.has(row.purchase_invoice_name) || - unlinked_docs.has(row.inward_supply_name) - ) - ); - new_data.push(...r); - reco_tool.refresh(new_data); - after_successful_action(invoice_tab); -} - -function get_unlinked_docs(selected_rows) { - const unlinked_docs = new Set(); - selected_rows.forEach(row => { - unlinked_docs.add(row.purchase_invoice_name); - unlinked_docs.add(row.inward_supply_name); - }); - - return unlinked_docs; -} - function deepcopy(array) { return JSON.parse(JSON.stringify(array)); } @@ -1765,11 +1312,11 @@ function apply_action(frm, action, selected_rows) { const active_tab = frm.get_active_tab()?.df.fieldname; if (!active_tab) return; - const tab = frm.purchase_reconciliation_tool.tabs[active_tab]; + const tab = frm.reconciliation_tabs.tabs[active_tab]; if (!selected_rows) selected_rows = tab.datatable.get_checked_items(); // get affected rows - const { filtered_data, data } = frm.purchase_reconciliation_tool; + const { filtered_data, data } = frm.reconciliation_tabs; let affected_rows = get_affected_rows(active_tab, selected_rows, filtered_data); if (!affected_rows.length) @@ -1821,16 +1368,8 @@ function apply_action(frm, action, selected_rows) { return true; }); - frm.purchase_reconciliation_tool.refresh(new_data); - after_successful_action(tab); -} - -function after_successful_action(tab) { - if (tab) tab.datatable.clear_checked_items(); - frappe.show_alert({ - message: "Action applied successfully", - indicator: "green", - }); + frm.reconciliation_tabs.refresh(new_data); + reconciliation.after_successful_action(tab); } function has_matching_row(row, array) { @@ -1852,65 +1391,6 @@ function get_affected_rows(tab, selection, data) { ); } -async function create_new_purchase_invoice(row, company, company_gstin) { - if (row.match_status != "Missing in PI") return; - const doc = row._inward_supply; - - const { message: supplier } = await frappe.call({ - method: "india_compliance.gst_india.utils.get_party_for_gstin", - args: { gstin: row.supplier_gstin }, - }); - - let company_address; - await frappe.model.get_value( - "Address", - { gstin: company_gstin, is_your_company_address: 1 }, - "name", - r => (company_address = r.name) - ); - - frappe.route_hooks.after_load = frm => { - function _set_value(values) { - for (const key in values) { - if (values[key] == frm.doc[key]) continue; - frm.set_value(key, values[key]); - } - } - - const values = { - company: company, - bill_no: doc.bill_no, - bill_date: doc.bill_date, - is_reverse_charge: ["Yes", 1].includes(doc.is_reverse_charge) ? 1 : 0, - is_return: ["CDNR", "CDNRA"].includes(doc.classification) ? 1 : 0, - }; - - _set_value({ - ...values, - supplier: supplier, - shipping_address: company_address, - billing_address: company_address, - }); - - // validated this on save - frm._inward_supply = { - ...values, - name: row.inward_supply_name, - company_gstin: company_gstin, - inward_supply: row.inward_supply, - supplier_gstin: row.supplier_gstin, - place_of_supply: doc.place_of_supply, - cgst: doc.cgst, - sgst: doc.sgst, - igst: doc.igst, - cess: doc.cess, - taxable_value: doc.taxable_value, - }; - }; - - frappe.new_doc("Purchase Invoice"); -} - function render_empty_state(frm) { frm.__reconciliation_data = null; frm.doc.data_state = null; diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py index 2d341ae764..aa10e5b76d 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -22,6 +22,18 @@ ReconciledData, Reconciler, ) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + get_formatted_options, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + link_documents as _link_documents, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + set_reconciliation_status, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + unlink_documents as _unlink_documents, +) from india_compliance.gst_india.utils import ( get_gstin_list, get_json_from_file, @@ -30,7 +42,7 @@ ) from india_compliance.gst_india.utils.exporter import ExcelExporter from india_compliance.gst_india.utils.gstr_2 import ( - ACTIONS, + GSTR_2A_ACTIONS, IMPORT_CATEGORY, ReturnType, download_gstr_2a, @@ -79,6 +91,8 @@ def onload(self): @frappe.whitelist() def reconcile_and_generate_data(self): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + # reconcile purchases and inward supplies if frappe.flags.in_install or frappe.flags.in_migrate: return @@ -229,46 +243,11 @@ def get_invoice_details(self, purchase_name, inward_supply_name): def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype): frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) - if not purchase_invoice_name or not inward_supply_name: - return - - purchases = [] - inward_supplies = [] - - # silently handle existing links - if isup_linked_with := frappe.db.get_value( - "GST Inward Supply", inward_supply_name, "link_name" - ): - self.set_reconciliation_status( - link_doctype, (isup_linked_with,), "Unreconciled" - ) - self._unlink_documents((inward_supply_name,)) - purchases.append(isup_linked_with) - - link_doc = { - "link_doctype": link_doctype, - "link_name": purchase_invoice_name, - } - if pur_linked_with := frappe.db.get_all( - "GST Inward Supply", link_doc, pluck="name" - ): - self._unlink_documents((pur_linked_with)) - inward_supplies.extend(pur_linked_with) - - link_doc["match_status"] = "Manual Match" - - # link documents - frappe.db.set_value( - "GST Inward Supply", - inward_supply_name, - link_doc, + purchases, inward_supplies = _link_documents( + purchase_invoice_name, inward_supply_name, link_doctype ) - purchases.append(purchase_invoice_name) - inward_supplies.append(inward_supply_name) - self.set_reconciliation_status( - link_doctype, (purchase_invoice_name,), "Match Found" - ) + set_reconciliation_status(link_doctype, (purchase_invoice_name,), "Match Found") return self.ReconciledData.get(purchases, inward_supplies) @@ -276,57 +255,9 @@ def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype def unlink_documents(self, data): frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) - data = frappe.parse_json(data) - inward_supplies = set() - purchases = set() - boe = set() - - for doc in data: - inward_supplies.add(doc.get("inward_supply_name")) - - purchase_doctype = doc.get("purchase_doctype") - if purchase_doctype == "Purchase Invoice": - purchases.add(doc.get("purchase_invoice_name")) - - elif purchase_doctype == "Bill of Entry": - boe.add(doc.get("purchase_invoice_name")) - - self.set_reconciliation_status("Purchase Invoice", purchases, "Unreconciled") - self.set_reconciliation_status("Bill of Entry", boe, "Unreconciled") - self._unlink_documents(inward_supplies) - - return self.ReconciledData.get(purchases.union(boe), inward_supplies) - - def set_reconciliation_status(self, doctype, names, status): - if not names: - return - - frappe.db.set_value( - doctype, {"name": ("in", names)}, "reconciliation_status", status - ) - - def _unlink_documents(self, inward_supplies): - if not inward_supplies: - return + purchases, inward_supplies = _unlink_documents(data) - GSTR2 = frappe.qb.DocType("GST Inward Supply") - ( - frappe.qb.update(GSTR2) - .set("link_doctype", "") - .set("link_name", "") - .set("match_status", "Unlinked") - .where(GSTR2.name.isin(inward_supplies)) - .run() - ) - - # Revert action performed - ( - frappe.qb.update(GSTR2) - .set("action", "No Action") - .where(GSTR2.name.isin(inward_supplies)) - .where(GSTR2.action.notin(("Ignore", "Pending"))) - .run() - ) + return self.ReconciledData.get(purchases, inward_supplies) @frappe.whitelist() def apply_action(self, data, action): @@ -361,8 +292,8 @@ def apply_action(self, data, action): "GST Inward Supply", {"name": ("in", inward_supplies)}, "action", action ) - self.set_reconciliation_status("Purchase Invoice", purchases, status) - self.set_reconciliation_status("Bill of Entry", boe, status) + set_reconciliation_status("Purchase Invoice", purchases, status) + set_reconciliation_status("Bill of Entry", boe, status) @frappe.whitelist() def get_link_options(self, doctype, filters): @@ -393,7 +324,7 @@ def get_purchase_invoice_options(self, filters): PI.name.notin(PurchaseInvoice.query_matched_purchase_invoice()) ) - return self._get_link_options(query.run(as_dict=True)) + return get_formatted_options(query.run(as_dict=True)) def get_inward_supply_options(self, filters): GSTR2 = frappe.qb.DocType("GST Inward Supply") @@ -411,7 +342,7 @@ def get_inward_supply_options(self, filters): if not filters.show_matched: query = query.where(IfNull(GSTR2.link_name, "") == "") - return self._get_link_options(query.run(as_dict=True)) + return get_formatted_options(query.run(as_dict=True)) def get_bill_of_entry_options(self, filters): BOE = frappe.qb.DocType("Bill of Entry") @@ -424,22 +355,7 @@ def get_bill_of_entry_options(self, filters): BOE.name.notin(BillOfEntry.query_matched_bill_of_entry()) ) - return self._get_link_options(query.run(as_dict=True)) - - def _get_link_options(self, data): - for row in data: - row.value = row.label = row.name - if not row.get("classification"): - row.classification = self.ReconciledData.guess_classification(row) - - row.description = ( - f"{row.bill_no}, {row.bill_date}, Taxable Amount: {row.taxable_value}" - ) - row.description += ( - f", Tax Amount: {BaseUtil.get_total_tax(row)}, {row.classification}" - ) - - return data + return get_formatted_options(query.run(as_dict=True)) def download_gstr( @@ -651,7 +567,7 @@ def download_gst_returns(self): def get_gst_categories(self): return [ category.value - for category in ACTIONS.values() + for category in GSTR_2A_ACTIONS.values() if getattr(self.gst_settings, "reconcile_for_" + category.value.lower()) ] diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py new file mode 100644 index 0000000000..fca48ae699 --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py @@ -0,0 +1,124 @@ +import frappe + +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + BaseUtil, + ReconciledData, +) + + +def link_documents(purchase_invoice_name, inward_supply_name, link_doctype): + purchases = [] + inward_supplies = [] + + if not purchase_invoice_name or not inward_supply_name: + return purchases, inward_supplies + + # silently handle existing links + if isup_linked_with := frappe.db.get_value( + "GST Inward Supply", inward_supply_name, "link_name" + ): + set_reconciliation_status(link_doctype, (isup_linked_with,), "Unreconciled") + _unlink_documents((inward_supply_name,)) + purchases.append(isup_linked_with) + + link_doc = { + "link_doctype": link_doctype, + "link_name": purchase_invoice_name, + } + if pur_linked_with := frappe.db.get_all( + "GST Inward Supply", link_doc, pluck="name" + ): + _unlink_documents((pur_linked_with)) + inward_supplies.extend(pur_linked_with) + + link_doc["match_status"] = "Manual Match" + + # link documents + frappe.db.set_value("GST Inward Supply", inward_supply_name, link_doc) + set_reconciliation_status(link_doctype, (purchase_invoice_name,), "Match Found") + + purchases.append(purchase_invoice_name) + inward_supplies.append(inward_supply_name) + + return purchases, inward_supplies + + +def unlink_documents(data): + data = frappe.parse_json(data) + inward_supplies = set() + purchases = set() + boe = set() + + for row in data: + inward_supplies.add(row.get("inward_supply_name")) + + purchase_doctype = row.get("purchase_doctype") + if purchase_doctype == "Purchase Invoice": + purchases.add(row.get("purchase_invoice_name")) + + elif purchase_doctype == "Bill of Entry": + boe.add(row.get("purchase_invoice_name")) + + set_reconciliation_status("Purchase Invoice", purchases, "Unreconciled") + set_reconciliation_status("Bill of Entry", boe, "Unreconciled") + _unlink_documents(inward_supplies) + + return purchases.union(boe), inward_supplies + + +def _unlink_documents(inward_supplies): + if not inward_supplies: + return + + GSTR2 = frappe.qb.DocType("GST Inward Supply") + ( + frappe.qb.update(GSTR2) + .set("link_doctype", "") + .set("link_name", "") + .set("match_status", "Unlinked") + .where(GSTR2.name.isin(inward_supplies)) + .run() + ) + + # Revert Purchase Reconciliation action performed + ( + frappe.qb.update(GSTR2) + .set("action", "No Action") + .where(GSTR2.name.isin(inward_supplies)) + .where(GSTR2.action.notin(("Ignore", "Pending"))) + .run() + ) + + # Revert IMS action performed + ( + frappe.qb.update(GSTR2) + .set("ims_action", "No Action") + .where(GSTR2.name.isin(inward_supplies)) + .where(GSTR2.ims_action == "Accepted") + .run() + ) + + +def get_formatted_options(data): + for row in data: + row.value = row.label = row.name + if not row.get("classification"): + row.classification = ReconciledData.guess_classification(row) + + row.description = ( + f"{row.bill_no}, {row.bill_date}, Taxable Amount: {row.taxable_value}" + ) + row.description += ( + f", Tax Amount: {BaseUtil.get_total_tax(row)}, {row.classification}" + ) + + return data + + +def set_reconciliation_status(doctype, names, status): + if not names: + return + + frappe.db.set_value( + doctype, {"name": ("in", names)}, "reconciliation_status", status + ) diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 5763d14143..974dd6cff5 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -116,6 +116,7 @@ def get_gstin_list(party, party_type="Company"): @frappe.whitelist() +@frappe.request_cache def get_party_for_gstin(gstin, party_type="Supplier"): if not gstin: return @@ -1040,3 +1041,29 @@ def is_outward_stock_entry(doc): and not doc.is_return ): return True + + +def create_notification( + message_content, document_type, document_name=None, request_id=None +): + # request_id shows failure response + if request_id and ( + doc_name := frappe.db.get_value( + "Integration Request", {"request_id": request_id} + ) + ): + document_type = "Integration Request" + document_name = doc_name + + notification = frappe.get_doc( + { + "doctype": "Notification Log", + "for_user": frappe.session.user, + "type": "Alert", + "document_type": document_type, + "document_name": document_name or document_type, + "subject": message_content.get("subject"), + "email_content": message_content.get("body"), + } + ) + notification.insert() diff --git a/india_compliance/gst_india/utils/gstr_2/__init__.py b/india_compliance/gst_india/utils/gstr_2/__init__.py index 01ad6d012b..8d373ad584 100644 --- a/india_compliance/gst_india/utils/gstr_2/__init__.py +++ b/india_compliance/gst_india/utils/gstr_2/__init__.py @@ -5,12 +5,19 @@ from frappe.query_builder.terms import Criterion from frappe.utils import cint -from india_compliance.gst_india.api_classes.taxpayer_returns import GSTR2aAPI, GSTR2bAPI +from india_compliance.gst_india.api_classes.taxpayer_returns import ( + IMSAPI, + GSTR2aAPI, + GSTR2bAPI, +) +from india_compliance.gst_india.doctype.gst_return_log.gst_return_log import ( + create_ims_return_log, +) from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import ( create_import_log, ) from india_compliance.gst_india.utils import get_party_for_gstin -from india_compliance.gst_india.utils.gstr_2 import gstr_2a, gstr_2b +from india_compliance.gst_india.utils.gstr_2 import gstr_2a, gstr_2b, ims from india_compliance.gst_india.utils.gstr_utils import ReturnType @@ -24,8 +31,14 @@ class GSTRCategory(Enum): IMPG = "IMPG" IMPGSEZ = "IMPGSEZ" + # IMS + B2BCN = "B2BCN" + B2BCNA = "B2BCNA" + B2BDN = "B2BDN" + B2BDNA = "B2BDNA" + -ACTIONS = { +GSTR_2A_ACTIONS = { "B2B": GSTRCategory.B2B, "B2BA": GSTRCategory.B2BA, "CDN": GSTRCategory.CDNR, @@ -35,16 +48,27 @@ class GSTRCategory(Enum): "IMPGSEZ": GSTRCategory.IMPGSEZ, } +IMS_ACTIONS = { + "B2B": GSTRCategory.B2B, + "B2BA": GSTRCategory.B2BA, + "CN": GSTRCategory.B2BCN, + "CNA": GSTRCategory.B2BCNA, + "DN": GSTRCategory.B2BDN, + "DNA": GSTRCategory.B2BDNA, +} + + GSTR_MODULES = { ReturnType.GSTR2A.value: gstr_2a, ReturnType.GSTR2B.value: gstr_2b, + ReturnType.IMS.value: ims, } IMPORT_CATEGORY = ("IMPG", "IMPGSEZ") def download_gstr_2a(gstin, return_periods, gst_categories=None): - total_expected_requests = len(return_periods) * len(ACTIONS) + total_expected_requests = len(return_periods) * len(GSTR_2A_ACTIONS) requests_made = 0 queued_message = False @@ -55,7 +79,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None): json_data = frappe._dict({"gstin": gstin, "fp": return_period}) has_data = False - for action, category in ACTIONS.items(): + for action, category in GSTR_2A_ACTIONS.items(): requests_made += 1 frappe.publish_realtime( @@ -115,7 +139,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None): save_gstr_2a(gstin, return_period, json_data) if queued_message: - publish_queued_message() + publish_2a_2b_queued_message() if not has_data: end_transaction_progress(return_period) @@ -183,12 +207,58 @@ def download_gstr_2b(gstin, return_periods): save_gstr_2b(gstin, return_period, response) if queued_message: - publish_queued_message() + publish_2a_2b_queued_message() if not has_data: end_transaction_progress(return_period) +def download_ims_invoices(gstin, for_upload=False): + api = IMSAPI(gstin) + has_queued_invoices = False + has_non_queued_invoices = False + json_data = {} + + for action, category in IMS_ACTIONS.items(): + response = api.get_data(action) + category = category.value + + if response.error_type == "no_docs_found": + continue + + # Queued + if response.token: + create_import_log( + gstin, + "IMS", + "ALL", + classification=category, + request_id=response.token, + retry_after_mins=cint(response.est), + ) + has_queued_invoices = True + continue + + json_data[category.lower()] = response.get(category.lower()) + has_non_queued_invoices = True + + save_ims_invoices(gstin, None, json_data) + + create_ims_return_log(gstin) + + if has_queued_invoices: + publish_ims_queued_message(for_upload) + + if has_non_queued_invoices: + frappe.publish_realtime( + "ims_download_completed", + message={"message": _("Downloaded Invoices successfully")}, + user=frappe.session.user, + ) + + return has_queued_invoices + + def save_gstr_2a(gstin, return_period, json_data): return_type = ReturnType.GSTR2A if ( @@ -204,7 +274,7 @@ def save_gstr_2a(gstin, return_period, json_data): title=_("Invalid Response Received."), ) - for action, category in ACTIONS.items(): + for action, category in GSTR_2A_ACTIONS.items(): if action.lower() not in json_data: continue @@ -241,6 +311,10 @@ def save_gstr_2b(gstin, return_period, json_data): update_import_history(return_period) +def save_ims_invoices(gstin, return_period, json_data): + save_gstr(gstin, ReturnType.IMS, return_period, json_data) + + def save_gstr( gstin, return_type: ReturnType, return_period, json_data, gen_date_2b=None ): @@ -253,7 +327,10 @@ def save_gstr( company = get_party_for_gstin(gstin, "Company") for category in GSTRCategory: - gstr = get_data_handler(return_type.value, category) + gstr = get_data_handler(return_type.value, category.value) + if not gstr: + continue + gstr(company, gstin, return_period, json_data, gen_date_2b).create_transactions( category, json_data.get(category.value.lower()), @@ -261,8 +338,8 @@ def save_gstr( def get_data_handler(return_type, category): - class_name = return_type + category.value - return getattr(GSTR_MODULES[return_type], class_name) + class_name = return_type + category + return getattr(GSTR_MODULES[return_type], class_name, None) def update_import_history(return_periods): @@ -300,7 +377,7 @@ def _download_gstr_2a(gstin, return_period, json_data): save_gstr_2a(gstin, return_period, json_data) -def publish_queued_message(): +def publish_2a_2b_queued_message(): frappe.publish_realtime( "gstr_2a_2b_download_message", { @@ -315,6 +392,25 @@ def publish_queued_message(): ) +def publish_ims_queued_message(for_upload): + message = _( + "Some categories are queued for download at GSTN as there may be large data." + " We will retry downloading every few minutes until it succeeds." + ) + if for_upload: + message = _( + "Some categories are queued for download at GSTN as there may be large data." + " We will retry downloading every few minutes until it succeeds.

" + " Please try uploading the data again after a few minutes." + ) + + frappe.publish_realtime( + "ims_download_queued", + message={"message": message}, + user=frappe.session.user, + ) + + def end_transaction_progress(return_period): """ For last period, set progress to 100% if no data is found diff --git a/india_compliance/gst_india/utils/gstr_2/gstr.py b/india_compliance/gst_india/utils/gstr_2/gstr.py index ce284940b1..e77ff2bb9e 100644 --- a/india_compliance/gst_india/utils/gstr_2/gstr.py +++ b/india_compliance/gst_india/utils/gstr_2/gstr.py @@ -1,6 +1,6 @@ import frappe -from india_compliance.gst_india.constants import STATE_NUMBERS +from india_compliance.gst_india.constants import GST_CATEGORY_MAP, STATE_NUMBERS from india_compliance.gst_india.doctype.gst_inward_supply.gst_inward_supply import ( create_inward_supply, ) @@ -19,13 +19,7 @@ class GSTR: { "Y_N_to_check": {"Y": 1, "N": 0}, "yes_no": {"Y": "Yes", "N": "No"}, - "gst_category": { - "R": "Regular", - "SEZWP": "SEZ supplies with payment of tax", - "SEZWOP": "SEZ supplies with out payment of tax", - "DE": "Deemed exports", - "CBW": "Intra-State Supplies attracting IGST", - }, + "gst_category": GST_CATEGORY_MAP, "states": {value: f"{value}-{key}" for key, value in STATE_NUMBERS.items()}, "note_type": {"C": "Credit Note", "D": "Debit Note"}, "isd_type_2a": {"ISDCN": "ISD Credit Note", "ISD": "ISD Invoice"}, diff --git a/india_compliance/gst_india/utils/gstr_2/ims.py b/india_compliance/gst_india/utils/gstr_2/ims.py new file mode 100644 index 0000000000..13c0bed85f --- /dev/null +++ b/india_compliance/gst_india/utils/gstr_2/ims.py @@ -0,0 +1,319 @@ +import frappe +from frappe.utils.data import format_date + +from india_compliance.gst_india.constants import ( + ACTION_MAP, + GST_CATEGORY_MAP, + STATE_NUMBERS, +) +from india_compliance.gst_india.doctype.gst_inward_supply.gst_inward_supply import ( + create_inward_supply, +) +from india_compliance.gst_india.doctype.gst_inward_supply.gst_inward_supply import ( + update_previous_ims_action as _update_previous_ims_action, +) +from india_compliance.gst_india.utils import parse_datetime +from india_compliance.gst_india.utils.gstr_2.gstr import get_mapped_value + +CLASSIFICATION_MAP = { + "B2B": ["B2B", "Invoice"], + "B2BA": ["B2BA", "Invoice"], + "B2BCN": ["CDNR", "Credit Note"], + "B2BCNA": ["CDNRA", "Credit Note"], + "B2BDN": ["CDNR", "Debit Note"], + "B2BDNA": ["CDNRA", "Debit Note"], +} + + +class IMS: + VALUE_MAPS = frappe._dict( + { + "states": {value: f"{value}-{key}" for key, value in STATE_NUMBERS.items()}, + "reverse_states": STATE_NUMBERS, + "action": ACTION_MAP, + "reverse_action": {v: k for k, v in ACTION_MAP.items()}, + "gst_category": GST_CATEGORY_MAP, + "reverse_gst_category": {v: k for k, v in GST_CATEGORY_MAP.items()}, + "classification": CLASSIFICATION_MAP, + } + ) + + def __init__(self, company=None, gstin=None, *args): + self.company_gstin = gstin + self.company = company + self.existing_transactions = self.get_existing_transactions() + + def create_transactions(self, category, invoices): + self.reset_previous_ims_action() + + if not invoices: + return + + transactions = self.get_all_transactions(invoices) + + for transaction in transactions: + create_inward_supply(transaction) + + if transaction.get("unique_key") in self.existing_transactions: + self.existing_transactions.pop(transaction.get("unique_key")) + + self.handle_missing_transactions() + + def get_all_transactions(self, invoices): + transactions = [] + for invoice in invoices: + invoice = frappe._dict(invoice) + transactions.append(self.get_transaction(invoice)) + + return transactions + + def update_previous_ims_action(self, uploaded_invoices, error_invoices): + errors = set() + + for supplier in error_invoices: + for invoice in supplier.get("inv"): + + # same key across categories + errors.add(f"{invoice.get('inum')}_{supplier.get('stin')}") + + for invoice in uploaded_invoices: + invoice = self.get_transaction(frappe._dict(invoice)) + + # different keys across categories + if f"{invoice.get('bill_no')}_{invoice.get('supplier_gstin')}" in errors: + continue + + _update_previous_ims_action(invoice) + + def get_transaction(self, invoice): + transaction = frappe._dict( + **self.convert_data_to_internal_format(invoice), + **self.get_invoice_details(invoice), + ) + + transaction["unique_key"] = ( + f"{transaction.get('supplier_gstin', '')}-{transaction.get('bill_no', '')}" + ) + + return transaction + + def convert_data_to_internal_format(self, invoice): + return { + "supplier_gstin": invoice.stin, + "sup_return_period": invoice.rtnprd, + "supply_type": get_mapped_value( + invoice.inv_typ, self.VALUE_MAPS.gst_category + ), + "place_of_supply": get_mapped_value(invoice.pos, self.VALUE_MAPS.states), + "document_value": invoice.val, + "company": self.company, + "company_gstin": self.company_gstin, + "is_pending_action_allowed": invoice.ispendactblocked == "N", + "previous_ims_action": get_mapped_value( + invoice.action, self.VALUE_MAPS.action + ), + "is_downloaded_from_ims": 1, + "is_supplier_return_filed": 0 if invoice.srcfilstatus == "Not Filed" else 1, + "supplier_return_form": invoice.srcform, + "cgst": invoice.camt, + "sgst": invoice.samt, + "igst": invoice.iamt, + "cess": invoice.cess, + "taxable_value": invoice.txval, + } + + def convert_data_to_gov_format(self, invoice): + data = { + "stin": invoice.supplier_gstin, + "inv_typ": get_mapped_value( + invoice.supply_type, self.VALUE_MAPS.reverse_gst_category + ), + "srcform": invoice.supplier_return_form, + "rtnprd": invoice.sup_return_period, + "val": invoice.document_value, + "pos": get_mapped_value( + invoice.place_of_supply.split("-")[1], self.VALUE_MAPS.reverse_states + ), + "prev_status": get_mapped_value( + invoice.previous_ims_action, self.VALUE_MAPS.reverse_action + ), + "iamt": invoice.igst, + "camt": invoice.cgst, + "samt": invoice.sgst, + "cess": invoice.cess, + "txval": invoice.taxable_value, + } + + if invoice.ims_action != "No Action": + data["action"] = get_mapped_value( + invoice.ims_action, self.VALUE_MAPS.reverse_action + ) + + return data + + def get_existing_transactions(self): + category, doc_type = get_mapped_value( + self.ims_category(), self.VALUE_MAPS.classification + ) + + inward_supply = frappe.qb.DocType("GST Inward Supply") + existing_transactions = ( + frappe.qb.from_(inward_supply) + .select( + inward_supply.name, inward_supply.supplier_gstin, inward_supply.bill_no + ) + .where(inward_supply.is_downloaded_from_2b == 0) + .where(inward_supply.is_downloaded_from_2a == 0) + .where(inward_supply.is_downloaded_from_ims == 1) + .where(inward_supply.is_supplier_return_filed == 0) + .where(inward_supply.classification == category) + .where(inward_supply.doc_type == doc_type) + .where(inward_supply.company_gstin == self.company_gstin) + .run(as_dict=True) + ) + + return { + f"{transaction.get('supplier_gstin', '')}-{transaction.get('bill_no', '')}": transaction.get( + "name" + ) + for transaction in existing_transactions + } + + def handle_missing_transactions(self): + if not self.existing_transactions: + return + + for inward_supply_name in self.existing_transactions.values(): + frappe.delete_doc("GST Inward Supply", inward_supply_name) + + def reset_previous_ims_action(self): + category, doc_type = get_mapped_value( + self.ims_category(), self.VALUE_MAPS.classification + ) + inward_supply = frappe.qb.DocType("GST Inward Supply") + + ( + frappe.qb.update(inward_supply) + .set(inward_supply.previous_ims_action, "") + .where(inward_supply.classification == category) + .where(inward_supply.doc_type == doc_type) + .where(inward_supply.company_gstin == self.company_gstin) + .run() + ) + + def ims_category(self): + return type(self).__name__.removeprefix("IMS") + + +class IMSB2B(IMS): + def get_invoice_details(self, invoice): + return { + "bill_no": invoice.inum, + "bill_date": parse_datetime(invoice.idt, day_first=True), + "classification": "B2B", + "doc_type": "Invoice", + } + + def get_category_details(self, invoice): + return { + "inum": invoice.bill_no, + "idt": format_date(invoice.bill_date, "dd-mm-yyyy"), + } + + +class IMSB2BA(IMSB2B): + def get_invoice_details(self, invoice): + invoice_details = super().get_invoice_details(invoice) + invoice_details.update( + { + "original_bill_no": invoice.oinum, + "original_bill_date": parse_datetime(invoice.oidt, day_first=True), + "is_amended": True, + "classification": "B2BA", + } + ) + return invoice_details + + def get_category_details(self, invoice): + invoice_details = super().get_category_details(invoice) + invoice_details.update( + { + "oinum": invoice.original_bill_no, + "oidt": format_date(invoice.original_bill_date, "dd-mm-yyyy"), + } + ) + return invoice_details + + +class IMSB2BDN(IMSB2B): + def get_invoice_details(self, invoice): + return { + "bill_no": invoice.nt_num, + "bill_date": parse_datetime(invoice.nt_dt, day_first=True), + "classification": "CDNR", + "doc_type": "Debit Note", + } + + def get_category_details(self, invoice): + return { + "nt_num": invoice.bill_no, + "nt_dt": format_date(invoice.bill_date, "dd-mm-yyyy"), + } + + +class IMSB2BDNA(IMSB2BDN): + def get_invoice_details(self, invoice): + invoice_details = super().get_invoice_details(invoice) + invoice_details.update( + { + "original_bill_no": invoice.ont_num, + "original_bill_date": parse_datetime(invoice.ont_dt, day_first=True), + "is_amended": True, + "original_doc_type": "Debit Note", + "classification": "CDNRA", + } + ) + return invoice_details + + def get_category_details(self, invoice): + invoice_details = super().get_category_details(invoice) + invoice_details.update( + { + "ont_num": invoice.original_bill_no, + "ont_dt": format_date(invoice.original_bill_date, "dd-mm-yyyy"), + } + ) + return invoice_details + + +class IMSB2BCN(IMSB2BDN): + def get_invoice_details(self, invoice): + invoice_details = super().get_invoice_details(invoice) + invoice_details.update( + { + "doc_type": "Credit Note", + } + ) + return invoice_details + + +class IMSB2BCNA(IMSB2BDNA): + def get_invoice_details(self, invoice): + invoice_details = super().get_invoice_details(invoice) + invoice_details.update( + { + "doc_type": "Credit Note", + "original_doc_type": "Credit Note", + } + ) + return invoice_details + + def get_category_details(self, invoice): + invoice_details = super().get_category_details(invoice) + invoice_details.update( + { + "ont_num": invoice.original_bill_no, + "ont_dt": format_date(invoice.original_bill_date, "dd-mm-yyyy"), + } + ) + return invoice_details diff --git a/india_compliance/gst_india/utils/gstr_2/test_ims.py b/india_compliance/gst_india/utils/gstr_2/test_ims.py new file mode 100644 index 0000000000..75c542b58e --- /dev/null +++ b/india_compliance/gst_india/utils/gstr_2/test_ims.py @@ -0,0 +1,215 @@ +from datetime import date + +import frappe +from frappe import parse_json, read_file +from frappe.tests import IntegrationTestCase + +from india_compliance.gst_india.utils import get_data_file_path +from india_compliance.gst_india.utils.gstr_2 import save_ims_invoices + + +class TestIMS(IntegrationTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.gstin = "24AAQCA8719H1ZC" + cls.doctype = "GST Inward Supply" + cls.test_data = parse_json(read_file(get_data_file_path("test_ims.json"))) + + save_ims_invoices(cls.gstin, "ALL", cls.test_data) + + def get_doc(self, category, doc_type): + docname = frappe.get_value( + self.doctype, + { + "company_gstin": self.gstin, + "classification": category, + "doc_type": doc_type, + }, + ) + self.assertIsNotNone(docname) + return frappe.get_doc(self.doctype, docname) + + def test_ims_b2b(self): + doc = self.get_doc("B2B", "Invoice") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_date": date(2023, 1, 23), + "bill_no": "b1", + "doc_type": "Invoice", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "B2B", + "place_of_supply": "24-Gujarat", + "document_value": 1000, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 0, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 100, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) + + def test_ims_b2ba(self): + doc = self.get_doc("B2BA", "Invoice") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_date": date(2023, 1, 23), + "bill_no": "b1a", + "doc_type": "Invoice", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "B2BA", + "place_of_supply": "07-Delhi", + "original_bill_no": "ab2", + "original_bill_date": date(2023, 2, 24), + "is_amended": True, + "document_value": 1000, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 0, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 100, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) + + def test_ims_dn(self): + doc = self.get_doc("CDNR", "Debit Note") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_date": date(2023, 2, 24), + "bill_no": "dn2", + "doc_type": "Debit Note", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "CDNR", + "place_of_supply": "07-Delhi", + "document_value": 1000.1, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 1, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 1000.1, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) + + def test_ims_dna(self): + doc = self.get_doc("CDNRA", "Debit Note") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_no": "dna2", + "bill_date": date(2023, 2, 24), + "original_bill_no": "dn2", + "original_bill_date": date(2023, 2, 24), + "doc_type": "Debit Note", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "CDNRA", + "place_of_supply": "07-Delhi", + "is_amended": True, + "document_value": 1000.1, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 1, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 1000.1, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) + + def test_ims_cn(self): + doc = self.get_doc("CDNR", "Credit Note") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_date": date(2023, 2, 24), + "bill_no": "cn2", + "doc_type": "Credit Note", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "CDNR", + "place_of_supply": "07-Delhi", + "document_value": 1000.1, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 0, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 1000.1, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) + + def test_ims_cna(self): + doc = self.get_doc("CDNRA", "Credit Note") + print(doc.as_dict()) + self.assertDocumentEqual( + { + "bill_no": "cna2", + "bill_date": date(2023, 2, 24), + "original_bill_no": "cn2", + "original_bill_date": date(2023, 2, 24), + "doc_type": "Credit Note", + "supplier_gstin": "24MAYAS0100J1JD", + "supply_type": "Regular", + "classification": "CDNRA", + "place_of_supply": "07-Delhi", + "is_amended": True, + "document_value": 1000.1, + "is_downloaded_from_ims": 1, + "ims_action": "Accepted", + "previous_ims_action": "Accepted", + "is_pending_action_allowed": 1, + "is_supplier_return_filed": 0, + "supplier_return_form": "R1", + "sup_return_period": "012023", + "taxable_value": 1000.1, + "igst": 20, + "cgst": 20, + "sgst": 20, + "cess": 0, + }, + doc, + ) diff --git a/india_compliance/gst_india/utils/gstr_utils.py b/india_compliance/gst_india/utils/gstr_utils.py index 67ae50e802..20a9747f83 100644 --- a/india_compliance/gst_india/utils/gstr_utils.py +++ b/india_compliance/gst_india/utils/gstr_utils.py @@ -6,11 +6,12 @@ TaxpayerBaseAPI, otp_handler, ) -from india_compliance.gst_india.api_classes.taxpayer_returns import ReturnsAPI +from india_compliance.gst_india.api_classes.taxpayer_returns import IMSAPI, ReturnsAPI from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import ( create_import_log, toggle_scheduled_jobs, ) +from india_compliance.gst_india.utils import create_notification from india_compliance.gst_india.utils.gstr_1.gstr_1_download import ( save_gstr_1_filed_data, save_gstr_1_unfiled_data, @@ -22,6 +23,7 @@ class ReturnType(Enum): GSTR2B = "GSTR2b" GSTR1 = "GSTR1" UnfiledGSTR1 = "Unfiled GSTR1" + IMS = "IMS" @frappe.whitelist() @@ -71,17 +73,30 @@ def download_queued_request(): def _download_queued_request(doc): - from india_compliance.gst_india.utils.gstr_2 import _download_gstr_2a, save_gstr_2b + from india_compliance.gst_india.utils.gstr_2 import ( + _download_gstr_2a, + save_gstr_2b, + save_ims_invoices, + ) GSTR_FUNCTIONS = { ReturnType.GSTR2A.value: _download_gstr_2a, ReturnType.GSTR2B.value: save_gstr_2b, ReturnType.GSTR1.value: save_gstr_1_filed_data, ReturnType.UnfiledGSTR1.value: save_gstr_1_unfiled_data, + ReturnType.IMS.value: save_ims_invoices, + } + + API_CLASS = { + ReturnType.GSTR2A.value: ReturnsAPI, + ReturnType.GSTR2B.value: ReturnsAPI, + ReturnType.GSTR1.value: ReturnsAPI, + ReturnType.UnfiledGSTR1.value: ReturnsAPI, + ReturnType.IMS.value: IMSAPI, } try: - api = ReturnsAPI(doc.gstin) + api = API_CLASS[doc.return_type](doc.gstin) response = api.download_files( doc.return_period, doc.request_id, @@ -108,3 +123,31 @@ def _download_queued_request(doc): frappe.db.set_value("GSTR Import Log", doc.name, "request_id", None) GSTR_FUNCTIONS[doc.return_type](doc.gstin, doc.return_period, response) + + +def publish_action_status_notification( + return_type, return_period, request_type, status_cd, gstin, request_id=None +): + status_message_map = { + "P": f"Success: {return_type} data {request_type} for GSTIN {gstin} and return period {return_period}", + "PE": f"Partial Success: {return_type} data {request_type} for GSTIN {gstin} and return period {return_period}", + "ER": f"Error: {return_type} data {request_type} for GSTIN {gstin} and return period {return_period}", + } + + message_content = { + "subject": status_message_map.get(status_cd), + "body": status_message_map.get(status_cd), + } + + if return_type == "GSTR-1": + document_type = "GSTR-1 Beta" + elif return_type == "IMS": + document_type = "GST Invoice Management System" + + return frappe.enqueue( + create_notification, + queue="long", + message_content=message_content, + document_type=document_type, + request_id=request_id, + ) diff --git a/india_compliance/public/js/components/data_table_manager.js b/india_compliance/public/js/components/data_table_manager.js index b56f13f699..fe88a1eb31 100644 --- a/india_compliance/public/js/components/data_table_manager.js +++ b/india_compliance/public/js/components/data_table_manager.js @@ -25,8 +25,7 @@ india_compliance.DataTableManager = class DataTableManager { refresh(data, columns, noDataMessage) { this.data = data; - if (noDataMessage) - this.datatable.options.noDataMessage = noDataMessage; + if (noDataMessage) this.datatable.options.noDataMessage = noDataMessage; this.datatable.refresh(data, columns); } @@ -81,7 +80,13 @@ india_compliance.DataTableManager = class DataTableManager { value = column._value(value, column, data); } - return frappe.format(value, column, { always_show_decimals: true }, data); + value = frappe.format(value, column, { always_show_decimals: true }, data); + + if (column.post_format) { + value = column._after_format(value, column, data); + } + + return value; }; return { diff --git a/india_compliance/public/js/components/filter_group.js b/india_compliance/public/js/components/filter_group.js index 3ea3b45bc9..7cee192719 100644 --- a/india_compliance/public/js/components/filter_group.js +++ b/india_compliance/public/js/components/filter_group.js @@ -103,6 +103,8 @@ india_compliance.FilterGroup = class FilterGroup extends frappe.ui.FilterGroup { this.filters = this.filters.filter(f => { let f_value = f.get_value(); + if (filter_value.length === 2) f_value = f_value.slice(0, 2); + return !frappe.utils.arrays_equal( f_value.slice(0, 4), filter_value.slice(0, 4) diff --git a/india_compliance/public/js/ims.bundle.js b/india_compliance/public/js/ims.bundle.js new file mode 100644 index 0000000000..ce7feb6f6d --- /dev/null +++ b/india_compliance/public/js/ims.bundle.js @@ -0,0 +1,5 @@ +import "./components/data_table_manager"; +import "./components/number_card"; +import "./components/set_gstin_options"; +import "./components/filter_group"; +import "./reconciliation_components/actions"; diff --git a/india_compliance/public/js/india_compliance.bundle.js b/india_compliance/public/js/india_compliance.bundle.js index 580889b4d8..521667f6bf 100644 --- a/india_compliance/public/js/india_compliance.bundle.js +++ b/india_compliance/public/js/india_compliance.bundle.js @@ -9,3 +9,4 @@ import "./quick_info_popover"; import "./custom_number_card"; import "./taxes_controller"; import "./help_links"; +import "./reconciliation_components/tabs"; diff --git a/india_compliance/public/js/purchase_reconciliation_tool.bundle.js b/india_compliance/public/js/purchase_reconciliation_tool.bundle.js index e236f74221..0dcfc31b55 100644 --- a/india_compliance/public/js/purchase_reconciliation_tool.bundle.js +++ b/india_compliance/public/js/purchase_reconciliation_tool.bundle.js @@ -2,3 +2,4 @@ import "./components/data_table_manager"; import "./components/filter_group"; import "./components/number_card"; import "./components/set_gstin_options"; +import "./reconciliation_components/actions"; diff --git a/india_compliance/public/js/reconciliation_components/actions.js b/india_compliance/public/js/reconciliation_components/actions.js new file mode 100644 index 0000000000..c2b5511371 --- /dev/null +++ b/india_compliance/public/js/reconciliation_components/actions.js @@ -0,0 +1,155 @@ +frappe.provide("reconciliation"); + +Object.assign(reconciliation, { + get_unlinked_docs(selected_rows) { + const unlinked_docs = new Set(); + selected_rows.forEach(row => { + unlinked_docs.add(row.purchase_invoice_name); + unlinked_docs.add(row.inward_supply_name); + }); + + return unlinked_docs; + }, + + async unlink_documents(frm, selected_rows) { + if (frm.get_active_tab()?.df.fieldname != "invoice_tab") return; + const _class = frm.reconciliation_tabs; + const { invoice_tab } = _class.tabs; + if (!selected_rows) selected_rows = invoice_tab.datatable.get_checked_items(); + + if (!selected_rows.length) + return frappe.show_alert({ + message: __("Please select rows to unlink"), + indicator: "red", + }); + + // validate selected rows + selected_rows.forEach(row => { + if (row.match_status.includes("Missing")) + frappe.throw( + __( + "You have selected rows where no match is available. Please remove them before unlinking." + ) + ); + }); + + // unlink documents & update table + const { message: r } = await frm._call("unlink_documents", { + data: selected_rows, + }); + + const unlinked_docs = reconciliation.get_unlinked_docs(selected_rows); + + const new_data = _class.data.filter( + row => + !( + unlinked_docs.has(row.purchase_invoice_name) || + unlinked_docs.has(row.inward_supply_name) + ) + ); + + new_data.push(...r); + _class.refresh(new_data); + reconciliation.after_successful_action(invoice_tab); + }, + + async link_documents( + frm, + purchase_invoice_name, + inward_supply_name, + link_doctype, + alert = true + ) { + if (frm.get_active_tab()?.df.fieldname != "invoice_tab") return; + + // link documents & update data. + const { message: r } = await frm._call("link_documents", { + purchase_invoice_name, + inward_supply_name, + link_doctype, + }); + + const _class = frm.reconciliation_tabs; + const new_data = _class.data.filter( + row => + !( + row.purchase_invoice_name == purchase_invoice_name || + row.inward_supply_name == inward_supply_name + ) + ); + + new_data.push(...r); + + _class.refresh(new_data); + if (alert) reconciliation.after_successful_action(_class.tabs.invoice_tab); + }, + + async create_new_purchase_invoice(row, company, company_gstin, source_doc) { + if (row.match_status != "Missing in PI") return; + const doc = row._inward_supply; + + const { message: supplier } = await frappe.call({ + method: "india_compliance.gst_india.utils.get_party_for_gstin", + args: { + gstin: row.supplier_gstin, + }, + }); + + let company_address; + await frappe.model.get_value( + "Address", + { gstin: company_gstin, is_your_company_address: 1 }, + "name", + r => (company_address = r.name) + ); + + frappe.route_hooks.after_load = frm => { + function _set_value(values) { + for (const key in values) { + if (values[key] == frm.doc[key]) continue; + frm.set_value(key, values[key]); + } + } + + const values = { + company: company, + bill_no: doc.bill_no, + bill_date: doc.bill_date, + is_reverse_charge: ["Yes", 1].includes(doc.is_reverse_charge) ? 1 : 0, + }; + + _set_value({ + ...values, + supplier: supplier, + shipping_address: company_address, + billing_address: company_address, + }); + + // validated this on save + frm._inward_supply = { + ...values, + name: row.inward_supply_name, + company_gstin: company_gstin, + inward_supply: row.inward_supply, + supplier_gstin: row.supplier_gstin, + place_of_supply: doc.place_of_supply, + cgst: doc.cgst, + sgst: doc.sgst, + igst: doc.igst, + cess: doc.cess, + taxable_value: doc.taxable_value, + source_doc, + }; + }; + + frappe.new_doc("Purchase Invoice"); + }, + + after_successful_action(tab) { + if (tab) tab.datatable.clear_checked_items(); + frappe.show_alert({ + message: "Action applied successfully", + indicator: "green", + }); + }, +}); diff --git a/india_compliance/public/js/reconciliation_components/tabs.js b/india_compliance/public/js/reconciliation_components/tabs.js new file mode 100644 index 0000000000..c36a3a4da0 --- /dev/null +++ b/india_compliance/public/js/reconciliation_components/tabs.js @@ -0,0 +1,412 @@ +frappe.provide("reconciliation"); + +reconciliation.reconciliation_tabs = class ReconciliationTabs { + constructor(frm, tabs, data_field) { + this.frm = frm; + this.data = []; + this._tabs = tabs; + this.$wrapper = frm.get_field(data_field).$wrapper; + + this.render_tab_group(); + this.setup_filter_button(frm.doctype); + } + + render_data(data) { + this.data = data; + this.filtered_data = data; + + // clear filters + this.filter_group.filter_x_button.click(); + this.render_data_tables(); + } + + refresh(data) { + if (data) { + this.data = data; + this.refresh_filter_fields(); + } + + this.apply_filters(!!data); + + // data unchanged! + if (this.rendered_data == this.filtered_data) return; + + this._tabs.forEach(tab => { + this.tabs[`${tab}_tab`].datatable?.refresh(this[`get_${tab}_data`]()); + }); + + this.rendered_data = this.filtered_data; + } + + render_tab_group() { + const fields = this.get_tab_group_fields(); + + this.tab_group = new frappe.ui.FieldGroup({ + fields, + body: this.$wrapper, + frm: this.frm, + }); + + this.tab_group.make(); + + // make tabs_dict for easy access + this.tabs = Object.fromEntries( + this.tab_group.tabs.map(tab => [tab.df.fieldname, tab]) + ); + } + + get_tab_group_fields() { + return []; + } + + setup_filter_button(doctype) { + this.filter_group = new india_compliance.FilterGroup({ + doctype, + parent: this.$wrapper.find(".form-tabs-list"), + filter_options: { + fieldname: "supplier_name", + filter_fields: this.get_filter_fields(), + }, + on_change: () => { + this.refresh(); + }, + }); + } + + get_filter_fields() { + return []; + } + + apply_filters(force, supplier_filter) { + const has_filters = this.filter_group.filters.length > 0 || supplier_filter; + if (!has_filters) { + this.filters = null; + this.filtered_data = this.data; + return; + } + + let filters = this.filter_group.get_filters(); + if (supplier_filter) filters.push(supplier_filter); + if (!force && this.filters === filters) return; + + this.filters = filters; + this.filtered_data = this.data.filter(row => { + return filters.every(filter => + india_compliance.FILTER_OPERATORS[filter[2]]( + filter[3] || "", + row[filter[1]] || "" + ) + ); + }); + } + + refresh_filter_fields() { + this.filter_group.filter_options.filter_fields = this.get_filter_fields(); + } + + get_autocomplete_options(field) { + const options = []; + this.data.forEach(row => { + if (row[field] && !options.includes(row[field])) options.push(row[field]); + }); + return options; + } + + render_data_tables() { + this._tabs.forEach(tab => { + this.tabs[`${tab}_tab`].datatable = new india_compliance.DataTableManager({ + $wrapper: this.tab_group.get_field(`${tab}_data`).$wrapper, + columns: this[`get_${tab}_columns`](), + data: this[`get_${tab}_data`](), + options: { + cellHeight: 55, + }, + }); + }); + this.set_listeners(); + } + + get_supplier_name_gstin(row) { + return ` + ${row.supplier_name} +
+ + ${row.supplier_gstin || ""} + + `; + } +}; + +reconciliation.detail_view_dialog = class DetailViewDialog { + table_fields = [ + "name", + "bill_no", + "bill_date", + "taxable_value", + "cgst", + "sgst", + "igst", + "cess", + "is_reverse_charge", + "place_of_supply", + ]; + + constructor(frm, row) { + this.frm = frm; + this.row = row; + this.render_dialog(); + } + + async render_dialog() { + await this.get_invoice_details(); + this.process_data(); + this.init_dialog(); + this.setup_actions(); + this.render_html(); + this.dialog.show(); + } + + async get_invoice_details() { + const { message } = await this.frm._call("get_invoice_details", { + purchase_name: this.row.purchase_invoice_name, + inward_supply_name: this.row.inward_supply_name, + }); + + this.data = message; + } + + process_data() { + for (let key of ["_purchase_invoice", "_inward_supply"]) { + const doc = this.data[key]; + if (!doc) continue; + + this.table_fields.forEach(field => { + if (field == "is_reverse_charge" && doc[field] != undefined) + doc[field] = doc[field] ? "Yes" : "No"; + }); + } + } + + init_dialog() { + const supplier_details = ` +
${this.row.supplier_name} + ${this.row.supplier_gstin ? ` (${this.row.supplier_gstin})` : ""} +
+ `; + + this.dialog = new frappe.ui.Dialog({ + title: `Detail View (${this.row.classification})`, + fields: [ + ...this._get_document_link_fields(), + { + fieldtype: "HTML", + fieldname: "supplier_details", + options: supplier_details, + }, + { + fieldtype: "HTML", + fieldname: "diff_cards", + }, + { + fieldtype: "HTML", + fieldname: "detail_table", + }, + ], + }); + this.set_link_options(); + } + + _get_document_link_fields() { + this._set_missing_doctype(); + if (!this.missing_doctype) return []; + + return [ + { + label: "GSTIN", + fieldtype: "Data", + fieldname: "supplier_gstin", + default: this.row.supplier_gstin, + onchange: () => this.set_link_options(), + }, + { + label: "Date Range", + fieldtype: "DateRange", + fieldname: "date_range", + default: this._get_default_date_range(), + onchange: () => this.set_link_options(), + }, + { + fieldtype: "Column Break", + }, + { + label: "Document Type", + fieldtype: "Autocomplete", + fieldname: "doctype", + default: this.missing_doctype, + options: this.doctype_options, + read_only_depends_on: this.doctype_options.length === 1, + + onchange: () => { + const doctype = this.dialog.get_value("doctype"); + this.dialog + .get_field("show_matched") + .set_label(`Show matched options for linking ${doctype}`); + }, + }, + { + label: `Document Name`, + fieldtype: "Autocomplete", + fieldname: "link_with", + onchange: () => this.refresh_data(), + }, + { + label: `Show matched options for linking ${this.missing_doctype}`, + fieldtype: "Check", + fieldname: "show_matched", + onchange: () => this.set_link_options(), + }, + { + fieldtype: "Section Break", + }, + ]; + } + + async set_link_options(method) { + if (!this.dialog.get_value("doctype")) return; + + this.filters = { + supplier_gstin: this.dialog.get_value("supplier_gstin"), + bill_from_date: this.dialog.get_value("date_range")[0], + bill_to_date: this.dialog.get_value("date_range")[1], + show_matched: this.dialog.get_value("show_matched"), + purchase_doctype: this.data.purchase_doctype, + }; + + const { message } = await this.frm._call("get_link_options", { + doctype: this.dialog.get_value("doctype"), + filters: this.filters, + }); + + this.dialog.get_field("link_with").set_data(message); + } + + _set_missing_doctype() {} + + _get_default_date_range() { + const now = frappe.datetime.now_date(); + return [frappe.datetime.add_months(now, -12), now]; + } + + setup_actions() { + const actions = this._get_custom_actions(); + + actions.forEach(action => { + this.dialog.add_custom_action( + action, + () => { + this._apply_custom_action(action); + this.dialog.hide(); + }, + `mr-2 ${this._get_button_css(action)}` + ); + }); + + this.dialog.$wrapper + .find(".btn.btn-secondary.not-grey") + .removeClass("btn-secondary"); + this.dialog.$wrapper.find(".modal-footer").css("flex-direction", "inherit"); + } + + _get_custom_actions() { + return []; + } + + _apply_custom_action(action) {} + + _get_button_css(action) { + return "btn-secondary"; + } + + toggle_link_btn(disabled) { + const btn = this.dialog.$wrapper.find(".modal-footer .btn-link"); + if (disabled) btn.addClass("disabled"); + else btn.removeClass("disabled"); + } + + async refresh_data() { + this.toggle_link_btn(true); + const field = this.dialog.get_field("link_with"); + if (field.value) this.toggle_link_btn(false); + + if (this.missing_doctype == "GST Inward Supply") + this.row.inward_supply_name = field.value; + else this.row.purchase_invoice_name = field.value; + + await this.get_invoice_details(); + this.process_data(); + + this.row = this.data; + this.render_html(); + } + + render_html() { + this.render_cards(); + this.render_table(); + } + + render_cards() { + let cards = [ + { + value: this.row.tax_difference, + label: "Tax Difference", + datatype: "Currency", + currency: frappe.boot.sysdefaults.currency, + indicator: + this.row.tax_difference === 0 ? "text-success" : "text-danger", + }, + { + value: this.row.taxable_value_difference, + label: "Taxable Amount Difference", + datatype: "Currency", + currency: frappe.boot.sysdefaults.currency, + indicator: + this.row.taxable_value_difference === 0 + ? "text-success" + : "text-danger", + }, + ]; + + if (!this.row.purchase_invoice_name || !this.row.inward_supply_name) cards = []; + + new india_compliance.NumberCardManager({ + $wrapper: this.dialog.fields_dict.diff_cards.$wrapper, + cards: cards, + }); + } + + render_table() { + const detail_table = this.dialog.fields_dict.detail_table; + + detail_table.html( + frappe.render_template("invoice_detail_comparison", { + purchase: this.data._purchase_invoice, + inward_supply: this.data._inward_supply, + }) + ); + detail_table.$wrapper.removeClass("not-matched"); + this._set_value_color(detail_table.$wrapper); + } + + _set_value_color(wrapper) { + if (!this.row.purchase_invoice_name || !this.row.inward_supply_name) return; + + ["place_of_supply", "is_reverse_charge"].forEach(field => { + if (this.data._purchase_invoice[field] == this.data._inward_supply[field]) + return; + + wrapper + .find(`[data-label='${field}'], [data-label='${field}']`) + .addClass("not-matched"); + }); + } +}; From 3d14d8d987e1ea2572b2b83d71dbab266856bf74 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 22 Jan 2025 11:48:26 +0530 Subject: [PATCH 38/38] fix: don't set default IMS action --- .../gst_india/doctype/gst_inward_supply/gst_inward_supply.json | 1 - 1 file changed, 1 deletion(-) diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json index 11dc8d019c..ec66017e48 100644 --- a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json @@ -412,7 +412,6 @@ "read_only": 1 }, { - "default": "No Action", "fieldname": "previous_ims_action", "fieldtype": "Data", "hidden": 1,