From f76cd880e19a25299a90d813740f483c10d1eac2 Mon Sep 17 00:00:00 2001 From: anhduy-tech Date: Thu, 19 Mar 2026 11:57:52 +0700 Subject: [PATCH] changes --- api/__pycache__/asgi.cpython-313.pyc | Bin 746 -> 743 bytes api/__pycache__/settings.cpython-313.pyc | Bin 3402 -> 3409 bytes api/__pycache__/urls.cpython-313.pyc | Bin 4666 -> 4397 bytes api/urls.py | 4 +- app/__pycache__/consumers.cpython-313.pyc | Bin 6457 -> 6454 bytes app/__pycache__/models.cpython-313.pyc | Bin 139980 -> 135907 bytes app/__pycache__/routing.cpython-313.pyc | Bin 401 -> 398 bytes app/__pycache__/signals.cpython-313.pyc | Bin 3971 -> 3958 bytes app/__pycache__/views.cpython-313.pyc | Bin 86784 -> 72249 bytes app/api_workflow.py | 28 - app/migrations/0001_initial.py | 1062 +++++++------ ..._dealer_rights_unique_together_and_more.py | 31 + .../__pycache__/0001_initial.cpython-313.pyc | Bin 118973 -> 121996 bytes ...lter_customer_dob_and_more.cpython-310.pyc | Bin 1325 -> 0 bytes ...lter_customer_dob_and_more.cpython-312.pyc | Bin 2529 -> 0 bytes ...lter_customer_dob_and_more.cpython-313.pyc | Bin 2444 -> 0 bytes app/models.py | 1342 ++++++++--------- app/signals.py | 21 +- app/views.py | 294 ---- app/workflow_actions.py | 375 ----- app/workflow_engine.py | 84 -- app/workflow_registry.py | 21 - app/workflow_utils.py | 652 -------- prefect-ui.log | 17 + requirements.txt | 1 + rundev.sh | 35 +- 26 files changed, 1248 insertions(+), 2719 deletions(-) delete mode 100644 app/api_workflow.py create mode 100644 app/migrations/0002_alter_dealer_rights_unique_together_and_more.py delete mode 100644 app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-310.pyc delete mode 100644 app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc delete mode 100644 app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc delete mode 100644 app/workflow_actions.py delete mode 100644 app/workflow_engine.py delete mode 100644 app/workflow_registry.py delete mode 100644 app/workflow_utils.py create mode 100644 prefect-ui.log diff --git a/api/__pycache__/asgi.cpython-313.pyc b/api/__pycache__/asgi.cpython-313.pyc index e777b4341cae5b4b5635e4ae7c3cf2f28fe9be3f..ccbfa8fd5b5067042241c71b92581b7a01c1c591 100644 GIT binary patch delta 34 ocmaFG`ka;fGcPX}0}$NtUAU23mx delta 37 rcmaFP`ihnNGcPX}0}!N~3)sl5%fx6t*^)_-t+XV+ATx1tEK?T%#U=`- diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index b34de7a7797f0e8f04d3f01789112c3abcdc843c..7abe7486a1f1adbea9f8087b9bc2758b533fdb25 100644 GIT binary patch delta 48 zcmX>lby14nxF9G}P?TyB z)F$^^u2`;wauHkD>~9^!RaJxt1dj;O z)`8LbE1`NtjsbRwkPrd+>>M&wzgQ;010tlsgLYeZw^(LvjtrE8_NZ9qAsiE-0NZ-( zA6ktO5E^`%vCT2A3HEf^@u#8zga}7dVxG91=VfpO?tgE`^H#eXe7U7D$rs{5N{s52^ zUuU%rqMohso2ilXzR&Zk3wiA)-iqJ1dHe32t^3!n-M_Z~zxR&I{>gs}F^D@n!A@4@i%N93mFuKjr+9^jz>f-lbj}IyX}cQ5X?_f_2XJL)`{~2hLyC3Y3wT^kNOugDnI@TO zif4EjyzhHI0p1ToR-2Bx7uXF@Z19ty5zvNX?B#ZI6p!!$z$oAaXNg6#B$}moj1K~9 za`H&0$VO+$=q$xGJ_H_r2x}_U;O|hfEhX0Xm7Jrm87Z^Pw`RktaR}dEL|b#3dL1E2A-2X=saLUlVqql zNpX>nLy?z--oDI?d1A~{yueR^7o^AA6HKoVT@IQ6pOhYTHkno=T9M)sFM^kZe|uVG z;ELR?wLL6WBC!(1WnPuBwmICn}!=^q-Q%fSZ;~rmMC84v*5K)`}@A^ zp97yqvWT}0mZ*}1Jc$Ja&Em%<-N~@b0?8~;yvR?>=1c)9ai&vjAWsJJ6rbWtvRRfz v!^yD77>SHgoa1L@QMXY~Z*oQE)sKUt&P}Ejh*qF@f}extXaClu_)~oVL^F1m delta 1493 zcmZXT+fN%u6o+^1SuXZ+2?>yZS>IS}jBOmiH!e2NKrk^W+O|Zk3@J_A$Z8@i2TIgD zxP7T2RjT6dLzb6fo=P98R^=t?U%_Zpje@H5q5r@{QQtdf427yIt^CdRJ99L9y!(^y zx52>Mwl;^vuRm4}O6yKZ`U@6kSJNrHeO(JobuFtE)gz5c1)EQL4;7q7U#+amu3FhE zH`QM2N^=1j9==jqIQT%fnya#|IQVi1ypux^K;;lJ>#{C~p{(6(x9Lg;moZ2?IgEk7 zWPZtg7hfj8U*?bkxWZx5JhtibG?ew28N06Zaybj>RSxq2*9uL>4S+rlivaxv#aIGx z6`GCZ?~vc#V&wVSt`%C%GV&{KzIPQ0JRA%FZ^2=_&mrvNX6=X^%-+Ch)v z=k*^!hrxns6wH|7x2whmh#@XM1_5~Eo<>lRT?TmtWbXOSi>I}x1o>Gv z$R5!lc&O9R6b?-hG|hU!Up@8J3&AxA`k>3QrR=MFDnUBy2k8QtxAgt!UX-9=HULrs zdD~i`t}u3m35qZ`XpiVTd>E%AGk9c%pbYbZ_X$6AsQldVV+TPg<_B9H{C~L)!4P!6 z`TT$P=wngGZk$P0%X1{nf*;SU%A85V8z`VI}GaUf05B#VHL3eQhLgDD(Lkt#~F zQ3zrp7_fG!djh*B2#T}o;Kzjb*Y8k$7VEPF&9QOt6T*Az.+)', views.download_contract), re_path('execute-command/$', server.execute_command), - re_path('excel-import/$', views.ExcelImportAPIView.as_view()), re_path('generate-document/$',views.generate_document), re_path('model-fields/(?P.+)/', importdata.model_fields), re_path('read-excel/', importdata.read_excel), diff --git a/app/__pycache__/consumers.cpython-313.pyc b/app/__pycache__/consumers.cpython-313.pyc index c79da143af09df7d9b45857e953efc27f4b3495e..083f07955ab7884bca8a51bec448bf85e141aa0d 100644 GIT binary patch delta 35 pcmdmKw9Sb7GcPX}0}$NtUAU3kfQiv^vJI0Wb81n+=47U8VgR)`3Mc>o delta 38 scmdmHw9|cKJU zV6`>;tcn&jhzeB;c(qusY^)7#5z%TbtyL(Iw*EZ+@4auYMC||9#>af~&AfTv%$qmw zy?HbH#;eJHekj>;!E81p!T+w?{=7f;>fM%fsWg4}h+FzVR+-)>LE-yDeou(VFLPSYP@=_`zpv>M{9WOBX0Z+H- z1BvPQT~8mL4ajxZh*CEZg-pc1WMo!kCk9&Fx7yVcC2D z|NcqRNk@9u6(^mpuBv~j`HZ}Ov3!1c{vKA$f1mFGm{TC6{3L#U$10w?%fvH=9b+T8 zcT^7V9WkDdE_AXnd{*IjfLjZ*pI6kZB8a9PLS2PyjKZiZyothwhS;_%U1DIqb34O8GRGv9GkP6 ze^YDX-;B=SwtLs|nb+rX+o%~f^PG-gd!WT9*VSxm^POGGE0(V2hFLD=<+-!QYJLnz zy?o`YaXe>?o~=bRqU9Q9V%zzw;qF9C{Po!m=A-Hp=A^Mre94^gn%klJCVs=XJU(NR z%di8C0=%>GR<=7@{~NPmrMJ1mr+mV7h)3A=uz7sdl~&c+o07Qy$};UQpx#YMV(H&l znk4V#tiWlw5Ah+m4)9&im##6f`v7MeGRB)({2 zX*}=!EM7g@%2(BnH9m-V5Zr>JJ%+zhJ5PErNmRVW(hm*0>MACk-2=GXynk9f|9G8K zb@sj_{?)n)!$GJ*G~6PV)i9U1y@!=ftcT37W($5#*iadeeg3A_D}39C&akd}`O=zs zb85osb{H*cGpubPHsQ-2E3rO?lpxpx{{?@+L-nJ$di{0MV@V>f1gLp=lTD>N`vAN= zJC^S&O({Qs6>tVUs+K2W`UwP2B3OgDjQ)`ySJD%C&*VzJK44UxJ(R>-17julXIgzO ze=cy@!m!5I;`KL&wcah>cCQ>x-q6w9-0E%dg-wnAU|X|yTOB^h&mi(u2%bf77(m!G zI}m90d0Q*}zUD?gvP~GD!*>WCNILXp8)wpUkq*69eo|7MJ)Fcxb+~QEu+&im^dXOP zuWxp$p(e1&*Q!jL*Wq1Q(+(MYSz*4mA0A&%;(zFv;eHR_ByVVF!I!OJeU0y$cBT2E zEu$qUv|`J2JJCv{5UnpIiQOC7NaH)$A_5>Iyrt2uxAyDZeR_AdXMaaXKfY@=pY@=V z*Khq3dxxfpd4%AfUt^Zez8#Yw)!Q7FvwuJuOhI3JyT7%G?^`ik`#I$NA&H;)YZ^{$ z_usK01aD(v1%edHilN+x9`@Nbj& zx7$Y;EKvVjB<&S`ewj-weU`b5MnsI@Uy4wE<3}cD#FIK$hI(%{!isY0<_fI~YB?DUSi1OT6iem30lhf&88eE&8K&_( zgB26`2Oeomk?X13!pRNI-eAzbVVisk^1|xYK)X+V9bd0Oun@r_1f2-pOyZCKWRbyv zv?7oWv0ElE=HTqEK5p1y1#%u)zZvlH&+jWX5@W#tB6QqChZCK>=$utDw*Lc9)S&qv}`f zwn=lK`tH%%Nlmmq&WT!sZj2&D<$1X8b37 zjCMNIfMldA+*1(?g87&zK|s@VDquJbi!Xk|Wp~@9Rg72OU8bD_1y)7A0f=zV!U_n0 z$CKnMaGa_7tVmbv!DcKh$G8iEGpJ6fXDTW&zz#jj7mg;U@az^ zW7sNV9c$TZWQ~NaWv@;0Gv2v(w01F+_Q%J@eFfG=(1?k01Vq#o)GM#uYh^8jV${!E z#vr^6o7=th%|3DN12)3295OI7&T8&zd_>TKiD3u`Uv-SE*8425?9)fy=ajBz{FD2} zXjef&NJa(X3ap1R7XahTl+_4Wh3_GE0*w_s32&ESucPc1hg!$WB z^wN&O90uy1RL6MkH|dAUyZ13W$HpeZ2UW^b@UxEb5BFVJ+Kp(n4S|-HKgV9BF8zHfR8|I%OX*Z^;^TF?Y9K+Sb#ieoO8CT1*VsIq8meykd= ze~SO{e^yy(lo22u!QXQ4BjfouS6ania@lHbZWzYL?6*mc?C?@6>tp zhc+VuFQ$lvTJbT!O^;MsHe(V2(sV)W%VlTS=0mggJ6X{Y$f=BI2UvAHfQWoQiqBIK z9OIjdrn_f>a;@;kF{l*QcLYHzCXJho&C^~7dB-C2!}3_x@Mo~%5Yb+Vc{C^_z&}#d z1s=FguSGh7%_8$@R>&WJY#G~z3(q}|uu{?60k*=f(`x?m{zm@EuO?)@{9i~$1&41t zkS6_}@jVB|8MZ^&-@||MPL%2F2IyF)wG%TCAf{kzw9%>jT%}Q*y8v>!GdAhUpD?nw zh>P!h!X~}Lc=r>dwLgJ+?a!G0FO^{JF3@!kKf<-h#3-KdkG zwxQYAbgV%C)a5jJ9*erH{D8seA z`?!G>@H2OL__xQc`eHOn~PsgVPpBUmoGE?8mf+s(yFL~hQo*x!AR(}t4RYs9K(|Ks!f`pV0nC=p5OVZ zNt!0{`(GVvK-n;DFe>+N5EX(6_~LndNX|gX4Idlz%b_^vg1)WXJ3ZgMb%@uuth5!`fiGx4k1M*YT4dmuN|Dy(GNnSh4XXEQ4T{bZF8i3mAI|d*RT1f4PI@ zufj&hBh?X^iC++za+rF7d*@a0yZ`Ex8YKSMU&m&n!|<;yXSYsr?g_gk;v@TrqPo%Y>CsKPj9n`!>`y1R5h&DNzu! zN@e^4`Hu)tH7DX_6j^dWEW5x)S>MAn0wgQ=J0keU|9DnG~ zn`B?Z7P6$31}~u%5v*0wgK8m)rg{jbC}bk)4Thz1{3-*ywBQ zfB{2ikpE_70_0wBnQwh!E^Tw7-m@{VYm2-+9q0-KXID`l3tv zABj8uRbjXQCI4p#iL@LbxNk6t=cBl+#u{&MQ{8f3u%o%%|1ctLl^&Nk`%k&{-%t;d zsA;3zy@-?`c$g#zXmso%p#ceJG0bZA$?Zx<9+ddLuNn-`(r0=hpTD@QNc%0+d^Yl# zXzb%6AtN}5q>aZX>b~AA{DtJDoPzUJvcTebb z&mpGfByN4ueW>i45zO>5WJDy+yZroJbNEBw+N3ume)!ume!NEwNnBRaMdwzQnL%7n zlK|(fOxg{pUI9MwyD3I3B1iDD#BcoW4lWOth()z3|Ua zE^Hw6uL+4Mkz@-ZehEJ!aW3NW_Gd}Ftd0^%?GuqyAwC&NAIw&W#Kl)ea7q$aY1ogM zLm~2iHEbdpb5on+4_miIVm*#Y;s#DQfz? zBrz?SO*McX(;<7?{ef0-V=}9-y0KjZ*hKK>!mD9U5lUv9xrqRAqn3N$G?&HqBda5D805K1dIuG*L_@G!o2ng}k*z_umH2QBRv)CPg`TLDFxR^6H4;wZ4TQ>xLxYY_kpQjRs zT(MTmu~h_wIo@hHcKPXa!)*A7<^HCP?Lq%=Xdv42(CM?q)Kq34QHsqP@LOdnRw9LU zQIqPWp$v)oN2yBiR5l}ndY?X;dVeAkqA(=?u(Q}W4xbSWSBac7R?o&MOtAhu^NSlZ z*zLBm1bU#M8jbH%z$<|rCReL<5WMwU!15Z@=xk*UzcIF)!hlg$`;DUw#xAWtLg~rK9BZ4xOSe(VSvdNLL5;@Fnp9DmFf2=gQEVW=h(r5KHGz2<8 z&xm%{MjZ1*+GYdl`6{tCH#S@|u|))gJU(1A`N0bV#^mL`W|~m`D!%LASBca4%qn?Q z;){GX$}k^ld5|PUZG{EGI1jNQs8ZoJkfN&2LsVAdY}wn|uu)kbR;fg27;BZ*sl=>8 zR<5mrD(h6yb;i92(I8lb=)a0_?jpdnKwLi^B`V< z{573-&q*Mo6xEu$X_82DN zKqoiW*ZEr;eb*??)>oiZ<)f;EQfsk|7qAlPcPI&lDo*cW9=U00gF>aC*R8?qwG#I( zo0Vaj6KHAkwr*2o)@?|c4%{h_C3fG%@(p#+3g*h#J1+F$mUV*G>b*u$3tt0NVSR98 zKyIh43{Bg{K&wyw9cJsc`s#yz@U9YDQ=igI!=4Z8yp4^rFBlA4QQP%4wAV#46bYDy zgGjT{3Iu?-Z)*h4j!ceUNmdpL+iL6q1lXZLaPe(_D_dlaXU@a=GB%M(t0mGi53em} z2UT-+Qm-#pEM}$GYD8W7 z1wbgbf|$$%v8oy=z0I*?if z2awuA#zPy*g?kG723E6!7u<(o+tkeX!GRL7YZ99*x~H)e>4Zugp2o%+x}e$#qzNr^ z7o*Ki_l<}U!Et-_2nYB{rQ`^rdL>JQ|Np^az$J zc8*~h5H)NJ$c7K+GT?!+OfA%7pyk>;v80lL7{#~g>RPBJsw-Kx_@A*X!R@Jcpwq~Cy(@`$)lRmOWfh)=pMEI4WKlKV{V@@w_Ck`T<`Si-rD;9+D#$&Gd6e4J!wztw-@x;3%W1waUGx2>$$Su z;|;-|y}oOa@J?W}#GQ-S6n2v`*Z#bS?PeqY2BcJmlVKg$g5yHkZlXgZZr;C(_Po=& zu(i>@#oyTBZB_*OfJzKhu`=x*_%2A3?SkgL;+~~XreWEEA5Y`ZhA9$CKWCzAF-zBy zm|ZVEY-gj4JMk@ozp2E#i`lDer$X-2OTfvb^%y-`k4e1fXPILA>n!6Q9aA&2=zg7L zV^U)Qrgrk})0`_^9=n{<#@WMQ`}G^1%VQsWtY&#IZFAUKNtaD5}HJ z)r-^w`2g-fu8>q}ak3gL0JLCLYOny(`I{eP=8;qx^2KB&)#EpTT=6brDe6Xe;HMY5 zRlsPpnQcYmTSkDi1z#5LEe1R2>@rqwU)NK3+|ygLroW~>1b@Z`zHIpfCQ$w)M1D-y z$@fBSzfNq=XBoW9Go5c=;grm3e$R?ATC~3*iTgL8IPkUq&`UZs=nSjG6E{GY-m7%) zf#uA=Ty`~bgBChR_O(SkB~j}`B-b#Tl%^ILHEgtYKVXG4Y$T@h+WN?7(}&P#-w&@z z)XOcHLJdq_Ad$`S5T+5>)#8setdl)N6bMjzKOG(qhCxrVKa=Q5#(_df5ur$N){wPW z-p*Xb`A{lsj)@g4`KT0DfoBOk$Iw`!!ubR=oUazQwnz2z_;?*e!pO&bm*QRe6=onn zGJ;KFdoP%4o%4SfniLCb&W>>=8q4D=!bT4W=!5Y|ITCIW0WxHU$mO=MjI zv>u9eFIg26#nBX|6H~UaDpLj084=P6of_T-7T)FXE^H*d!&gV0--GQW_T}@Cak*NI zxHj6Cw7}E&5A93iudy!i6|3fj<3<&>YsdAbc;fsfrULKHOA?C{AEeMFt&3Vi8PLi?3etuWtks?^2=3SN7 zDYNg@7-F-b(&wvF0?zz33HWDrGP}MO@f$Yyd>DJCeG{jeVfl^<(I{)`Tx(H#2AQ z7W^`kw}G>|Qf)t=*pk?5?su^b1g%KkYxqP;)4K}$Ja#kKq*>SEyYWC$)JdM8V3V&4 z9J`atZ^D*re(-$Dt={Ikh;3S;lCMFm^lh4<;^Gv%x=AfQy@f3tiyt1=)q7!!xglb5 z5OwjcNy6%cqXSW1L)>HyV;TXH8QhOfYH|2hCRkYchK)Pe^UQuLysS*rbYl~;P+Wf- zb4Yiq#je{}IeF>QwI2aK$T@u*n=zVJ)sL|iT7jrNBI*;!3!54`WEq|Pq}uDmqTAUv z;~6ZC;8wMG>vr}ha6u!78~+Tk5?#Q1F?bdg3)Z?}wnJiag9{Ku3?M~*B z9#jhuq7{a7fc!y3kRa74UT=D(RXUX zh6J>!3`JsW`ugtz?fH4N=(-E^_OAfP^H@GXZ+H9~GGzpZ@D1wi;@UKDm-dpvei#(? z8It6&h+P8o?_AM)H%pg1R(u>L-nkpFgZfVI@`ZOb%hcID4v%w^O4O}p+2+WDTnL)@ zBGAjz#QJ@p#}vWi5_rtkjEJP%niN#n`N4ObGfF+_^2jE4Nk-VRFv`x}A=L!zNEhJ^8NDK^fL9Y~6w(8(~|u8~_Cm9F38jv%>(*S@`}W zm@DW$;2FMbQ4dkGBI8~d@%LaIyB9jo0NTrY)OpG}cIZJ?UP0C(Ij?E@XuU1y5K5V}ZR-ZnC;%?=0F1}iFIPx&)6EZ&>!RpPt#J?i6^^)6rCzkGcN{w&_E`6tt| z`_o4BrH$wXsDY5Mb*6eH;q>t3X@C!CW@eb2R(jX+P*@|~K z1Io$>-dBsee*q5446*xW=9ao8KI@7lqWTdC75h>Kg|k~N?gAgSw$|iPc?>*%TAHvu z2bQV)F3ZrUJSK=7yJ}jdj#9Fn?;Gzno53Pgc`P35B(?0o9HWwB0~^)J^CzeC2R_n3 zOimORY=uLnwl)=NCFG_lx#@#*-AZo8pj=I6t$O)HUQwFD^QzQ(2h67I#GFnvh;zsc zc7Yk3G9CsV|0m~qQsum+w32jL1%2WFE3!D+1`&gS)*V@ceWe8e+K3L*$pFxES|+RQ#aYF?i;uRDG4jI??IPfBKgN>N`*QTL5U=AH=l zu50XH*Bn~cTo6iW=~{Bq?Cdw^^qF(^-q07lp{wsFrAZao30GLxIjhJ=-%qjHrLmJGfX}JwJ45~nT zn-Y53i=7yWW`fiy@x}pGmP6x5+n+dh!&>lk!fvVT!_Iz_iQUUsk#!{YHiB=lxBp3s zN7Li%LusUnSBy_TT>k{~G0CPuZ<`S#m6SkkSj<5`oYcdN`b-VrR)ZL(Ac%>83UeYs z5ELz+z742V*o<}$wBTsz;S)@`8hW&`oo&giBLL=C-VCc_<@)a7gq0xIk8*w0j zvbd{2k+-vzk^kmdwn$wm{{*_lKOQ>^=R4@7Yp|clGe$nL3HT8SYAQayiy*FEMciS% z&oh%$tr4f6XC;OO&}cPc4J&Kx@E6$dq5d#Z2iveYVv-+n|0+H*^70P$jD?eFr357x z1b`oWLjDF|EW{Ug{oaL-cO&SKMgt@jTAAsXsd#;nqsR$N{Y8Ng(NVzep*|X~IXvn& ztW07*i7j>;(`t|v!0C^^v?1$Unbm{2b(LV`E0XX^ZQdoHi^V1038s{<6npDQlF|kDHQpTB4axQ zG4TA7V(kjR;?uU z1jvjvNFvFMbexKr)8lpk#jNLHALk>eN5(3^6bJ&4Ex4^syg{_k_rJ+_#UuH~)rb>; zUn81dV|%65Dh!Z4ob!A3dzQZiDW-iQg-wyQ&l(Z=cmPUZ!EABG8_baWW9$P}u!RJN z7Z75G{0lTM(N_kRfLc8N1}h|KCsW`IT3}&~w+Yl6I56UCZU~?x`}PfXxp5tmgi3M*nOUkX-VX(0T9)w^^WVt;Mb6q|9HcaL_HR9;oFqvBb&z(4# z$#XT-EkKMw?bTzWitM2M+N<#y!L1r`Lod6YU9AwY_a9)%te?v2`TCnQ;;|5WrDzX+ zEcQv%Abl4*iKcd<9S$Lrunp;L*c1u3@ZYZyXZm1ILxxuFM;wto4H(|ia`I(;Z`hzv zd|D=sJB?=>(u`n_Mm+uw>yg@2hwJ{pu3>rIh(2K~ky=F{k&rqX?K@MlCm0FJ{gp<{ z`Xehg;AYsbu(~pw-P7~L!+&I{sjni6-vS7$+q~@?BT_&Kx#&Pb5OiyVzMnlKb>Mso z^6p)ii>ptvx~wM=;ibFmDBtdxCJNqT+0wH>!h3A8wi7Txdi#59m-PljjQ~*uH^4GC zN>qiJ!y45W_NznI(OnBTo11DGHIT$KqZA*)4XVSt!{CIFo}(oY&c%S9CE8Dc$@Z#7 z+;$3Vnp*(vt4Nt3dCR+}mx`;;FoRaNO+UnxvNmnkpU&BAVYq@Crh&c?WkYr;J-jXc42hf~SWEn&_W!#0R5%eQb%}9C*g1Zz21KbIv zEb9!un~Yx=31K8wqseegEc+N}II9tldLz(r9j;A32OtlHkvAj)BeKJnEfB{c8~>3%zbVkPu%z^C}Ce} z#I8?a3u7N3f^;nWY6K0k0pup^1a;J3?SplYOxb6YZ-llL*{QJj14s;le`v(Yzp`7U z2dEdszyHc!bo>W9frdHK3CBR7PZKwM3cgtD-Ozf-=| z_zRhg&knF>LFK!=BLKsF;3Em&F`|z zxGh_&@|Y(v*-qx71;BNk##UsQ~ zmt+-{-?0&U;z-1r^Kw=qo)%GDfY_8(Dr68xH_Qmqy@>(Vq3qysLm z&75WP*7;gy;2%zPi@@DX>1~z9qZj8dXIaS}>}@7n;eCdwJjr8P#-OJq*@hK{Szs62 z3QzW0M?q4p6L-ep$wf~o%9D6;BKS{w$1trX9nAZ&=u^|%0cA$?X}trUGHK+TFmkTg z$Yps_|70;plJ)js3{|XqekN>{~EgpC$#(=rk+Otk^*fgaY~YI z%ZrXQjw7VkP*p5N+y5`B_`ZsCh`sX5T8~E7HEBz9VNh~?ZL`3V3Mi*& z{Zo(@Bho@c2L3utKn5ROi6p-QI8j%IR}`N?mQhJEo<_3Ifkwm0R1(g&%9|KJ-y&Mx_G^vRv|rSo zH>-Lf1Qp_;mkta!5Kr5s(Zk=xt~!a}PY7@UmBW}iim5mgU7lfz5TVF~YGf5%2hpWO zSe=7fm>Y?(iUi+Vb7KP+HYvroDQ9j-JAnX<6qht^(C}H$V*3d2bAwOC-f~H4 zCCN#S%_50TVeJaV3=)#p6vX^_uU!?yaW6BF_xbn zfs6o|V-m+EvTUP1`#aFfFOcQ3L;Q1hoF3a4fd@@hGbCwJ@&;9 z%K{p-V^}6}yda8)O)1wSGG(;ZBStl11P(b3#lw1hNSMmXSv~hx@D2;d&g!{g!e&gk z&C16=4QoR{qnxXv3HA{-i^!Cz`ZcD8OjR0Vn4&*3OIfKjthb|mcpi%mnz8@F90d67 zLHu62xI?%t(t`2t*)!t94MRV-M;&sE2OnI_1FqZO2oCb1x65BE@7)*(S=zehiyL&( zSevA|d>{#cGWP$XC4=Ah7ubF}Vu0%(-~h5$$M?=pRp~uyZV5sF^pRvfKA3_3-u)DW zIqdpWD}MeM%M#}<2Um0Or{JBQ*~j$am#;H7?|oLy*H2cLI%)QrVfI>Lv)ARZLS%@o z#2!wLZgu$mT5^JsLxzOwr1val1qKuJ zE54T>i@GP~(b7kqP70?AyT1~6Kdfo?*87?vxD>{7lW$wtfQg_F@PTcBf_XB|7m}>X ziHKQxtcw7@C-|T;fzULK_@gz)7x!M((0|pY5d2x2!5?jM^qaE!Oj-L?J;Ot$iDKuQ zEG7SYqd9m8!57^vu?ZkjOHibiDAP($!^wM^8u`xC8l4vOT$N0^mp&N=6IV$ZOnB1L zlZgbT8J-MEkX7&}%$-b8S5TN+*JWxT{wlImV#JlOZD%=@7`vSn%r!s&s<#0K&?ozE zf$xR!gDtv9PGtPXLl;Dhp!iUd#f#EKrkR&-jU!S9a1US$8{)`t_Gwd~TlZ+=L1#;U zXxx-D!6XAe#zvN+u=@m3VONYNV!MC-#^_LzWl`b45k!Dr6nqtGfe8F>Bgi)&E2fJK zZr^GVTgsTTC}gRFIhvW@pE!G@Z`ntq!k1nL&z>{trB=CEtKfpmULiVn99t&x1&kbJvR-)yVZL5pxu zL3Y$8i!-1C7@!3-09-?!QzFl%$otgVWKaiSx6thLxF9bDaHJ0@soElo>Dh2nrF6o+z}e`zmHFkJy8HXzA@0dI*W_#u=UszqUaOsk;x_Z`?^TX=dOKTR$5(vhx%%Jj) z_6A393v`Tb3Unj`BhC5nxd-z$di{yczm+7IBx~ixcnEHek{lo&c zSyC2+U9%TfRV`dHzi!YsnxeV%t+6Nk=6H>oE34<#%~`N?;hcFMzPUc7lLQsb9a>=M z6fUhdxYUGA_5S8&u-FuRydJwj)zH=f+kD=Zx~BTD4)$d>K|)S}&xF$6*2b;=#`cYM z?e(}75|F`*j;{e1n2qRk@zw>~d=0^{MG<$MpnSB!#*UA6Z^QPO5LAMN8n%UPu`KL= z)Km;XUvslkD{POa>ox{DqjLhj6wsBjItua@F|;`V$B6MY+(-2CnG@C zfq-ZdteGV!7oBdnUYKnzIkk6 zXH+8h=2slioS5HR=j*R)3BjMW6&BU#Ti5~C9G~C2>Z<-#z7YIbH^9r7@+9|8S=xJL zOUT&@%^Pg}hU`8=HmF#J5nXd)vbq~Ke@Avzl9HR>XTkkx%c!mtMF%VDa~AdFHTeG(flY$Xt?m!s%{ z*(q>317c8Wi>)}#>*5Qu{|y*62p%T}X>3T?b(uZWHKeRE(Ly-UtV;QqS^%c68nfh7 zVh7-sC5p{soDLaSMTYaOgE#Job7Zf&c*zvZK!DT*R|7g&tue$PtIwFV zw-!c!QSX}d{cARc;Lqq!jGLd_Gw*~d&KmX&pfiVxU0|zJpsiA&C{lv6>0EJN$#OrSQa7W2tGy1qnBWerzKO41jUE`1_DG%s^2}5&6ErtKXV%qfK`F5Suec&*QUJG7#;r<)yo0@NO)Llen0oMIdjFhe zwn90KkjjYVXd^k9(Htw5v4e`E2Gt`{&ls(j4Y;?5GF8#Ga%=S>e>F=3oxuq@Lw@uz znH~!UKMsS(bCpmy+2ex3!y~j8#lFw2DTWrS#j?v;nhx=mM2eIqeBQ|K=V?YkwKY@? zdKtqU%VHUw$(~fmC?`8U4fK%om>x356eOO$3%o{k031gymB@%SoQ_YEDb{`TmC z98X6VWNU0bN8+~N!ZHkeZi5>wBMZM0wncFR*TdI_lW`XiY%E{c-U?bnT~rOhELzdQ z-tA}))ImJ4Z!1kL*rEr*;;Z{}*pF7gB{xyX*Hl#dXn=5hf@9$gh~xMgKXiFNr%@@( zx*^G#F3yZ*1sh{_^3owo`XHg6@Y2}&{}Jk!Uhl<=<5|v_9|#6hV!_Y|kFC4oLiTDn zSY@nbMOnS;>O!gO!OuB3CN1Px)^!Dc;gg(rEL%s&KKc@|Y`A)l=u025zcG}487|z> zruNr_>|>!qGz_gbckEy#!Mq8Ca4du_;@*k*-{7Fwk%kj1#kn$O5g(Vq{fLohHRb$) zM4NkXE!n}QCF05RaE`G$0Pg?A9!Q-ZU?Xd3-Wg%unPT(K?lH@bCQFGmoRT;d>gGn4 z4}UsL2MFJZvR~p1!)Zt}tOnF{ety{0@fnxIq0FmXKom|%oUL^VQtq8o;8+J!0!v(g z|B$JirF_KVvdc)bCaxp>fKL76g{&0JMC%*fYO>4_0g1C;IrxpnDSr?d_50a!TZCN4rZaf!%vvoa;-Dc&_hVxEf18?Xxy zAc4UZN*ACOoY`l{+*`H3Rx$6sO7Sp$7An++_~fCr)S_v|z9SKd$MD^E`b zpTzFd`RE=3$87YFp7_U4SV7s~Zdn3%Zu&>Zx@GXGDF}hWbJ1mkI|r*MoipM4odXEq zw6wMvzGZ?=oSy*~Yl3Tb@(|vj4j|b@r>dYZn435x!RQGq=nwMhDd)jw__2c!AgMt- z3IKff!yOc{8M6D#+1=wq<`RC}nGKd=WR_xuS^T6*V(ei?+A4@Y_rY~Fa4SI&w`BvZ z%DIQ>g8i0VH8Sf5-YZ=&54#|3tAc_r7&pd&wjbsO;Xj%kqxdfl7aQB~3lL0)J5&$* z&7+Z;(F!$ficWNZkxpb+dq5ikPYw8MP$z<0$(NO;f=UF=K^;8oE_3m7H)&MHTAkSa z082NKR$~C|#0)tV6Im)cW$3&nI5^N%qZ2P)#oXXPv?Nq_L<(D|u$9ys=Oyx7kjGXr z)PYdDk>u1`-Ezf2WA#`;Uy;B)6Uphs92excNqh}H)tmvRnltHCvvm$civ{rJZKZGh z3$YjBhUa8S%A9Yy2pjf|vAXb{e=RKX)i6^Y&tpMWgh{a3i#@rDYYnCM6 zhD-JkjieG@#9a|Bcu5z@%hOnvrc&&{Q#b{}9O@!xqv{~`%Lg|$G&jQGBRLB%6RB(V zxA@yBf(NgSQJfo%-fehic4r3?hyeRP_#bE$W|buk9Hv%x*Mc*8Td$+6H@W6Ez{{a;Okp(qSdFha6L2K|jA}1YYINIuuzCw-z{f z-W$buc0X6Vx`Ng5`f2G#qen*up{-WSz0=&WXS?#OkG^;C^L=i;8A{XXWs$!UCW#C7 zEUjY3N|p&WIp&CSV?c^VveKxE4a#5@*sjMaZrlQ;Dw+&9G&BMBGTohac%MbHzyXW6 zGq#9ldt7o(Q%Xr{*r70gqTun@A#+KqYReMqih1gXNJbkMXAY^Pw+fo?!CEQ&(-aH&h8Lp6W zyP|X~iQSn&^KP3^uLNCo>r)vp*1W5e0viW3SMHg|vNaUeF=z&*;FE^R@T^0* zDk#Bc9#TrWdETK+s*^$U45EwRY*wx&3+C8liY`KYIngI7nM-Zjl{Uo_BGPC9UgMF8 z*hS}9mI;9eSao=$s#_d~>pbY_AL4b>fuv%ZyMqrTjerA5C3qkyY?;#>=xBt4dde{P zZ^FJ+EnMRHzR|0X_ITo~os1KP&Lg~wDJ3QoQ@9hSg79Fx!DobXa4X6%-j7XL8mRpU zYS6e0_rRFpt4L@eY$JmOT&5Vjam|xvVa(kfb>9KP_-)x7@I#bTRi@iWVi&B_ssK|owb1}z3s zM||Z$(8NF}c;OZhFoIjagl>;K%7Flw-*9(41mHE24GcPca3k&uD~GHACmzSUrdS1Pec7XrIFFADWiRMeXOML{y3ank2aMQ>U|q%?;78OU=QNNU=r6~^R{JmO zf#X(_VL2stcc+E zpCzXoqP|LC)gNliMz_8r=fH6J6}Ho`wYZ0*0H1Nie+^c^5FitHp!Bq(%^e@P#S^FD zCj@NkG6V%FO#71MjECcO5m^TJ>-oE3XyBqkI4BXBE;u2S`7s_+kwY02!#mubSW}rC z`*Cu>O?45BfQB?PJc_)ZJVOwdnK^}2DB=cO$MCSAQeKHPpp#UAR>i?WZk3-R>G7Q?iXTk1Zh|31Cjwd{`yCKjV4esI&-Ziw+ma9~ z4qGB-2pKv5K)Q1H>6eP>Fj3+M-(QT5gmh6BfUAnyo`P$Oey)Oe&&tWEDhMB;C{JM% zOpUQv`bjFj{_mOKeuN9n=&9@PnXs9v1ShEAtf&&dB}MOiIb&lPI*H=7Qt%yVMZ$3f z%Yq*?fM-=YM0(k;VA&7@lt0-m@~R@2!en4P*Q!j2=gLwpg;^2AJ+kFKIAzl4@1fx~ zcTgVO5(|0CxJRzS8HKt6Z7GstkQ8@vD^l3PQP6Bs3^*|OzYEhLrZYv@tWuVzT=Hlp z!I^zohu-8ST}g7-p4hO$EiVzbBsNaYM7(`N_6Hj4HpqdN5*F5Oz~5#OZy2RvE}#cY zRk(}CcMTqnhJSoPu}dvwjKWj#h%lH7MSvfi`L`yYF*$Zt{j{n(rzg8NZ$`*8^PEa@ z8gA2xtq0hsOUh>A%14pE=t#*yNQLzga$tLr+>aXE_zTK(=>+LpF>63^73vx`dgUfx zBk54`4n&5E7wjxXY$Xtha5`2EzQ{$KjmhpjfJG4Ca0G`bf-%O9C=i#PEFb^mjgQ`l z;l~gwdM1I{8&Bk(99i+?^hc+|OWPE%8&Fj$>T?wBpBHkB1w{#L1zTaCt#JSFkZnxY zLJG6V?K9=>AA7`m+!r#9uIidMWEX8!D19|L<6`?{ag_~)aWzUVvL%LxEMq`&VU%NO zpS!eo0) z9QpdjYzUu#y$9N9aB%xNw;A7oi46e}YB*&LIr>Oxh|R{BNHUzsxF)q@aCmeYgh!`i zcyu^=6uGX--|F+fNs~td`_Fi?OMeo zAkQ~4xBLtOvTi+yD|vH=7oyPGx5+E;(SRRP*B)qx``^Jd5I0<4`cC}r;Ay~x)@tP1 zF3_`m$BTPctnFXX5Q0BTBXGrkW#R8D^syXJ@XI26o7ZQ<4L92;;M<62xN6YuFV2L5 zK5N1L`s4N!vqIJ-AnjAL`cq5#QcHT4!_HiFP0U9fbuIVAcaO!3&$0scoFc{lD4_%B z@|Zb0gXce$hbAq^X&uDRs$`pDlD4me`{oK)vQ&)|ZC%A&b&2o58rA7YNT&*W1T5UioxqF671Qye2s4eAqRu)8fl64 zX~?FH@5m@e@zL*2N`w`cGE#3LCKdH+Uvgc$ADm!T*b9JDs3(~+BV*?g-40>|H)4qq&%m#2j8tE#&CqH0600rs_NNuzCV{NQKui?F&|kcl;vQ^v_gMq*gUAT@)I#zp|J*j z>WhhsGcWyh%G9$%AQ%{oQ59nlA_%dkDMb>W&dDf&CL{jvuE|K;H5q>!1d2ne z@^A!HzN7%p)#SX1bL1@qZzH)DPtB~F6`ojtm+7qxKP^E+9IBUCZo&>)Rq7P&FLFCh ze4>*vO1@c=544cc#%h7W7$^>nanrZ!N8`X2z7Y!jP@JYa!Tt zMOH~O0mE=6D1-8bA_vs8SZ$-5mXcvLF~9_}&?QXKC%AiL9fZnu-Kc>@2_tmu%5vg> z(q?7Nf?H#wPFE#Il>$!M-K`M%Q3v->rO|8A(@mF6qElAXjfC?CG3z2ztRD$3T7145 z*E+@HI{45~HXItt!9zo`8bq)^1Nl@{y>emVO{x=s(JB+7@gizYWC5ZegoNpWM-=i> zs-rROky)us12~G4B%_&ttBwIL4x$_i;&t(>KE`9i?lr`uZq&Zo`+c0vY74;0eQW1SH)eZR{8(HX1H#|9Q7+g zP7kad=)s3IBZL3=78@)ea|Om9i&wIkUCew7HXz|JFzL|K;OLK5EPINL2ZoS5$%-bO zpbFzzPZ~@*x?m$wJNOKV9nO$A=nTpC`d?9sN5cnK%FknmqDmZb7{oQcBLjn z#$io57N`9bK6PF)?vq8l&KaLz4AjeGQ$6FMyxAkw>B7%kRCLv%Af1B%XinxOt#|P==)0l?mJ#~Yq zSqFEM&b)_dG;lgYj>)r_gi|(=G^uf!rwBC-I7}i^kYt(;o#t>nHZIWUPTj2dGYPnC zniGQsTn-4hTsjqL@pQDc!G-M*)QAJ-Ux8mQ)m-s?wLnK*(jF0T@##ULj_$oGfq210 zWJ-Vf5Thj+d7~g9Np}Hy4_`(vY`D-tlJ1fbS8iyP-|3KA)mc8)4To(7c6K<%C}hMeTvFuS~Ihfy0Xc`y)77(X8SAbCW1q z%fQ{AxREm{zU(>!5qB&t$~ev3u{)|l))ts%Lk&t*hL+3#Z}y)n6kg-xa!1~w{Lvy#*lSgWOkE-Wp2n(p=^sPe%UXGSK(L6 zA|WBe!~-fgLlzARN#@>txe!H&@stwW{`TTS&dZ%1lk93Tmsr9kg=xS(Yir)-_d{1f zs0?hrH~R;vjdZFqcEd}dJ+YR;Bb$*&ObTy+4c6AIO#82405&H>c;01mNlMbFpp*=2 zm9olOMi)I`><3p$6!aY&u;Fl**?wQhF|O+hk@qyqG)Kp^yD(%b=J`hyXX|Ke2rgG} zBLU=;EQVCc&mrRKYS7;UWsUW;rw>1_)8TIvKS`0QI(J}MvKsHh)Po4e;&q+hQ2 zJLb_Z6ypxOa;uvW%|-`;Tm-`rj6_g@;Bo|05zz0aR$}T31oTUz_#2`0Bc5^{+X%@< zeBO*;8v^=?#2;g72LjS0=&Gu2e59Yw!r!lx>8I2VV~Pw}`mHqjl`#6*tuy!nfA5NZ zok~70o=lapI~`~@yAcdWP>P@&0sZ94WK7LOFb}~Z1WOTk5UfVvN6>))@9S6MB6ebm zZn(Y|Qx75d6#~3^nXWdbD}&*R-%fab9W#3oyo-SDt;G9Q<qDppF=?Y1vFJiF(ie9)?7un(rl#Zg!Y!Q{G$CCPM!(@C~z`* zx~$eDp@EbXoUKnvI;Uccy`PlzrzGS1Nkw03RczFN-DF`!yDCN1?y7+#NDh<=n_F5h z&bXy9qALTynGC6%xenVhrLW=26qa%8gq>4&OdUvq5%+ zgJ|B}JdmVk6$9yFbe?o$=Rmri74BLoYIoOSe&IlRCOlt-7zzib>R9QnRFSniYaj`d z1F24yed~su%{u@GBnL)WS?(^aFzq%CBtdeZ&=jX0k^?!(Y-IF3BnMKHm_DRIS2D}rRU}639yO2z$$?Zuyg^6~WLTN|)}o!GcEEc`4ixI* z4M1|hWn@{muH0F>13nIt16gnaFj^jx138_^Y}DSB57gcZ1t2+)YKr#;BnNWfT9#dl zvGTBiEC?rz5(CKrhavt9$$<<#%Zv6FBnQ%U@n=X5xO8#ckVN8gJEPqLX>2U#)-5}) z-*G)Qm@|;&V&kF+AvsWHi+4XH2Z}80@+h-Fa-h^4uMf$AJR>XFTlB!Fd!ctAiC-Th z5RzXO8CYl0t}WvF-O!+c6Bc@?h*6uf$ruGn)-D@b9sCJ3Z%{wpfy-51d=_^VsA)6%S~)oXpt!llD=G>GnRa zAI*w&!&I0yCPto-9FrYWwJ{~8>h_vQ>`aQi17h#2(eb6m=KujOFq!brc+(O@x7+ld zgpV7oOOhfPn<(R}knuIhm~VTI>G%&e9+g6-6dyv0yth~qtpp3HA10}V~JnrTOK>X650j-SL?r&+t{Ow4Yffqa9 zu*E_pd~*SG#+AD9K9VV7T2lfjzvG9tOyV6irU%~4X6!OZo58;x zu-GsTY16m++%i9V(?CrPJdWdQb2dSej>_d?2V*AQIq-QlDU5wd{h2VsUk+$Dp@kGXpT3uKx!GmZ2sT; zZ&`IPxtf~_YuIA$DI8&0j0klAfG2S|+@2lBU(ZYto3}F?pPQ}c=L*Xp-@4K(`Avgz z#l(7c74I3AC@OAbT7K2s6uzV+mfMDB@U8*b#@H$Ca$TFp+j8m}D9Neb&EygM&`*;R z&7l`1m%H2QPPw7DKx~6}u7Tw_Zh;4k2osS`MBJBCXwp}^uWOrz-9nxrw#;Tl)^(6j zMsN-M`j+vNe<|X>cw@!m*{oD-OaNx?Eq=#tnRjiATlO`1T5GGjpaiH)rB( z)pA{nudW`MqtN6-n%H_|PkO?IZxx!>B26~2<`65(-3DQN4s&VX9kFr(M92dXWB`~e zZ{+NXIP2XQfxw5q{dvR>+-PEV|KABta72?-)mGQm?mKl;gyh_$i{C3mV{--^ZAx`{ z>z#vgyC4Q*!=&f^D(Uw_G8w^5@OuI2=KQ`GrpZ?-eLq%#Z+f?spK;CR8Aapm7LZcQ z-Sus?L1H&Xw|Q1v|F;UM*uDWvE9K*7@6I>uf(VS1xAL7s;0TE8 z4|4zD+eL+oO%bj4u~;!67KF@_61B!OQj*PraTb?ssj|w}Mq7bBV63WZc84hA#KvTP z_NzH*wx|~hQQM)2fYAr(Y3OSohv)4yq?1Ifn+@V^n^L)JaE9?wqy+(w5!RnrnVA0* z8^*i-ohTNsVkY(|Up2`Iv1}~6LDaa|W!f53mAcBv&t9Ln^>dAw{G39t$~;oybuXqO z@k1;?yd7teDl1H>wqX*Whkvr8$}Yz>Itt{b~$CXOiQn16iO0S;HMDzRD&iG};Rsf!LWXtKFbk z$fsTha!>sN0H%q{kbdk%)8x?@To2%s6+rc=!T3Ci;XAhxaUx>{5=d}OOy$J)m^Ku$5=N z--8hkO=6=aF+r1`hlOj4#eSnq0R;b=B zDNQmGW8O!A@21I5V{jifMv5tzOKFQw;YC2F$WpPTon<-BLJY=+#eSMN`5KE8&+n%u zvAUi|T*SW@F!V73l(Ye@kLDZXuk~LTivY{_-6^(puzd0H-=L$Wr!qaCvprtid5-1r zKRjS!|GIE2n)HjIS(W+ri|8Tgw1i(Mqb zf^Wkm^T&6X*e@3rY|_uEYiUqw`eSsdDVFdHD$;`m1oX|2n#wc4qr^YDn#yzP;7LaC zBNqG<0-Eh~yso3)bZawB_O5KLYgt1@dbEt&yVE&nS6uaT9Oy(m`R6jN6%avi&rD*Y z(~QUwXfdEcK*-G(A~)-&cx0H$L09tF!kAMn;aH_6iGU%XZ@{GDU<1q1#6v`4s4fl% z7J$Hp0jvw3d!N?s$AKt9;t zNfov~vp9a$WhKURM2G-#c2<`0moJ+tr89A;lqK&SFyd;a@{}V=r#3R?_)jC_te#Pj zCtz;y`0BmwE!1laIS_@>@?pU=QYjV^pP^ZewUlX?5%oCC0_rx~-Zef`T-~Zn^6Yj#!Iw^&Od-6L5dgNbiZWE_xwCB$ z=`ytwm`N&B*uvUSn^}zO51P9S!g?P|)6^KNR8@S{XGx&ekOaJZtWJEpCa8~Tt1Kf` zynJN3##&_qRj0fdIWc=WVCP0TMDnoIJ_moQYKla#CYrK~hjeqN?VKtLyo*DogJcUZpZYf|`33Ew#mKG!iteYBpm1#ga*cGq+EywU=QQm|h+V*NQI{kYBny8#%LFY+ zzbs&yqLdn(mKxlxUYM7Z1}BApZ)6rEY1nn^deoM!mNM)OAql z>YAIq^|XGgg)UN$Q(vQ-+Xk0d%K(RrU>5xPp2L1(x5OW{Wc6CIc2yo^e#@lJ>7wNv z%NS^mJ8E|Inq9l%{pOO+X~zZ*Iy!Jf@4ylJ8;->HEL`LtxVT51&DT}rgjH3kOle-x zem84~9R_^DfBev)6hr7iHVV7MoUpt*^i(F#e$g38Hu1%j$U|N9!@3t!#e~n9S_KM> zI0WhoJ!y2%+VOnd3voFKRR%e+(O6&#m@4Yltc3d3RzUlB7C|LLQ=RZjZ2E9}^n0bP z8Xysf?qEJ>_m%=FpjDbJps8O|-|XgVK5 z4K)=~DOxX%9yu2QjoUm31=J07YkZ2@5-zpH+XjkB1BfyV;d~iC5pyBHO8B~|Oi{9c zWtdHlqo&MWQ|3FStj?)CZ*)-sswo-0#*BB2uFfgP;+#k02K2@acqeXP=bU5KgrnBX zUTfw%)~wDMVxpSm^BMb1(nKcS-UtHfxzRS2&f~$tj&tTs7(_w}R{*=!)Yawv5SE`o zfCE4tj!E}nXgzm*F^yj{-l+22$awSk!P*<~U5Pk*1&ik|j31(rKZA7qKjX8Ki9=Ul zF5(a}LLLi%kDefmbMXm*hwc4v0%y|Pp!B?b(&LiKvySmGQ=K-{B$d)5ax}?M?Oow+ zQ5y1Yz!1>1VFV60$5MF4xUk-mLVC+ROiJY4Ifl4;Odq?fekEn07U5&26-#3oUomZ* zjYuZah~)WV{&lR#SPPkC1VGi9X$^LR?WiH8*O1ax->vZ*hIdZlhpvs|*G+$))spD7 z&Js3LmAkDC3~9;~doNCem3-MuhsyH@#;>1Qrd5{k+Ti)Be)j>3$V^Kx3&eb zJ*4vdQ9)ps4Rh1OjAzXrY`7j0Ka3fa^)6V9jBD@_0VLl%dp@6jy`8(Z>NyZ(H-`wC zjP^s%BMQQEbK+Ee58blvUgUb5LDJ zU#xl(2w2ZSL|Vfwl?IjPFypC}`G&h7hI^)i|i3yYoE@lcYtAzqW9eVKZh!@cQ12D~2<+ znc5fZAfmON)o)!iTson&%I%Bd*=KymqQQn8koq&srnE`7H{NhRJ|Z~5&5Ik@{r!;e z%^7*9-ZgvW+R05`PrcjorGoRf^*a98RVH?ZKYLZV=0V7RhM&2r*zf?NL+~Y_yAq9{ z>;XavoraIO+Mqhsry$sVwT=CUKYDeE=21xfA5hhbExR!@fZ7FnprA^0hT0*$t;`=V;`)H)-WEp3RY&L)`vQ(duk zKjeZSYWu(({Uh11Be`f4U)+;_Gp|QxDai=32mRX)C`^!z+UZ&L@0vGg(Vr< zFc}p`%EF6)+l6eQ$~X^Vc?eRlyxC#7a4^i>JC~Wk_Knp4YnekTkT`2B)#5-YkivFk z=PQT?K@Os64x@P`jAl@ikrfk~+na3CV2S^}saX34b(!7>@Bf!}#6~Q*_4)3!9)+ zY(<=LDkKY-d`)$-n-|T_)4m5erb;9`|BjgvOh8l-+4=V{s^XPqHj_~OcBR8G7f~5O zU1%pv??cYH5nl1v|ERakL`)Gj{i9zcae(pDEoN3pDCc<{u~!nx<~m+DyFhya5MCKX zh#m4c(ubh3e~0APe}3mUU`lDJH(rZZJ7aJt~_0om$PK;KOx#9@z>=kwl6S6B-!me(|3jO6O2SqBk^^< zk?LW7<4FFzuihHc{JI*tC;H99`Id@Ae*JY8=@W&-mbO=!z2y=l&f&jXose|{l46ALaR5G!*E^VDJJOALL5;A_6%+G+8pF^B-E z@Vy5~ZUr?H=H2^!>EUYdY1i3)wJN8T34-7K)N=9B-C%!6TytCQI=~iE7qQr30D@D- za4c~QMy?lIMzN9n$2C@IgT&2ii+R^y)EJzwc1X^p=7wpQi&|$aB3Ys65$7?CV4cL@ zTDwL%ADpr3u2-@2JO39UeBBLKN?hXa-!RnhGa%rQSTRGgANZk{;*38cY6N#me9n#6 zvmZm<-=v@B0qZqdG(3X3?9rQS`rkwTfPNX6%g|z?f~VgrBb^~)1Ofw*kBKOZ9$H(4m&_7E#k$ih|mca^7 z7!%S$H{D!1^iqvNb)yLz$pY2eTVG4->FHRyP2%6)JcIY_(@T#?eCjP-u#z>Zhx*MU zc;77xdG&r&r9S23Z_}{nvHGUiD))7YEPqAff4MDBn+#YX`2KA}j5v?T2%eWfH}ROA zxclK$gl1xhm1e^ncOxu2scl{boGq+}|FW@En*kYnlxYAtnTE&^yb8YxCvk>M3*q7K z-jTrmPE9i7&J^}OufKCdJj(j_;n$~w%Hhf-|BhH*Q900<4UsZ}zY}Sq`!TSEWdkm! zD0_tc#8>>*#y$dMDPZzocTVTWf16SZRMFiPCRP+?ftuS`AF)8@3GBao10P}-42l1XxfBH%Sy_gU2>Rex zVI`7O7QzD?QNcA~6ihq9hV8aN=s8vNHbAG^fq?^sDkz&x+T zUF`*+Y+}y^T!GlCsnxY}7tEO>&qvfGIsS?Gan&oM#ateZ=@dIq-AXj_1!BR_P>Y*_ z0Xxp*9$0?CMso0_1z$#@!%2!Tjf{I5E%Hpj8!#-bYpZXnZ2%2GvHcjxgh+__p=mkR z(b)44Aa8x+c*_SFqNk5(Q_)nA*=x+~QtuktGj49r;w48Hd;IWYT+Z!F6L`-DBRU+( zM;!xu9Rs@^2PYmG>UUItg9VxqvU{!ByOtibA5r_Qmv_!MmYi`kc~EciplV#vsA(G#lkl#J&;Ho1H{)8&n61j9rd(u(+z+U^nT|@o$vd&q@@HwZ~9-Ff( z$8Rs~oW=K)=JV!%n%HO@Jz|H4Ee0E!tgq4Xv%?bkrgIvYxGPR43RgbUFf%`UJUJFR zr?CpQZpEInK|7IHy_`)cRZp&y+dNs+)27cj1%IP^T4ZM=v@t?vP|TzBk3pDGU0)}Y z8Pre(oW-c%G$SgiVMnOzW9L18WEwloH`D-RIG8MP{orA?dDtQdkOtrFG2Ht>IyDKO zchsPP?WN8sqTq6%`O`l!2k-ncPoTw=x4&x_J*7-9-KQ2iJ?u(pB;)gsr}7Egw9J5_ zfvNeX<1THD8N~;%fcAv1T+UMT<|^2?hCyiOhfXAl&LK>rQz9JU2%{1aSEH@W=l)^w z5j$N1FF&5925u(;hnw!4(;w?(Mms_i{<0)9z-c!xdyr<#>#!P(aMGk#-w79 zc(}7v+w8?PX@i@#)Z9wY07ho;@}k{W>g%+vMtz-jn@H%zVoq42)dkE;+da+bN2Wa5 z+PrO`=wPIhKoMZJmiCoPLDYMf)h=uHdOKua6a_xdS~u+g$q6J-S9_Y9>uSjbjyBa~ z(&T7|@jD#Y_tFcQ^R$qd{b1imYB+R#!pyz=QQY3$8l4pxzHZwgyNZEwlaB$sn<@fV|%L zyzapV>-_OEpopaOqe+FmNrl}@kJzjINwa&@@!`04l8W&0QdIqFnK*Z3_d2q7)$dpP z9b-FZ^JQ0L@W;PP7v6I$Jq@Ryv|eM{w(4E#{UduS79Op*#t%Qn+K~8LGlr$l4)43i z`;ECllXh-zTyA&v!TCq3{c)9G*iLaBO)2h8Dc&h86V_8dj9kq+}QV11#6-#RmjP)^`i|oezx_r8;Kc`i7d1e$~Xh z{i{L#R8)N4FpJ* z+=fr2F0Uqac?#|l7VsCI8Wq_XS~nn-x1Q576YV33!zyXi*4Nac?m3o0iYU{Q4~w!x z1l|*JoM2PHS>d_on4uutqI1C@>sc0JMFZJqaZjTqCCxaye*0`ZEMi;pT0qhA8>S{e!pOYm{zQ@f+{WH{1p!+>AMuJsi}1ajQhn zKfc#7i;OU8$QRHyxnTm313KJrgv~aRb^(2TT?_2<*Vi>G8<(_5qjo3b4lTDxCLiY$ z?<>#+Il))mmm7aG)*k_q?Zf%w))?Xc9gExh{C!1?-ApUeb9b@%qRhoyytOP_aa+Cr z_kCF#kltZHR>-;^WhB5#9vAB@>~j9g{SMRbAZfs~68wbf8p+u4J1FvZ)Np*sA94q6 zgh%i!^0a}G#NDFI4tHW4XxCIxjHSwa&G-CaxOFSMlMx^dzQ4j|D%f{-Ji@k8JG{H2 ziw)X#A$7RAQA`+-4G5x`79}aB%Za*Rcl#3(tP2v1=8f%87)AysYuJ2r@xQ#Uy$pI- z)O<{I5$9=M#A?a+AfWjUYb29N=WFVH)_XAm0TSywOx5W7NK!>GUuUk;|@fKV3&$-c<4d4BfMHSSJgMU8`?wmt*2D9 zakObyy!o%lMYB(NjqPsO1qd)UY@$j)dyV`MfTu9pfKMcd9tz>N7$)ORuwtM&h{&(z zoqx6&4kK~{?h;dvdJJ+NR*}^nYzz)GYsm->B3iWb3$GfSP0)Ju%Oh6lb%o02N8=5> zh{)Vl=UX94x3dDn6Ob6=!ZgM^yAd0L*D*dqWH%Bix+~Zd`bxL2y}8Zvo{F=-v`8PS z_^Q8@YWG2w4^_AcP-J-uaUggPa41#}us(yIW6%&|F-(MocAHYqlPcJ(sW*Oxq#1A} zPEIP?=OFuMSV`ChL39XCB1unTHz2+p2;t*9_L!wF5l`jTCmg145S7taFMC=QTrWc2 zZ-_Q>bax&^gb2Pwgb~a8L2iG;x%c5GGMV{1coP(3OJFgSJCVmfX_tOf@w_KXG>0MU zk9^+uPBG5JQd6kM(*U4uPqtVRWMA`LPmVUef~6t&PQ_0@d6#(4%9Q067>3WM#elSn z*2K~BKw}q52NS{oe&TR8|N3dOq*L?%KJ8*;cTche4AW(Adn?Tt@Mhw^@fX^K-@;;v z7BU(rYA6jx@wOe-dNFGL{=PNhb302Bo78L`?|wB!lt^qiH$T(nG$C2wlOwnLXdDLA z?Jz3C!|%j1_MA9K5-{Llh1y1>1+WR)08zd?X?pv^^8CE+!%*^8?->cSP!l|D?r|%(-bJw#$!kh0;ign zKL2NS49Vf&J^z$FAE}8r@nEwZ&g2zv5a9b4tWvR>#~v&){1b8(L&Bh|DLQwhkXRzT zRQ_=+ftu-S(Q|<189%`Y1o>+I%)u+!CwuL0^s)@&B+uH~;O4rg9C1dhRZ#fPtLE^z zFF7*CBL|F2A?>g3k7&OH3wz6Ld4`?nxtoZn#q2ncDUIFN2&H3F5(!!JMFWby#= zP+x#5g<9f1u)b=3q|L!%w9`om!*3_wDY~Cv2*%8 zgTY#ktBqf%b~otQPA*E#o;tSYKu0)gx6ocB=OO0xrE)d+P=J&pSt)y1bc4|x8GmL-orro{)ioSR- z&VMU0`yM39T2~*mp#Iv1rPc(aRy{Oh+3 z(snhfBgy7UH`orqnyWPFAF=3I+1)H&XkY^kcEE*Eva-_w3WU*$Wgq|quzAE`r$e$T zXh-y#rH9lo&tJ{|)N40AhPaGN;kZtH6FLNI6Cu}QYMS*^;t>skhYi8QHFHMd%i-A$= zGMsP8Vo&z0@ksV?jJpLw)8ujZd>o%?OOR&g3sh1?v&JO2x`Ukn!iL5&nXu80?zJ)e zk)0!W(fej80ITQs2O08!$^bG!v55)eaK*ga@d@T!2wF}+?`!hPZD<3MBR2o9@HI^x z#3LGJEvOTH3NHdyTt$$3rgB&zVt!Jj2WkZYU3)88^~pO<-j12kj3m!OAZo9JIVKw! ziQsSQy~{sXX*ak8@CHfj!LKt=@@gT$%zh>+NYtf94{a`0y!xRiY{ zofm&>lD-Fad|YTOgZ$qkJEll{)%$V$_K!T2X$Sil)tdT<3&}-}$%POK=-{Xa*%}o!3Cne=@&FQNi5dBr zgCsEWP$tWKl)hpu#`g%2JHB^eH>8~Z@<-j?-cKF^Tc?U#wZ(?RV0X7=)b3S%_63vv ziv!k~fIW?Nq=JS4q-q~J{qn{y%qF7-=+g$r+@3`)<0vQtBSV~e{)@t4Sd)Mnlp$|~ zUr|j@-G#;{af()K#VVeDDoGlw;=@l(w2eUO5&Rda&tZIB_Mp8_oB}iL2pW3m3I7v+ z?JJvP1-I#t3;fux6Qm>!ul{ zVT!2O1&595_atgma4HCP9JZ#zSrlF}JiF9h9d#74qN&d3_M{@NVSp>>HqlS}fgR+M z#EbGMk}w-G2JCH72wMRmSyyHNjoBKp<$3Vl3i;hj@)O*9ZBhI*tONoq*tZM{2E)Oj zK9&HRDvq$DY*)44R4A7G#0o8WxcQ!^)O1>~;bGqLpmyDnLzSqV;;lzl08cL9oCX_q zSTpbs2(Eb*UAA?XE4yE6r5Z(84KwAag)Dr2PL2VM>lhC%{d72P<|LLnfLfiX$5zK< zOHc^8NCb3{5z9v};LB%Y1rZ=Iz7Je4X6;~gLltT5^6nEUXkpVC(NS`NkWz#aNg*0)D2n8v$l!?B8H|s33j$4Wiiin|qNq`O2#KMw zIvTOizQRQU2!^YySop-4O3(?h=3pjj`Ji-f+INhJ!pp%KApM`&{P!O!XP!lMB}>_B zJhz1v&4#ql`CvzTbv>Nlr_L8`yNlS7U=57i(0^~27HD|d_a$01rb4jd`yo@PpH*Rf zsLEt#raBJ6I1J=@tJ<08y}V77+;^!+Q+ zBBcxM{ovmwrmeA6X<_4T%k`kbW_+_$I277OI3N4rx}!4FXzk-hBGBh`$%#2VNqR>|n^ap)vVii%3Q zI11fBE=c9PI0~IZE=YymAs0lYT^yAjiqcdeWhiYjf>G88shJ>eCXfJ9!S<{eSIE+X zf@gr3P{>kYQx@G%axUS1G7#KPa>@O~7&RApWN5sAG<}L9l_FCZ*gV^=i*(U|7M@@S zR{7gpuV_%^sFaZN$^?)Gpk~@g&y2X8ka0IN`tAT_zSxMR8iUiGa`vqqns=kH!o?3# zL7pPUF4eggqS&E*p~!Y7%_kMuN2tTYK@)HfL2J6)2&n_s&`Gn9yNLn)P=Z^Yu-I?K zLr0-^w$QWeZUo2i)$WFXJraeUD6pdo%O>(_0pFBh8gn=TlfWTyIH1(rwkCu%!Cfae zuYto>%8|EVvHQwIKZjaO6D^@?3EUwIta)!pl+JF89Iwo`p3>7dH6e$Kvjs zPMabbz2=N<*LNozys~Fe?a@Vze)usr!SSX3ljeZPTT>HEnxPHHcZ})3vfY@Nb~LfT z+M8I=U3rA{R8;#D7j(`O6P{uT1^ql;+WZOC;PDc@uZq4gN#Gk3FS`E3lEjLqm{p@> z-*g#Eh!17o)v#agsjQ;>;^)7yR6U;Jf#X+kCAdF1oVAdi&wdVhk)?-6ve=x*GSK%$ z597rIBR-K&_?KC%`UPyw5MUNz{}wt_l}0>vmSt#b?2yzb4*tx*TSUiSJdq$Kyu>u% zlopEAzzH7k8i~!0sY$?r7)+~ybr3p}R6Gu;%~7e7L#dsRI)Wr66rCC_Fc!SkY$G*t zTIiKC{0ff!X<+(=xLIDmc z1aJAN^)@#|J+-*6RYuclEFvNyLdr_Ma!rSP4(c0-4Z3y8LDz-gV{MyfrJLN!euGF5 zAVYn(ga)S;)a}>~W$c8U$9GNIzv^I{-#iD#-X#aaF&a@945;vNHPG3uh2XJ(huLy^ z<8r#L?4EMa7a8zzZg2tU_j1;SZ_yPYcyEOJNI_4n`)KV-Km0gaz@P1ZiUa=hr>PlO z5tJr9ndSS{M~0B6T$6usQ;+QPo7=?7FIlk|vWn$dqOUXgjWQt%SW|%2{`v%|#vsO* z!MWS54$z=m{~9Zv!1Ju26M^Ta1kRoQmblBTRYuv?XeuxV;-aRc+R1cys?GBt@Om1Y z55Z{&k5ehip!_tQXq=vWi7ZNYd<45=TyS!s?ikDuMmOpWGjK3u00{XaI5;#dW(!*n z^Tna{EE&)Gre;pXDTgF8=_!PQdLi`+-Q%dsp)aA zIO_KK;m6SiEfLLU&5_AHSJfT8%HxM0^K!oB#(bNk8QK>EKv}eHjxd-OZ8zmb&#JZL zWNvy>>9Ws_E<_8 zx4#}IaG9t0aS(fmW!sTCCS;=x927yT9&!)HAV8XZ9%u`&xuPFrcCS6VXTY%iQ+p~F zMHpsp>zoBv*CKI7&l1H*Gc4uqi7@2|^Ah<6%Lah9$i9TxN- zT$L|fA@-~VFTE3o2aLr#c*p@l$)m9F5V8>O;Gs9TesDL0oL;D_M0d2Lols@W5$4*UaB>Y)4g-@?X~FfX}e zjR8rLtXw|uC!`v|-!$k2KaFo1JPhvRGO<4?b9*5ihYsn0GNEU3{*RoKlTVoZQOd!6Oy^YpTGV91q4rmdmR%1soS4xOc9hH6Reex(WcAxOy?T@1b`5yRh2+n+j_&Fxdr6PC)o)$} zlUF~NwIN4x-!6Hz#BW>#YGO!f>?a^;8<~zR^gR!AQF! zW0r*2U5O`<-*{fu*LH!-x+?E8WUDzmzvKJel8lFyE*43m?0Tk_KGTR*DmGpbKatfx zWr-F);;ZXnxA}xdFg0tF9#^Q^2RqJ*EcL4hMPc_Sc|9;xnRs$In81Wu3%h>bYJ^kE z$_>u|+rNceVCX9Mu~KDSdqL@>>C?PJ3P|u!_0!mD2)@#Y<63sJ^mK$leWvqBNzhbH zQe3&(~x~tylG)^dc78Cz*GGEL}!8aCCIDS;)X}?s*yuT5CW#ft43~< z4iUzEV6fFQ$sC2P#?I_gf>wC#tVH`N6G_lki%upo0yC5+r{Z`>z?L{1Y%%6w zYnbFjd`@GO#S9JA;_EnOl?G_VPjRfs@D?B$fJhWW895NaMO{i;h3^(oEL*o>Idpje zm7h;OPj4V82%LZgmS=cx-;#=0VM&1My%EK*M-zczc~MnNPhxTAQes$evyHZ?-T_>t zQA|tmV+jcIkj_`|i7M+4R`zd6>{^yK1Ye>eA9DLzPhArMy0xKi1|0NtvZT^6SftGZ z4qP&M9~5FoL>UFcXP-ho?Zr1yeVxjB^`VD^ddcAC6CLch|vF z!QPiXQ2OIXnXHJ#Rb9x^B}`;wu_WbgLvdXeit6$s8ccs!oZQXEiLUK1 z<(LK=w6sYRyyKBvcn~0F-|bNUf#R3(aFwfiSdMr&n@uLipsmTuRX$T;+6dl4v?QwA zwB|6ou?Y|a45<39p-yuakTyk+5cDgO5iCKXA|vFRNRM<_loBR`RRoXCdYYq3xh0n+ zNNrm2`&>5Ia31iqA)e^{l0GZ}K{LLI?3eT@Z7J16OzD}Tr<3XSZOLO!Nvg>du0oax z=Jd{Q5_#ug4IEX#6|hRq4#k*5F%i4EJ{A_gk<$*r#cv)gewE`7!NqSrEq=!rz~Z-% z7QdF6p?*%4^<0OHlO|Q4g1@1KFB;qX_qTC_Sh_iCE$kx$yEroln6gPLbVb0FUm({e zVv4eUg-yT`YV134A|hF_0a-+yk(whiK8SZ;hX3K z>{c=@hDkirD%CCuyg&|JYs}-Ya7V$^3Xg&Wm5(F%_HcWZH$G)G6Jp@ zfpWGKE|pyOd*}*(h!|-TsE05EnPo7n=kre|;pKgyGk_JRVOVrBbu7dv#{e&kvBLgL z|1>r{!w2>rEuPz14mu-301l(GjyXOJn*8oz)=^l@mA*JF&KR^ytDRu z2bSI7yOfLGfjze}U!47nCFw5A7G=c2(j!J3SoAX4rPR8skm+zi0VKnypf}kURSC}k z^{-^kMKJ@zF{U81a)GqDq5UbiD@x7}M}TD*H>?Us4|c$Dow#ApfAODOSQRf1YD_3d z4A{aaKZ8D{p8K(nNL9g2E1Y+f9nRo5iWq?LjE?NX?yYL|H3fG85PQhg;EkUyD@wQ! zzoI|y$3kqNP0$TSA%|7;`$c~~d<)x2w1;>_5i2Fj5ggo>y(>w`!$QrgNP8S2G7d&i zkLo-PAuH=m7-LqB5r*QHB5Bhgz2d{_1_W?95sl#smaU}A&}=sx2mHC1T7Wc|2LWC( z;lj0xD4}K2@8&?NK&*E)?zI5Q4lLXhG6y4B5#z8zQuH&dA!+~9YQ?dH02{#f&#=LX z^m#lQ)8+Qtia?9Um0w1$C8KMq-%=Q~1=H-s9rMBV z5IPUDt9t*^p81vj_$oLObIHiirAu84{H8&jQ%ggK6?1kCj2vE!TC^|dscSr1=k>#n zy%ml!!YLS0^(8pHOu3w;bRf30UQ=3Ej^C6A=4V`kpgTWY8fnf4TG0Z42YtHQ{PrR^ z<`X{211B$w!76qslQCJt@Q}=|Df=sXD&{MvWG*<=x`6BT? zmY{`u*|b##Q8x{24&5W^sO7ptS>n3;SjGq)+~i4D%N)S&iF~1+uY*ihfc-YzgII&I{F8sD+;HJ zc(HHk2=)V$4r|3Vqu8Zyp%Sl-g5AA$wc`Czu)S9R<-V(>?Y)7cS#IV(kwTTXtyOX4 z*^D(&Zi2vJdb?OYnw6x}h(ot*+NTX@!R`!x+_D>o=Q)v;&GM|dSSbXDu~OfXo4##_ zURcJkV^Xdfov@xB%iK)*hq4a9BOl6rS;0&FCbqWv6hZSTU(y}OYUsj0E4jEL_y^MTXmB;47PD$$%b{;PDC*_J zM6`!`#OVoaqw@<$8CuODXJXM27)bGj?MveC6PazmX{3?vTd&2pU`iT_47k`aa}-3M z){23XSkN<*h@!cfazrgOt(PGR1W1LikyqT9BR-tO=17+z|GpC$Q()2Z9rCY6+z!Wv zq#v|m#}rnkMUUMdXqCdz1#T?jK>%sdEe~%>8mn}d?Niw-^^AZ9tVYY0d47g+$bSfy zDX4cT{%YjoK<*9ev4JBiHrnuxc(W;v4x)J$*!i3~v33@m3z!0>I(3k%fsTs_zLO;g z*KlU+0G}wuIW5Z1h!zP$uwd$8iJ=}Q_xH$B+#tyDs=xCCxWB+X4<1$M&tRWPzy@2O^zfES*o$-Zp%+b~W+UYu!j_Bt9Vu80IfD9l%@h}0k5D`Z;KPSo` zX4%%oNG$@S$ajZ0odMI-H`VMWX|Z~rcLDpDWzB{+L8nm~TBJRS0J5#O^EEH*AT*yeAex10-AxNMUyoyQ*)ca*9J%d_~}pgowKe8m3++@~#5O z;{_aWT^rs*7x5N}bW>`6Dh!?Hg9cOOj6yR;Ah=p5KD&zDz?u~jZm3~bvD`+i7fsSZ zU%iOziO8e%*rsCCHOwrn(utYZutH-SAY6rc6=zVq`hX^jQX4kgg-wJ!j!25=Hs%%C zSypUZ839t`dmwuLxIvQPW`ie^_noL^OITJr7OD9CQWGNOip}IWhT!a3T^*aQ?}TIl zmCx6Nd#=VcfEi=3{mPr5-=BfY!$sUu=18nUfmDs{r*5rlYYMvCk_$rowMZ2LEY!CP z@Wb^|buPGSutF`iE@gL1*V1&0pNcq}z8(v>G$V&IerV(vaYqA7mp1CepBvaH?K;2< z;V&E5W@`r)gaC0UUr7*8xtYTnKKPyEx0QCz;9H90>=M%yp>|TF48{&3VV`3e0}tT4 za6|_`LLokEgk}3So#<<59Qel{9%BwO-lm-t);UGs=e{6_{$@dJ?mhz>mclIgfxJ`J^3 zVIaaumiDE1YFj4r3=K=FZ%}e>fp4%FcOnT04(h}UKDI&Hsnqffoeflsf}hbR9*hrTp$J~ri4EY+ zE-F^DiPFR2l3#KB{o>Mg{&t)=crEm%fKI%3Ep+R@0D=If3wCQPMh9{5VHag*Rh&5v zTN{6;GHThQ#9IN9uT0oLn(V7w107}I@o-6zBLr*H+$D`AMJ`Y}ORA&%krhV|FdJWn zrJ#RTK;PtUZmq?88>3V>kVJx0$Jlg#Lj4|bz zMg2C_BUguii#6`IsUEsU-0&Qbi?2iW4BRYNj#XXYUr>l*x|l|c*!~IA=OZb;Myllh z+|b2ca=e%tzmp@jdRuu1JH}d)yo#@jnrX6G16XN@|b?S5m}B+M|;xRo6mVyhydy z8_Etk`D#?hFVe{!uL0qhKI|8er8uBOyeTf=Nl-(lp-_Gp>y7})yJRoP>$$rA=+!Iy z@Z)HPx~HYGNtS5;lEZ2`^e&Y#es+Gm81W%X1HU@380u;?dEXcy4>lt>-gS1VRcd%x2X>QKFyd-tE8wty6wl9G1K!zV)hc`B zS!Dq~)(PWSaz_fB)T(m8M^@qr;sdc)c;yvvAR;Mf?`m#8d4NNT`lgp2gRjBmuf!wa#fSx>y!cVLJ+7B_wiJ{Y76!i& zH_DY)IkjqP<-&>83un%on@8yy;lpr`)5qRKkt=V`yoqybCsx1*dlpWdqa>f$kFDFKTXLd9W*T@^JdNDvhjb z)E4Lh=1`@So|A+^BUH_JnhEa$8pP~#0EKuRVmkPqBxP+d9m^hR_wC(SO@jQ4uWqU zq^GbXDS83-5~+{$TtICMx#Zb0wx!6qmfa!ANb6Z1M)uJjQcc-oYd zfjlNgV}JAQgtBjqIV-6&v1ejqOb*f2!LG2LLHegj@Libr=@Q(qQ+Er?i7PxCqtXyRj-GzFXdZ6ghe8 z8nwy-Z|eRDHxc61ZcMf*nXH;p_zcc`2H%B%OU{(IK~^wT+Q7sQS0UEKf%j@WJkw_( z=qWMylvJaE4|N>+U6Sa!2QF500{+e5E34On+c<-J43)w99-G$N;&bKCs+u=7+Go(9GCru-7&8n$D{I@OGT;`xfC_+qLRV; zo-{NPHBSI0C~q+qMG?J-`W@(*cP7JyPxH%@I!c+WsZz^YltEQmIkrkC>l3wKvQER7#;OF#o`aLX{f-vhL8BotraQN^`J%o}X^wPt+q1)qXatH2;< zB2g~B0d6SjT+a-q8PIX$e;|Eu3KfPT%Bnd(z%!QX%Yf7hvd^wRqcv@ z+_~Gi%h)}*yJ26;o|c|rQxDGXu}<%aoh~Qv|4emFI*Em$h*s2Qnk{sbCss|KIAy9r z(=>cZG?6@})zZh93%{`;|ELy4H?x5QZbtHS!Epqi>wuAi*KUI|X`^B$_=Gg?q>#wO zu}=a3$G`7OC<8?%IQX71#MlzW(>Jr55_)E>^2f=Yb52^4;O5OKeoJ}h^pj%REo^L} zMpJS&2EaKB)68JX?DKX|3%NP0h5Ub&otc&zD~LflpG9Ia`pE%86LcD3oep0yz~s@MZE~Vvg85RdMKh4 z3WF&bPO!B3qLrk{AsRraDe?`(TSs19Z5;|cFMMmK4!(zn@^I`}jG^x~;iIn=KJc^5 zQ(vpZ-01rF)~&9|@EP5Wt~P*s>RnArHCsT2!&h?AhFW5P;(}CQd3DRcS0X5R5NAjj zceumS7|e;a%K33faD9L29Q4@@OJ*XAE;h!fHZMLavw*uH@5UQJTFcNZKKXO(-N&I* zSIhl%J4HDYC&XhhjWlF&g+D#*yQhTtKX-nqnyfAa6;cl>B;FKgtTBP4 zG~>5b;mOkP30H1GNf{f9A0k5~6g{t#jB1jGAWBho4olLbFa*h|Jayphu?d%OHc704 zTe0ONaNJ5p$1T|iob%KJ>)LUYg%rr(uo^|OBBV4bGZSfP@X7C|T0jT9NIwLjavO@4 zjssgjx~*u!WnsReF-@$P2?EB`T;!TuxnPP5Bwiipg;Pijq6WI&CA(oyHmHS0L}e)s z!Vr?ilLsF25E>F?@OYQU1>e|gs3i-R3kn(S0tq!8luN*e5-KWTDHdAlg5<4Ukg-tw zuS=K$El7pWMDcBGfK$$aPPx_?^q#>&;N4>|=6<{-INQhD-}7X5#ygnM*pU=Ke2|5+nLn z2J#Ph4_}4Qi2FCNp}I?n6l5@Q8OMe1MM!?(TC{ZfiO|>1cFEtysK#hv6&kPWoO9L? z<46t5nv;o{yVTuN{D~tw=bdn79(5MHEc)`LlS^* zP9${;c9uK{6+8rAt6W~!(&#N|SgL58crrmWXr(zFcVg1J5!{d9K?IK?pi7AlVdzx^ zJqZ4RfG$Y=9z(w%(BbS!XVD!PNIs+4Z8*)euIiW zs70T|qOVNRN22J{PV|W;xf_!_jo<)+g9zx`K(AqlzRB}<4E+NEeYb|b7IRt%os`}| zZUl6Z0$tFEUv;3*G0^7}WbzNAYZ2)7<%Rf`?yFsmp&Jm;g`Zn7bT5J(2=Mkrx+{^c zIHWrY>1IK?4UjJBlj(9gztjN<>25ZBCvmB=obWPG%@1+Ov)Mhcnmoa z&~*>l7$WN-IW0`YM>-xk8$-0gx(Gvc20IG z?ef!3J#B!~elzX((w-`9Vbb;@ZR^oa8tr${wi9jq&_;|*8w|LEAU})XAObR_k}-(% z1)AMyJi ztrjAC_ENgSFDB1$Mr4Z~(Hbn42urFE1285`9V4o;6 zNLemRga`=sW$Rh`hGmVsl)hwBga!!qWtdsX=HX(@7I+UqsQEgBRLUIt_86oenLD1PZMeEG2LAMw z7y;ARz8Lt^m*xT@R`$ifpT23}EE`4v!M^wlpA9TKR0IV3QsBU8h$IO16^iFfP-Keu zmr2@WPccSPL$EJH++~(B4LLS8TH(ET$SmdBsgPicQK8~Zv$U-!mgQ~J?l9d8Jb)m! zKtGPgCj&EY?2CaveZ$2ni8PfqluwNFze9FD;f8DlILZwMYfxJBxHf$GK#E zq(4CLd~z~81NY(2`QbXqktou(K;MGk`S>*Gdr%YjbAC*0q)R~Xe6p2=YX!mcIi?81 zA$UF`mJJH^6bPON>Y;%;luiKQ^NDFJb;Gnxb2iSw0#eV9(?fN`)hRy@9ko9(v5`&% z!SjiR2mug0pJHXs4Y`|&HUj$~cs@rLX=4bUk2gX$>sYv{W@8QZue9@Nrbq?|o=?-U zqHPOz)Z7d2A$UIC6yX#E&u18+oiE3{1I}k?Bg8=Pyv-1KhT!=WJ#&RP3BmKwN9egj zVXf=DqyI3+!ouu?AQBTE_7FVp(6hpA!*`6i7lsf7k)1Zmczs}F5R|?lu5?34X1e}F{g3P{{A>;GUwAerv#w{B zF$5Vl6-fMOV7RBOH8FDoX9xEOCU#CI#v7CMI1SjCIF0Y{2u@Dplw-Uzxs`LBh%QKn z6p;AU!0?@elhujwi`Zs&t|CT8=gGUc)A;N_@{$k}zjAHX<6&lE^w?~~$H&NMI@yt5 oO8G8>;v?ar3k(9ESQHr9KDjXRGn#x-;$SrSEW*KPR|GO20GTfBYp0~Te;4{Qt^!q?fQFS1Ktk=MV> zu787r?+Y7)g5G2aR%Mls91KEA3p6eZYBzX&;9wAxz9Fu3Lr7-2{zUzc>@56j4enp~ zHpjBAXO^)B895zD{Aggfr>r$Ga|34w_Xj3!PAA3(lQlREI9NH2f#&iGO^)M~V|+5X zmUEqmIY^HbkoeWW@STH~)rs+o+-7^OB1XpG$(y*-gndBrk`Ob$@-edNe_@+!&n3Fq zf#)S7W5i}HK0ZcgE1>iTW=2NF`wZ518C32v$lPVnz006>mqGC>8v~=#Ck_UYV#W&$ f0-spa7}-7rFbXr8eA41#H2EyS!Dv^c3)Bt(G*(bu diff --git a/app/__pycache__/views.cpython-313.pyc b/app/__pycache__/views.cpython-313.pyc index 598028a974bb803e9b508504a01eb84f9dfe3368..09fd6680d3dcc9eab67de3e8bc4fedc21dc1d912 100644 GIT binary patch delta 1357 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

+Ceb}vRLUqhq&WewcF4r5&|?T-FxBtInv}1 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 ZuI&(}{rmG~KcI)mDm)S!w$Cm*e*nMEZRh|1 diff --git a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc b/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-312.pyc deleted file mode 100644 index bee5bd8e44d23573852e2e1347801ba5537b4a10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2529 zcmchZ&1>UE6u?K4ElY}%c$G98Z{qB3vJ^~PJG*5IrrXjqZOXRYZ3%%EUBtvQab#pk zJCcK)LO);+J?^dl!j|~ZV|wbf(90SEHq%27J>_OdN+GAtNY=*L*dYsL1s=Wk^z@rI zZ{9rlkMVI0ufz9&U@BiR)5$wRK>MXJ@T3W0&;IzE+aF2c6|upBu?<5XWc(h_`j3=9DQ zLqtMFFeDM+#IU)Li1;(PIx&cfWRyV3mb@oMS}PUDbpcBm$x@Nl8jD#d-5TFxQSvLK zWg>ZSWRXpTG=0z-$t z>g;)YH9f?3ZHR6DybZmEW?EO{9lX>I%)a9IvM(9mwUK+FqN$gz+__i0+WBFtt!Tz% zGnZeW>Vvp-us$L|&!g$(<>lL^uoKcAiUF% zNj~sOrWuc5ACnzy_8qB&+|9f|#`)7R%lGRTnFt1$ru&L#!!o9djVsVLEp*7Mo@$XA zG(CJGysN;#58*>`LHlw0wC1T(*qFW9->~1=IyRXrtx9vKZN{MAnXpLu#@D@nb z)on;ZooXv~fcvug5eu$wUULbyD$ZxPNo7=pPQ|4n+N4Uj681%)sV~>R*!XyLgKDma z`(KHl!4boQ6Sug{_O@udR?sLSyi=?Pu4lm_^ep!Cil^U|BCCo!W?T$rR12qiyuBvm zQJ&h_cA3yYECU1=ye5&^tl^Mig3k^Xcno?nP;c}kaa#WV^PUFO`5(28GS^n-x>s*@ zuVlOW;*-}q`eIvOd|KX59O&;IrxL1m-g9oy^X~rof&M`^w|J~3nR6Pb3mltkE4dIm zGw95qZ#{>e^;@{4-}OPi7s5PCJQL>TFTsI+Z^YcWuD-;VzSP#2o=)%2{FePS+bP{| zm+p5;>+RBdr)0EC#xu+Av7`zgipKy&XVJiQl;9qA$PZ9*-Ul|DbKf#S-`CUPNqIEL2+L>Gb0H%b1{{U-oCfxu4 diff --git a/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc b/app/migrations/__pycache__/0002_customer_avatar_customer_fullname_alter_customer_dob_and_more.cpython-313.pyc deleted file mode 100644 index 9ad7026db0fbc0215a4c9dbae8635fc87c45438e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2444 zcmcguy>Ht_6hD#@MKQ81(~9Gd_@j;&p{VEvZ~>x8QOB);*lik!o&p1hvv`)7(j;@F z0^0@J04+Lq$YiO^B(yy@;PRelQ73p%OB9o})M3tl1pDV`3o*e&Of&^1 znW8SR%vQEqxNMNnQ7jNiu3GH5mo;TGXNaa^NdIA{DpDWMv z{O9RaZs*{D;BrgE_jzGyY3Y4zyLM<3hby&45PEx*ahW_NAz|&`KzB2q1Y{pO)UAiR zJmWSTr!2vAh$aGd$sPq+v}Si~>R4Ra0fkr~?eFimZzuk87TqBYCuBpwuaz@w1imbq z0BjT%Ui29aD0@nAN0CdGw_O%k)Oh@w9Rv+(VT**67dnjZkQ(KRL+ixBw$+XJ-Mo(p~)8lf@j0IHQ_B8DCydbN1$9TsBi4#%_puIExN1$6%~ z7wrJ?6M9hAxPoJAjJb+2)TQB2pbHp3X^^-Sz>Q$MW3wQ1Y?r!Tv~5wJx)yb7HVs%b zfw{6`S!+=dSterhtGk57JXcqokap<&ClGH(yyh`#*WC?zz-8+(aqAuzt!=Kv$-(62 zxrfy+Ha}h6bednsI0zxd8YW@L?CnNK1B}@sI6a3K@n`xjG=lzRW6x?F=nt9q znASr5o?V9n0Oz$`y%Bo8O?2Yh@cMf9r$UFGx{cFT!bvA}`LsDOi4y9{;jl4rEvO!& z75{+A5JtR2{Tlo!AoM*tl~I2FR1!<_v(HXdlpkv;)5ps6$&GhTMki0SiI%o-tS!8# z9c7x@-LqUKul`$Wx~sK(RBdV>pUfx4{EEp*eovog@B0x{sAlD9@_u_ diff --git a/app/models.py b/app/models.py index 86886e02..bf5251d8 100644 --- a/app/models.py +++ b/app/models.py @@ -2,16 +2,8 @@ from django.db import models import uuid from django.db.models import JSONField -# Create your models here. - -#==================================================================================== +# ==================================================================================== def generate_increment_code(model_class, prefix="CODE", padding=4, code_field="code"): - """ - model_class: Model sẽ sinh mã (ví dụ Customer, Product,...) - prefix: tiền tố (CUS, PRD...) - padding: số lượng chữ số (0001, 0010...) - code_field: tên trường lưu mã - """ last = model_class.objects.order_by('-id').first() next_id = (last.id + 1) if last else 1 return f"{prefix}{next_id:0{padding}d}" @@ -20,6 +12,7 @@ def generate_increment_code(model_class, prefix="CODE", padding=4, code_field="c class AutoCodeModel(models.Model): code_prefix = "CODE" code_padding = 5 + class Meta: abstract = True @@ -33,6 +26,10 @@ class AutoCodeModel(models.Model): super().save(*args, **kwargs) +# ==================================================================================== +# GIỮ NGUYÊN TỪ FILE GỐC — lookup tables không liên quan BĐS +# ==================================================================================== + class Money_Unit(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -91,83 +88,6 @@ class Register_Method(models.Model): db_table = 'register_method' -class Duration(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'duration' - - -class Ownership_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'ownership_type' - - -class Transaction_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_type' - - -class Transaction_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_status' - - -class Project_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'project_status' - - -class Sale_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.CharField(max_length=20, null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'sale_status' - - -class Product_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.CharField(max_length=20, null=True) - index = models.IntegerField(null=True, default=1) - sale_status = models.ForeignKey(Sale_Status, null=True, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_status' - class Customer_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -200,85 +120,6 @@ class Payment_Method(models.Model): db_table = 'payment_method' -class Investor(models.Model): - name = models.CharField(max_length=255) - tax_code = models.CharField(max_length=20, null=False) - address = models.TextField(null=False) - phone = models.CharField(max_length=15, null=True) - email = models.CharField(max_length=50, null=True) - bank_account = models.CharField(max_length=20, null=True) - bank_name = models.CharField(max_length=100, null=True) - representative = models.CharField(max_length=100, null=True) - website = models.URLField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'investor' - - -class Project(models.Model): - code = models.CharField(max_length=20, unique=True) - name = models.CharField(max_length=255) - investor = models.ForeignKey(Investor, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Project_Status, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'project' - - -class Zone_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'zone_type' - - -class Product_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_type' - - -class Land_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'land_type' - - -class Company_Type(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'company_type' - - -class Direction(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'direction' - - class Value_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -315,49 +156,6 @@ class Discount_Type(models.Model): db_table = 'discount_type' -class Gift(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - detail = models.TextField(null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'gift' - - -class Sale_Policy(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - deposit = models.DecimalField(max_digits=35, decimal_places=2) - method = models.ForeignKey(Payment_Method, null=False, related_name='+', on_delete=models.PROTECT) - enable = models.BooleanField(default=True) - contract_allocation_percentage = models.IntegerField(null=True,blank=True,default=100) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'sale_policy' - - -class Payment_Plan(models.Model): - policy = models.ForeignKey(Sale_Policy, null=False, related_name='+', on_delete=models.PROTECT) - cycle = models.IntegerField() - value = models.IntegerField() - type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) - days = models.IntegerField() - payment_note = models.TextField() - due_note = models.TextField() - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'payment_plan' - unique_together = ('policy', 'cycle') - - class User(models.Model): username = models.CharField(max_length=50, null=False, unique=True) password = models.CharField(max_length=100, null=False) @@ -388,7 +186,7 @@ class Token(models.Model): browser = models.TextField(null=False) browser_version = models.CharField(max_length=100, null=False) os = models.CharField(max_length=100, null=False) - ip = models.CharField(max_length=100, null=False) + ip = models.CharField(max_length=100, null=False) platform = models.CharField(max_length=100, null=False) expiry = models.BooleanField(default=False) city = models.CharField(max_length=100, null=True) @@ -415,6 +213,7 @@ class Setting_Type(models.Model): db_table = 'setting_type' +# GIỮ NGUYÊN — không thêm field 'type' vào Setting_Choice (file gốc không có) class Setting_Choice(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -604,16 +403,16 @@ class Lang_Choice(models.Model): class Common(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) icon = models.TextField(null=True) link = models.TextField(null=True) detail = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) + index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'common' unique_together = ('category', 'classify', 'code') @@ -622,7 +421,7 @@ class Common(models.Model): class System_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -630,9 +429,9 @@ class System_Setting(models.Model): link = models.TextField(null=True) detail = models.JSONField(null=True) detail_en = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) + index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'system_setting' unique_together = ('category', 'classify', 'code') @@ -689,7 +488,7 @@ class File(AutoCodeModel): doc_type = models.ForeignKey(Document_Type, null=True, related_name='+', on_delete=models.PROTECT) name = models.CharField(max_length=200, null=False) file = models.CharField(max_length=200, null=False) - hashtag= models.CharField(max_length=200, null=True) + hashtag = models.CharField(max_length=200, null=True) size = models.IntegerField(null=False) caption = models.CharField(max_length=200, null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) @@ -775,111 +574,6 @@ class Payment_Status(models.Model): db_table = 'payment_status' -class Dealer(AutoCodeModel): - code_prefix = "DL" - code_padding = 2 - code = models.CharField(max_length=20, null=True, unique=True) - user = models.OneToOneField(User, null=True, related_name='dealer_profile', on_delete=models.SET_NULL) - name = models.CharField(max_length=100, null=False) - phone = models.CharField(max_length=20, null=True, db_index=True) - email = models.CharField(max_length=50, null=True) - address = models.CharField(max_length=255, null=True) - sale_amount = models.DecimalField(max_digits=35, decimal_places=2,null=True) - pay_sale = models.DecimalField(max_digits=35, decimal_places=2,null=True) - commission_amount = models.DecimalField(max_digits=35, decimal_places=2,null=True) - pay_commission = models.DecimalField(max_digits=35, decimal_places=2,null=True) - commission_remain = models.DecimalField(max_digits=35, decimal_places=2,null=True) - batch_date = models.DateTimeField(null=True) - count_sale = models.IntegerField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'dealer' - - -class Cart(AutoCodeModel): - code_prefix = "GH" - code_padding = 3 - code = models.CharField(max_length=20, null=True, unique=True) - name = models.CharField(max_length=255) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'cart' - - -class Product(AutoCodeModel): - code_prefix = "SP" - code_padding = 5 - code = models.CharField(max_length=20, null=True, unique=True) - land_lot_code = models.CharField(max_length=255) - zone_code = models.CharField(max_length=255) - trade_code = models.CharField(max_length=20, null=True) - zone_type = models.ForeignKey(Zone_Type, null=False, related_name='+', on_delete=models.PROTECT) - lot_area = models.DecimalField(max_digits=35, decimal_places=2) - building_area = models.DecimalField(max_digits=35, decimal_places=2) - total_built_area = models.DecimalField(max_digits=35, decimal_places=2) - number_of_floors = models.IntegerField() - land_lot_size = models.CharField(max_length=255) - direction = models.ForeignKey(Direction, null=False, related_name='+', on_delete=models.PROTECT) - villa_model = models.CharField(max_length=255, null=True) - type = models.ForeignKey(Product_Type, null=False, related_name='+', on_delete=models.PROTECT) - project = models.ForeignKey(Project, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Product_Status, null=False, related_name='+', on_delete=models.PROTECT) - cart = models.ForeignKey(Cart, null=True, related_name='prdcart', on_delete=models.PROTECT) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) - policy = models.ForeignKey(Sale_Policy, null=True, related_name='+', on_delete=models.PROTECT) - note = models.TextField(null=True) - origin_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - price_excluding_vat = models.DecimalField(max_digits=35, decimal_places=2, null=True) - product_type = models.CharField(max_length=255, null=True) - template_name = models.CharField(max_length=255, null=True) - link = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, null=True) - locked_until = models.DateTimeField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product' - - -class Product_File(models.Model): - product = models.ForeignKey(Product, null=False, related_name='prdfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'product_file' - unique_together = ('product', 'file') - - -class Project_File(models.Model): - project = models.ForeignKey(Project, null=False, related_name='prjfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'project_file' - unique_together = ('project', 'file') - - -class Product_Note(models.Model): - ref = models.ForeignKey(Product, null=False, related_name='prdnote', on_delete=models.PROTECT) - detail = models.TextField() - files = models.JSONField(null=True) - user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - deleted = models.BooleanField(null=True, default=False) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_note' - - class News(models.Model): title = models.CharField(max_length=200, null=False) subtitle = models.CharField(max_length=500, null=False) @@ -901,6 +595,7 @@ class News(models.Model): db_table = 'news' +# GIỮ NGUYÊN — hệ thống messaging/notification class Message_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) title = models.CharField(max_length=200, null=True) @@ -946,6 +641,7 @@ class Message_Receiver(models.Model): unique_together = ('message', 'user') +# GIỮ NGUYÊN — task/schedule/alert class Schedule_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1027,7 +723,7 @@ class Group(models.Model): note = models.TextField(null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'group' unique_together = ('creator', 'name') @@ -1039,33 +735,31 @@ class User_Group(models.Model): deleted = models.BooleanField(default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'user_group' unique_together = ('group', 'user') -#=================================================================== class User_Session(models.Model): token = models.ForeignKey(Token, null=False, related_name='userlog', on_delete=models.PROTECT) - session = models.BigIntegerField(null=False) + session = models.BigIntegerField(null=False) start_time = models.DateTimeField(null=False) end_time = models.DateTimeField(null=True) duration = models.IntegerField(null=True) click_count = models.IntegerField(null=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - + class Meta: db_table = 'user_session' -#=================================================================== class User_Log(models.Model): - session = models.ForeignKey(User_Session, null=False, related_name='+', on_delete=models.PROTECT) + session = models.ForeignKey(User_Session, null=False, related_name='+', on_delete=models.PROTECT) link = models.TextField(null=False) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'user_log' @@ -1073,7 +767,7 @@ class User_Log(models.Model): class Biz_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -1083,34 +777,16 @@ class Biz_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'biz_setting' unique_together = ('category', 'classify', 'code') - -class Dealer_Setting(models.Model): - category = models.CharField(max_length=100, null=False) - classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) - vi = models.TextField(null=False) - en = models.TextField(null=True) - image = models.TextField(null=True) - icon = models.TextField(null=True) - link = models.TextField(null=True) - detail = models.JSONField(null=True) - detail_en = models.JSONField(null=True) - index = models.IntegerField(null=True, default=0) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'dealer_setting' - unique_together = ('category', 'classify', 'code') - + class Info_Setting(models.Model): category = models.CharField(max_length=100, null=False) classify = models.CharField(max_length=100, null=False) - code = models.CharField(max_length=100, null = False) + code = models.CharField(max_length=100, null=False) vi = models.TextField(null=False) en = models.TextField(null=True) image = models.TextField(null=True) @@ -1120,7 +796,7 @@ class Info_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'info_setting' unique_together = ('category', 'classify', 'code') @@ -1130,28 +806,17 @@ class Biz_Rights(models.Model): setting = models.ForeignKey(Biz_Setting, null=False, related_name='+', on_delete=models.PROTECT) user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'biz_rights' unique_together = ('setting', 'user') - -class Dealer_Rights(models.Model): - setting = models.ForeignKey(Dealer_Setting, null=False, related_name='+', on_delete=models.PROTECT) - user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'dealer_rights' - unique_together = ('setting', 'user') - - class Group_Rights(models.Model): setting = models.ForeignKey(Biz_Setting, null=False, related_name='+', on_delete=models.PROTECT) group = models.ForeignKey(User_Type, null=False, related_name='+', on_delete=models.PROTECT) is_edit = models.BooleanField(null=True, default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'group_rights' unique_together = ('setting', 'group') @@ -1170,7 +835,7 @@ class Account_Setting(models.Model): detail_en = models.JSONField(null=True) index = models.IntegerField(null=True, default=0) create_time = models.DateTimeField(null=True, auto_now_add=True) - + class Meta: db_table = 'account_setting' unique_together = ('category', 'classify', 'code') @@ -1238,8 +903,8 @@ class Company(AutoCodeModel): deleted = models.BooleanField(null=False, default=False, db_index=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'company' @@ -1267,8 +932,8 @@ class People(AutoCodeModel): deleted = models.BooleanField(null=False, default=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'people' @@ -1304,27 +969,6 @@ class Document_Audit(models.Model): db_table = 'document_audit' -class Transaction_Phase(models.Model): - code = models.CharField(max_length=30, null=True, unique=True) - name = models.CharField(max_length=100, null=False) - color = models.IntegerField(null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - - class Meta: - db_table = 'transaction_phase' - - -class Phase_Doctype(models.Model): - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='phasedoc', on_delete=models.PROTECT) - doctype = models.ForeignKey(Document_Type, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'phase_doctype' - - class Customer(AutoCodeModel): code_prefix = "KH" code_padding = 5 @@ -1341,14 +985,17 @@ class Customer(AutoCodeModel): contact_address = models.CharField(max_length=200, null=True) note = models.TextField(null=True) type = models.ForeignKey(Customer_Type, null=False, related_name='+', on_delete=models.PROTECT) - dealer = models.ForeignKey(Dealer, null=True, related_name='+', on_delete=models.PROTECT) + segment = models.ForeignKey(Customer_Segment, null=True, related_name='+', on_delete=models.PROTECT) + # Field mới cho cloud: tài khoản portal + nhân viên phụ trách + user = models.OneToOneField(User, null=True, related_name='customer_profile', on_delete=models.SET_NULL) + sale_staff = models.ForeignKey('Staff', null=True, related_name='+', on_delete=models.PROTECT) creator = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) updater = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) deleted = models.BooleanField(null=False, default=False, db_index=True) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'customer' @@ -1357,12 +1004,12 @@ class Individual(models.Model): dob = models.DateField(null=True) sex = models.ForeignKey(Sex, null=True, related_name='+', on_delete=models.PROTECT) zalo = models.CharField(max_length=20, null=True) - facebook = models.CharField(max_length=200, null=True) + facebook = models.CharField(max_length=200, null=True) company = models.ForeignKey(Company, null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'individual' @@ -1373,141 +1020,41 @@ class Organization(models.Model): website = models.CharField(max_length=200, null=True) bank_account = models.CharField(max_length=50, null=True) bank_name = models.CharField(max_length=100, null=True) - type = models.ForeignKey(Company_Type, null=True, related_name='+', on_delete=models.PROTECT) + type = models.ForeignKey('Company_Type', null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'organization' +class Company_Type(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'company_type' + + class Legal_Rep(models.Model): organization = models.ForeignKey(Organization, null=False, related_name='orgrep', on_delete=models.PROTECT) people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'legal_rep' -class Transaction(AutoCodeModel): - code_prefix = "GD" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - customer = models.ForeignKey(Customer, null=False, related_name='txncust', on_delete=models.PROTECT) - product = models.ForeignKey(Product, null=False, related_name='txnprd', on_delete=models.PROTECT) - policy = models.ForeignKey(Sale_Policy, null=False, related_name='txnplc', on_delete=models.PROTECT) - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='+', on_delete=models.PROTECT) - date = models.DateField() - origin_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - discount_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - sale_price = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - deposit_remaining = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_remain = models.DecimalField(max_digits=35, decimal_places=2, null=True) - penalty_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - early_discount_amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - payment_plan = models.JSONField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction' - - -class Transaction_Detail(AutoCodeModel): - code_prefix = "CT" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - date = models.DateField() - amount = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_remaining = models.DecimalField(max_digits=35, decimal_places=2, null=True) - amount_received = models.DecimalField(max_digits=35, decimal_places=2, null=True) - due_date = models.DateField(null=True) - customer_old = models.ForeignKey(Customer, null=True, related_name='+', on_delete=models.PROTECT) - customer_new = models.ForeignKey(Customer, null=True, related_name='+', on_delete=models.PROTECT) - transaction = models.ForeignKey(Transaction, null=False, related_name='resvtxn', on_delete=models.PROTECT) - phase = models.ForeignKey(Transaction_Phase, null=False, related_name='+', on_delete=models.PROTECT) - creator = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Transaction_Status, null=True, related_name='+', on_delete=models.PROTECT, default=1) - approver = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) - approve_time = models.DateTimeField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_detail' - - -class Transaction_Current(models.Model): - transaction = models.OneToOneField(Transaction, null=False, related_name='txncurrent', on_delete=models.PROTECT) - detail = models.ForeignKey(Transaction_Detail, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_current' - unique_together = ('transaction', 'detail') - - -class Transaction_File(models.Model): - txn_detail = models.ForeignKey(Transaction_Detail, null=False, related_name='txnfile', on_delete=models.PROTECT) - file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_file' - unique_together = ('txn_detail', 'file') - - -class Product_Booked(models.Model): - product = models.OneToOneField(Product, null=False, related_name='prdbk', on_delete=models.PROTECT) - transaction = models.ForeignKey(Transaction, null=False, related_name='transbk', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'product_booked' - unique_together = ('product', 'transaction') - - -class Contract_Status(models.Model): - code = models.CharField(max_length=30, null=False, unique=True) - name = models.CharField(max_length=100, null=False) - en = models.CharField(max_length=100, null=True) - index = models.IntegerField(null=True, default=1) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'contract_status' - - -class Contract(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='+', on_delete=models.PROTECT) - signature = models.ForeignKey(File, null=True, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Contract_Status, null=True, related_name='+', on_delete=models.PROTECT, default=1) - user = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) - link = models.UUIDField(default=uuid.uuid4, editable=False, unique=False, null=True) - document = models.JSONField(null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'contract' - - class Customer_File(models.Model): ref = models.ForeignKey(Customer, null=False, related_name='custfile', on_delete=models.PROTECT) file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'customer_file' unique_together = ('ref', 'file') @@ -1519,7 +1066,7 @@ class Customer_Note(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'customer_note' @@ -1529,11 +1076,23 @@ class People_File(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'people_file' unique_together = ('ref', 'file') +class Customer_People(models.Model): + customer = models.ForeignKey(Customer, null=False, related_name='custpeople', on_delete=models.PROTECT) + people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) + relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'customer_people' + unique_together = ('customer', 'people') + + class Payment_Type(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1611,7 +1170,7 @@ class Account_Book(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: + class Meta: db_table = 'account_book' unique_together = ('system_date', 'account') @@ -1621,7 +1180,7 @@ class Interest_Base(models.Model): name = models.CharField(max_length=200, null=True) en = models.CharField(max_length=100, null=True) index = models.IntegerField(null=True, default=1) - detail = models.TextField(null=False) + detail = models.TextField(null=False) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) @@ -1669,7 +1228,10 @@ class Internal_Entry(AutoCodeModel): date = models.DateField(null=False) ref = models.CharField(max_length=30, null=True, unique=True) customer = models.ForeignKey(Customer, null=True, related_name='entrycus', on_delete=models.PROTECT) - product = models.ForeignKey(Product, null=True, related_name='+', on_delete=models.PROTECT) + # Cloud: thay product → subscription + subscription = models.ForeignKey('Subscription', null=True, related_name='+', on_delete=models.PROTECT) + invoice = models.ForeignKey('Invoice', null=True, related_name='+', on_delete=models.PROTECT) + provider_invoice = models.ForeignKey('Provider_Invoice', null=True, related_name='+', on_delete=models.PROTECT) allocation_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) allocation_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) allocation_detail = models.JSONField(null=True) @@ -1685,52 +1247,33 @@ class Entry_File(models.Model): file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'entry_file' unique_together = ('ref', 'file') -class Payment_Schedule(AutoCodeModel): - code_prefix = "SH" - code_padding = 5 - code = models.CharField(max_length=30, null=True, unique=True) - from_date = models.DateField(null=False) - to_date = models.DateField(null=False) - amount = models.DecimalField(max_digits=35, decimal_places=2) - paid_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - amount_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) - remain_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - cycle = models.IntegerField(null=False) - cycle_days = models.IntegerField(null=False) - txn_detail = models.ForeignKey(Transaction_Detail, null=False, related_name='psh', on_delete=models.PROTECT) - type = models.ForeignKey(Payment_Type, null=False, related_name='+', on_delete=models.PROTECT) - status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) - updater = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) - entry = models.JSONField(null=True) - detail = models.JSONField(null=True) - batch_date = models.DateField(null=True) - ovd_days = models.IntegerField(null=True) - penalty_amount = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_paid = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_remain = models.DecimalField(null=True, max_digits=35, decimal_places=2) - penalty_reduce = models.DecimalField(null=True, max_digits=35, decimal_places=2) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'payment_schedule' - +# GIỮ NGUYÊN — Invoice gốc (liên kết với payment_schedule BĐS, giữ để không break data cũ) class Invoice(models.Model): link = models.CharField(max_length=100, null=True) ref_code = models.CharField(max_length=30, null=False) amount = models.DecimalField(max_digits=35, decimal_places=2) - payment = models.ForeignKey(Payment_Schedule, null=False, related_name='invoice', on_delete=models.PROTECT) note = models.CharField(max_length=100, null=True) - + # Cloud: thêm fields mới (nullable để không break data cũ) + customer = models.ForeignKey(Customer, null=True, related_name='invoices', on_delete=models.PROTECT) + issue_date = models.DateField(null=True) + due_date = models.DateField(null=True) + paid_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + remain_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=True, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=True, related_name='+', on_delete=models.PROTECT) + status = models.ForeignKey(Payment_Status, null=True, related_name='+', on_delete=models.PROTECT) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + class Meta: db_table = 'invoice' - - + class Phone_Otp(models.Model): code = models.CharField(max_length=30, null=False, unique=True) @@ -1785,7 +1328,7 @@ class Staff_Status(models.Model): class Meta: db_table = 'staff_status' - + class Staff(models.Model): code = models.CharField(max_length=20, null=False, unique=True, db_index=True) @@ -1813,29 +1356,17 @@ class Staff(models.Model): user = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: + + class Meta: db_table = 'staff' -class Customer_People(models.Model): - customer = models.ForeignKey(Customer, null=False, related_name='custpeople', on_delete=models.PROTECT) - people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) - relation = models.ForeignKey(Relation, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'customer_people' - unique_together = ('customer', 'people') - - class Staff_File(models.Model): ref = models.ForeignKey(Staff, null=False, related_name='stafffile', on_delete=models.PROTECT) file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'staff_file' unique_together = ('ref', 'file') @@ -1860,7 +1391,7 @@ class Batch_Log(models.Model): status = models.ForeignKey(Task_Status, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'batch_log' @@ -1888,7 +1419,7 @@ class Customer_Sms(models.Model): user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) create_time = models.DateTimeField(null=True, auto_now_add=True) - class Meta: + class Meta: db_table = 'customer_sms' @@ -1919,13 +1450,15 @@ class Ssh(models.Model): class Meta: db_table = 'ssh' - + + class Document_Configuration(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=200, null=False) template_path = models.CharField(max_length=200, null=False) - mappings = models.JSONField (default=list) + mappings = models.JSONField(default=list) update_time = models.DateTimeField(null=True, auto_now=True) + class Meta: db_table = 'document_configuration' @@ -1959,7 +1492,6 @@ class Layer_Setting(AutoCodeModel): db_table = 'layer_setting' -#========================================================================== class Send_Status(models.Model): code = models.CharField(max_length=30, null=False, unique=True) name = models.CharField(max_length=100, null=False) @@ -1968,6 +1500,7 @@ class Send_Status(models.Model): class Meta: db_table = 'send_status' + class Email_Setup(models.Model): email = models.CharField(max_length=100, null=False, unique=True) password = models.CharField(max_length=30, null=False) @@ -1992,6 +1525,7 @@ class Email_Sent(models.Model): class Meta: db_table = 'email_sent' + class Email_List(models.Model): name = models.CharField(max_length=200, null=False, unique=True) email = models.TextField(null=False) @@ -2001,6 +1535,7 @@ class Email_List(models.Model): class Meta: db_table = 'email_list' + class Email_Template(models.Model): name = models.CharField(max_length=200, null=False, unique=True) content = models.JSONField(null=False) @@ -2013,7 +1548,7 @@ class Email_Template(models.Model): class Email_Job(models.Model): name = models.CharField(max_length=200, null=False) - model_name = models.CharField(max_length=100, null=False, help_text="e.g., app.Transaction_Detail") + model_name = models.CharField(max_length=100, null=False, help_text="e.g., app.Subscription") template = models.ForeignKey(Email_Template, null=False, related_name='+', on_delete=models.PROTECT) trigger_on_create = models.BooleanField(default=False) trigger_on_update = models.BooleanField(default=False) @@ -2021,80 +1556,37 @@ class Email_Job(models.Model): create_time = models.DateTimeField(null=True, auto_now_add=True) update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: db_table = 'email_job' - -class Transaction_Gift(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='txngift', on_delete=models.PROTECT) - gift = models.ForeignKey(Gift, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_gift' - -class Transaction_Discount(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='txndiscount', on_delete=models.PROTECT) - discount = models.ForeignKey(Discount_Type, null=False, related_name='+', on_delete=models.PROTECT) - type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) - value = models.DecimalField(max_digits=35, decimal_places=2, null=True) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - - class Meta: - db_table = 'transaction_discount' -class Co_Ownership(models.Model): - transaction = models.ForeignKey(Transaction, null=False, related_name='co_op', on_delete=models.PROTECT) - people = models.ForeignKey(People, null=False, related_name='+', on_delete=models.PROTECT) - create_time = models.DateTimeField(null=True, auto_now_add=True) - update_time = models.DateTimeField(null=True, auto_now=True) - class Meta: - db_table = 'co_ownership' - unique_together = ('transaction', 'people') - +# GIỮ NGUYÊN — Workflow engine class Workflow(models.Model): - """ - Bảng Workflow: Quản lý các luồng chính (multi-flow cho dự án). - Ví dụ: 'RESERVATION' cho giữ chỗ, 'LOAN_APPROVAL' cho duyệt vay. - """ - code = models.CharField(max_length=50, unique=True) # e.g., 'RESERVATION' + code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) is_active = models.BooleanField(default=True) - initial_step = models.ForeignKey('StepAction', null=True, blank=True, on_delete=models.SET_NULL, related_name='initial_workflows') + initial_step = models.ForeignKey('StepAction', null=True, blank=True, on_delete=models.SET_NULL, + related_name='initial_workflows') create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) class Meta: db_table = 'workflow' - verbose_name = 'Workflow' - verbose_name_plural = 'Workflows' - def __str__(self): - return f"{self.name} ({self.code})" class StepAction(models.Model): - """ - Bảng Step/Action: Định nghĩa các bước/hành động trong Workflow. - Liên kết với Workflow, lưu chi tiết actions (JSON: list of dicts). - Ví dụ: Step 'create_reservation' có actions: [{'type': 'create_record', 'model': 'app.Transaction', 'fields': {...}}] - """ workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, related_name='steps') - step_code = models.CharField(max_length=50, unique=True) # e.g., 'create_reservation', 'approve_detail' + step_code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) - order = models.PositiveIntegerField(default=0) # Thứ tự chạy trong workflow + order = models.PositiveIntegerField(default=0) trigger_event = models.CharField(max_length=50, choices=[ ('create', 'Create'), ('update', 'Update'), ('approve', 'Approve'), ('advance', 'Advance'), ('confirm', 'Confirm'), ('custom', 'Custom') ]) - target_model = models.CharField(max_length=100, blank=True, help_text="Model chính, e.g., 'app.Transaction'") - # Actions: List chi tiết hành động (dynamic, multi-model) - actions = JSONField(default=list, blank=True) # e.g., [{'type': 'create_record', 'model': 'app.Product', 'fields': {'status': 'reserved'}}] - # Config extra: e.g., {'auto_advance': True, 'required_fields': ['customer_id']} + target_model = models.CharField(max_length=100, blank=True) + actions = JSONField(default=list, blank=True) config = JSONField(default=dict, blank=True) is_active = models.BooleanField(default=True) create_time = models.DateTimeField(auto_now_add=True) @@ -2104,33 +1596,35 @@ class StepAction(models.Model): db_table = 'step_action' ordering = ['order'] unique_together = ('workflow', 'step_code') - verbose_name = 'Step/Action' - verbose_name_plural = 'Steps/Actions' - def __str__(self): - return f"{self.workflow.name} - {self.name} ({self.step_code})" -class Rule(models.Model): - """ - Bảng Rule: Định nghĩa điều kiện (conditions) và quy luật ràng buộc cho Step/Action. - Liên kết với StepAction, dùng cho validation/constraints. - Ví dụ: Condition: {'field': 'a_a', 'operator': '==', 'value': 'specific_value'} → Chỉ thực hiện nếu match. - Constraint: {'after_action': 'create_record', 'must_update': {'model': 'app.Product', 'field': 'status', 'to': 'reserved'}} - """ - step_action = models.ForeignKey(StepAction, on_delete=models.CASCADE, related_name='rules') - rule_code = models.CharField(max_length=50, unique=True) # e.g., 'validate_customer_vip' +class Utility(models.Model): + code = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=200) description = models.TextField(blank=True) - # Conditions: List conditions để check trước action - conditions = JSONField(default=list, blank=True) # e.g., [ - # {'field': 'customer__segment__code', 'operator': '==', 'value': 'VIP', 'related_model': 'app.Customer'}, - # {'field': 'amount', 'operator': '>=', 'value': 1000000}] - # Constraints: Quy luật sau/before action (ràng buộc) - constraints = JSONField(default=list, blank=True) # e.g., [ - # {'trigger': 'after_create', 'type': 'must_update', 'model': 'app.Transaction_Detail', 'fields': {'status': 'new'}}, - # {'trigger': 'before_approve', 'type': 'require_approval', 'min_count': 2}] - # Utility linkage: Liên kết với Utility nếu cần reuse - utility = models.ForeignKey('Utility', null=True, blank=True, on_delete=models.SET_NULL, related_name='rules') + utility_type = models.CharField(max_length=50, choices=[ + ('email', 'Email API'), ('crud', 'Data CRUD'), ('payment', 'Payment API'), + ('document', 'Document Gen'), ('notification', 'Notification'), ('custom', 'Custom') + ]) + api_config = JSONField(default=dict, blank=True) + params_template = JSONField(default=dict, blank=True) + integration_module = models.CharField(max_length=100, blank=True) + is_active = models.BooleanField(default=True) + create_time = models.DateTimeField(auto_now_add=True) + update_time = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'utility' + + +class Rule(models.Model): + step_action = models.ForeignKey(StepAction, on_delete=models.CASCADE, related_name='rules') + rule_code = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + conditions = JSONField(default=list, blank=True) + constraints = JSONField(default=list, blank=True) + utility = models.ForeignKey(Utility, null=True, blank=True, on_delete=models.SET_NULL, related_name='rules') is_active = models.BooleanField(default=True) create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) @@ -2138,39 +1632,531 @@ class Rule(models.Model): class Meta: db_table = 'rule' unique_together = ('step_action', 'rule_code') - verbose_name = 'Rule' - verbose_name_plural = 'Rules' - def __str__(self): - return f"{self.step_action.name} - {self.name} ({self.rule_code})" -class Utility(models.Model): - """ - Bảng Utility: Chứa các action có sẵn, liên kết với hệ thống khác (e.g., API mail, CRUD data). - Dùng để reuse trong StepAction/Rule (e.g., gửi mail → gọi send_email API; create record → gọi /data/ endpoint). - """ - code = models.CharField(max_length=50, unique=True) # e.g., 'SEND_EMAIL', 'CRUD_DATA' - name = models.CharField(max_length=200) - description = models.TextField(blank=True) - # Type: Phân loại utility (e.g., 'email', 'crud', 'payment') - utility_type = models.CharField(max_length=50, choices=[ - ('email', 'Email API'), ('crud', 'Data CRUD'), ('payment', 'Payment API'), - ('document', 'Document Gen'), ('notification', 'Notification'), ('custom', 'Custom') - ]) - # Endpoint/API linkage: e.g., {'url': '/send-email/', 'method': 'POST', 'params': {'template': '[template]'}} - api_config = JSONField(default=dict, blank=True) # Config gọi API (dynamic placeholders như [user_id]) - # Params template: Mẫu params khi gọi - params_template = JSONField(default=dict, blank=True) # e.g., {'receiver': '[customer_email]', 'subject': 'Approval'} - # External integration: e.g., liên kết với Email_Job hoặc account_entry_api - integration_module = models.CharField(max_length=100, blank=True, help_text="e.g., 'app.email.send_email'") - is_active = models.BooleanField(default=True) - create_time = models.DateTimeField(auto_now_add=True) - update_time = models.DateTimeField(auto_now=True) +# ==================================================================================== +# CLOUD DOMAIN — PROVIDER (NHÀ CUNG CẤP HẠ TẦNG) +# ==================================================================================== + +class Provider(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + website = models.URLField(null=True) + api_endpoint = models.CharField(max_length=200, null=True) + partner_id = models.CharField(max_length=100, null=True) + contact_email = models.CharField(max_length=100, null=True) + note = models.TextField(null=True) + active = models.BooleanField(default=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) class Meta: - db_table = 'utility' - verbose_name = 'Utility' - verbose_name_plural = 'Utilities' + db_table = 'provider' - def __str__(self): - return f"{self.name} ({self.code}) - Type: {self.utility_type}" + +class Provider_Credential(models.Model): + provider = models.ForeignKey(Provider, null=False, related_name='credentials', on_delete=models.PROTECT) + label = models.CharField(max_length=100, null=False) + api_key = models.CharField(max_length=500, null=False) + api_secret = models.CharField(max_length=500, null=True) + active = models.BooleanField(default=True) + note = models.TextField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'provider_credential' + + +class Datacenter(models.Model): + provider = models.ForeignKey(Provider, null=False, related_name='datacenters', on_delete=models.PROTECT) + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + region = models.CharField(max_length=100, null=True) + country = models.ForeignKey(Country, null=True, related_name='+', on_delete=models.PROTECT) + provider_location_id = models.CharField(max_length=50, null=True) + active = models.BooleanField(default=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'datacenter' + + +# ==================================================================================== +# CLOUD DOMAIN — SERVICE CATALOG +# ==================================================================================== + +class Service_Category(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + icon = models.CharField(max_length=100, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'service_category' + + +class Service_Plan(models.Model): + BILLING_SUBSCRIPTION = 'SUBSCRIPTION' + BILLING_PAYG = 'PAYG' + BILLING_BOTH = 'BOTH' + BILLING_TYPE_CHOICES = [ + (BILLING_SUBSCRIPTION, 'Subscription'), + (BILLING_PAYG, 'Pay-as-you-go'), + (BILLING_BOTH, 'Cả hai'), + ] + + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=200, null=False) + category = models.ForeignKey(Service_Category, null=False, related_name='plans', on_delete=models.PROTECT) + provider = models.ForeignKey(Provider, null=False, related_name='plans', on_delete=models.PROTECT) + billing_type = models.CharField(max_length=20, choices=BILLING_TYPE_CHOICES, default=BILLING_SUBSCRIPTION) + cpu = models.IntegerField(null=True) + ram_gb = models.IntegerField(null=True) + disk_gb = models.IntegerField(null=True) + bandwidth_tb = models.DecimalField(max_digits=8, decimal_places=2, null=True) + storage_gb = models.IntegerField(null=True) + extra_specs = models.JSONField(null=True) + provider_price = models.DecimalField(max_digits=20, decimal_places=4, null=False) + provider_currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + provider_plan_id = models.CharField(max_length=100, null=True) + sell_price = models.DecimalField(max_digits=20, decimal_places=2, null=False) + sell_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=True) + sell_currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + active = models.BooleanField(default=True) + index = models.IntegerField(null=True, default=1) + note = models.TextField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'service_plan' + + +class Plan_Datacenter(models.Model): + plan = models.ForeignKey(Service_Plan, null=False, related_name='plan_dcs', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'plan_datacenter' + unique_together = ('plan', 'datacenter') + + +class Pricing_Tier(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + months = models.IntegerField(null=False) + discount_percent = models.DecimalField(max_digits=5, decimal_places=2, default=0) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'pricing_tier' + + +# ==================================================================================== +# CLOUD DOMAIN — SUBSCRIPTION +# ==================================================================================== + +class Subscription_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_status' + + +class Subscription(AutoCodeModel): + BILLING_SUBSCRIPTION = 'SUBSCRIPTION' + BILLING_PAYG = 'PAYG' + BILLING_TYPE_CHOICES = [ + (BILLING_SUBSCRIPTION, 'Subscription'), + (BILLING_PAYG, 'Pay-as-you-go'), + ] + + code_prefix = "SUB" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='subscriptions', on_delete=models.PROTECT) + plan = models.ForeignKey(Service_Plan, null=False, related_name='subscriptions', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + billing_type = models.CharField(max_length=20, choices=BILLING_TYPE_CHOICES, default=BILLING_SUBSCRIPTION) + pricing_tier = models.ForeignKey(Pricing_Tier, null=True, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=True, related_name='+', on_delete=models.PROTECT) + unit_price = models.DecimalField(max_digits=20, decimal_places=2, null=True) + unit_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=True) + discount_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + final_price = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + start_date = models.DateField(null=False) + end_date = models.DateField(null=True) + next_billing_date = models.DateField(null=True) + auto_renew = models.BooleanField(default=True) + status = models.ForeignKey(Subscription_Status, null=False, related_name='+', on_delete=models.PROTECT) + note = models.TextField(null=True) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + updater = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'subscription' + + +class Subscription_Discount(models.Model): + subscription = models.ForeignKey(Subscription, null=False, related_name='discounts', on_delete=models.PROTECT) + discount = models.ForeignKey(Discount_Type, null=False, related_name='+', on_delete=models.PROTECT) + type = models.ForeignKey(Value_Type, null=False, related_name='+', on_delete=models.PROTECT) + value = models.DecimalField(max_digits=20, decimal_places=2, null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_discount' + + +class Subscription_File(models.Model): + ref = models.ForeignKey(Subscription, null=False, related_name='subfile', on_delete=models.PROTECT) + file = models.ForeignKey(File, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'subscription_file' + unique_together = ('ref', 'file') + + +class Subscription_Note(models.Model): + ref = models.ForeignKey(Subscription, null=False, related_name='subnote', on_delete=models.PROTECT) + detail = models.TextField(null=False) + files = models.JSONField(null=True) + user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) + deleted = models.BooleanField(null=True, default=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'subscription_note' + + +# ==================================================================================== +# CLOUD DOMAIN — WALLET (PAYG) +# ==================================================================================== + +class Wallet_Transaction_Type(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'wallet_transaction_type' + + +class Customer_Wallet(models.Model): + PREPAID = 'PREPAID' + POSTPAID = 'POSTPAID' + BILLING_MODE_CHOICES = [ + (PREPAID, 'Prepaid — Nạp trước'), + (POSTPAID, 'Postpaid — Trả sau cuối tháng'), + ] + + customer = models.OneToOneField(Customer, null=False, related_name='wallet', on_delete=models.PROTECT) + billing_mode = models.CharField(max_length=20, choices=BILLING_MODE_CHOICES, default=PREPAID) + balance = models.DecimalField(max_digits=20, decimal_places=2, null=False, default=0) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + low_balance_threshold = models.DecimalField(max_digits=20, decimal_places=2, null=True) + credit_limit = models.DecimalField(max_digits=20, decimal_places=2, null=True) + billing_day = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'customer_wallet' + + +class Wallet_Transaction(AutoCodeModel): + code_prefix = "WT" + code_padding = 7 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + wallet = models.ForeignKey(Customer_Wallet, null=False, related_name='transactions', on_delete=models.PROTECT) + type = models.ForeignKey(Wallet_Transaction_Type, null=False, related_name='+', on_delete=models.PROTECT) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + balance_before = models.DecimalField(max_digits=20, decimal_places=2, null=False) + balance_after = models.DecimalField(max_digits=20, decimal_places=2, null=False) + description = models.CharField(max_length=300, null=False) + subscription = models.ForeignKey(Subscription, null=True, related_name='wallet_txns', on_delete=models.PROTECT) + invoice = models.ForeignKey(Invoice, null=True, related_name='wallet_txns', on_delete=models.PROTECT) + ref_code = models.CharField(max_length=100, null=True) + performed_by = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'wallet_transaction' + + +# ==================================================================================== +# CLOUD DOMAIN — CLOUD INSTANCE +# ==================================================================================== + +class Instance_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'instance_status' + + +class Cloud_Instance(AutoCodeModel): + code_prefix = "INS" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + subscription = models.ForeignKey(Subscription, null=False, related_name='instances', on_delete=models.PROTECT) + provider = models.ForeignKey(Provider, null=False, related_name='+', on_delete=models.PROTECT) + datacenter = models.ForeignKey(Datacenter, null=False, related_name='+', on_delete=models.PROTECT) + provider_instance_id = models.CharField(max_length=100, null=True, db_index=True) + provider_instance_name = models.CharField(max_length=200, null=True) + ip_address = models.GenericIPAddressField(null=True) + ipv6_address = models.CharField(max_length=50, null=True) + hostname = models.CharField(max_length=200, null=True) + cpu = models.IntegerField(null=True) + ram_gb = models.IntegerField(null=True) + disk_gb = models.IntegerField(null=True) + status = models.ForeignKey(Instance_Status, null=False, related_name='+', on_delete=models.PROTECT) + provisioned_at = models.DateTimeField(null=True) + terminated_at = models.DateTimeField(null=True) + root_password = models.CharField(max_length=300, null=True) + ssh_key = models.TextField(null=True) + note = models.TextField(null=True) + extra_info = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'cloud_instance' + + +class Instance_Log(models.Model): + instance = models.ForeignKey(Cloud_Instance, null=False, related_name='logs', on_delete=models.PROTECT) + action = models.CharField(max_length=50, null=False) + status = models.CharField(max_length=30, null=False) + performed_by = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + detail = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'instance_log' + + +# ==================================================================================== +# CLOUD DOMAIN — USAGE RECORD (PAYG) +# ==================================================================================== + +class Usage_Record(models.Model): + subscription = models.ForeignKey(Subscription, null=False, related_name='usage_records', + on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='usage_records', + on_delete=models.PROTECT) + period_from = models.DateTimeField(null=False) + period_to = models.DateTimeField(null=False) + hours = models.DecimalField(max_digits=10, decimal_places=4, null=False) + unit_price_hourly = models.DecimalField(max_digits=20, decimal_places=6, null=False) + amount = models.DecimalField(max_digits=20, decimal_places=4, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + charged = models.BooleanField(default=False) + wallet_transaction = models.ForeignKey(Wallet_Transaction, null=True, related_name='usage_records', + on_delete=models.PROTECT) + postpaid_invoice = models.ForeignKey(Invoice, null=True, related_name='usage_records', + on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'usage_record' + indexes = [ + models.Index(fields=['subscription', 'period_from']), + ] + + +# ==================================================================================== +# CLOUD DOMAIN — INVOICE LINES & RECEIPTS +# ==================================================================================== + +class Invoice_Line(models.Model): + invoice = models.ForeignKey(Invoice, null=False, related_name='lines', on_delete=models.PROTECT) + subscription = models.ForeignKey(Subscription, null=False, related_name='invoice_lines', + on_delete=models.PROTECT) + description = models.CharField(max_length=300, null=False) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + unit_price = models.DecimalField(max_digits=20, decimal_places=2, null=False) + quantity = models.DecimalField(max_digits=10, decimal_places=2, default=1) + discount_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True, default=0) + line_total = models.DecimalField(max_digits=20, decimal_places=2, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'invoice_line' + + +class Payment_Receipt(AutoCodeModel): + code_prefix = "PT" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + invoice = models.ForeignKey(Invoice, null=False, related_name='receipts', on_delete=models.PROTECT) + customer = models.ForeignKey(Customer, null=False, related_name='receipts', on_delete=models.PROTECT) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + payment_method = models.ForeignKey(Payment_Method, null=False, related_name='+', on_delete=models.PROTECT) + payment_date = models.DateField(null=False) + ref_code = models.CharField(max_length=100, null=True) + note = models.TextField(null=True) + status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) + approver = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + approve_time = models.DateTimeField(null=True) + creator = models.ForeignKey(User, null=True, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'payment_receipt' + + +# ==================================================================================== +# CLOUD DOMAIN — POSTPAID BILLING CYCLE +# ==================================================================================== + +class Postpaid_Billing_Cycle(AutoCodeModel): + OPEN = 'OPEN' + INVOICED = 'INVOICED' + PAID = 'PAID' + STATUS_CHOICES = [ + (OPEN, 'Đang tích lũy usage'), + (INVOICED, 'Đã xuất Invoice'), + (PAID, 'Đã thanh toán'), + ] + + code_prefix = "PBC" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='billing_cycles', on_delete=models.PROTECT) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + total_usage_amount = models.DecimalField(max_digits=20, decimal_places=2, null=True) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=OPEN) + invoice = models.OneToOneField(Invoice, null=True, related_name='billing_cycle', on_delete=models.PROTECT) + closed_at = models.DateTimeField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'postpaid_billing_cycle' + unique_together = ('customer', 'period_from') + + +# ==================================================================================== +# CLOUD DOMAIN — PROVIDER COST +# ==================================================================================== + +class Provider_Invoice(AutoCodeModel): + code_prefix = "PVI" + code_padding = 5 + code = models.CharField(max_length=20, null=True, unique=True) + provider = models.ForeignKey(Provider, null=False, related_name='provider_invoices', on_delete=models.PROTECT) + period_from = models.DateField(null=False) + period_to = models.DateField(null=False) + total_amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + currency = models.ForeignKey(Currency, null=False, related_name='+', on_delete=models.PROTECT) + document = models.ForeignKey(File, null=True, related_name='+', on_delete=models.PROTECT) + note = models.TextField(null=True) + status = models.ForeignKey(Payment_Status, null=False, related_name='+', on_delete=models.PROTECT) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'provider_invoice' + + +class Provider_Invoice_Line(models.Model): + provider_invoice = models.ForeignKey(Provider_Invoice, null=False, related_name='lines', + on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='+', on_delete=models.PROTECT) + description = models.CharField(max_length=300, null=False) + amount = models.DecimalField(max_digits=20, decimal_places=2, null=False) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'provider_invoice_line' + + +# ==================================================================================== +# CLOUD DOMAIN — SUPPORT TICKET +# ==================================================================================== + +class Ticket_Priority(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_priority' + + +class Ticket_Status(models.Model): + code = models.CharField(max_length=30, null=False, unique=True) + name = models.CharField(max_length=100, null=False) + en = models.CharField(max_length=100, null=True) + color = models.CharField(max_length=20, null=True) + index = models.IntegerField(null=True, default=1) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_status' + + +class Support_Ticket(AutoCodeModel): + code_prefix = "TK" + code_padding = 6 + code = models.CharField(max_length=20, null=True, unique=True, db_index=True) + customer = models.ForeignKey(Customer, null=False, related_name='tickets', on_delete=models.PROTECT) + subscription = models.ForeignKey(Subscription, null=True, related_name='tickets', on_delete=models.PROTECT) + instance = models.ForeignKey(Cloud_Instance, null=True, related_name='tickets', on_delete=models.PROTECT) + title = models.CharField(max_length=300, null=False) + description = models.TextField(null=False) + priority = models.ForeignKey(Ticket_Priority, null=False, related_name='+', on_delete=models.PROTECT) + status = models.ForeignKey(Ticket_Status, null=False, related_name='+', on_delete=models.PROTECT) + assignee = models.ForeignKey(Staff, null=True, related_name='+', on_delete=models.PROTECT) + resolved_at = models.DateTimeField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + update_time = models.DateTimeField(null=True, auto_now=True) + + class Meta: + db_table = 'support_ticket' + + +class Ticket_Reply(models.Model): + ticket = models.ForeignKey(Support_Ticket, null=False, related_name='replies', on_delete=models.PROTECT) + content = models.TextField(null=False) + is_internal = models.BooleanField(default=False) + user = models.ForeignKey(User, null=False, related_name='+', on_delete=models.PROTECT) + files = models.JSONField(null=True) + create_time = models.DateTimeField(null=True, auto_now_add=True) + + class Meta: + db_table = 'ticket_reply' \ No newline at end of file diff --git a/app/signals.py b/app/signals.py index c9576130..d0106b06 100644 --- a/app/signals.py +++ b/app/signals.py @@ -4,12 +4,8 @@ from django.apps import apps from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from django.db import transaction - -# Import hàm get_serializer đã có from .views import get_serializer -# Danh sách các model không muốn theo dõi để tránh "nhiễu" -# Ví dụ: các model của admin, session, hoặc các model log không cần real-time MODELS_TO_IGNORE = ['LogEntry', 'Session', 'ContentType', 'AdminLog', 'Permission', 'Group', 'Token', 'Phone_Otp'] def send_model_update(instance, change_type): @@ -19,35 +15,29 @@ def send_model_update(instance, change_type): model_class = instance.__class__ model_name = model_class._meta.model_name - # Bỏ qua các model trong danh sách ignore if model_class.__name__ in MODELS_TO_IGNORE: return - # Lấy serializer một cách linh động _model, serializer_class = get_serializer(model_name) if not serializer_class: print(f"Warning: No serializer found for model {model_name}. Cannot send update.") return - # Serialize instance đã thay đổi - # Đối với 'delete', instance vẫn còn tồn tại trong bộ nhớ tại thời điểm này serializer = serializer_class(instance) - # Chuẩn bị payload để gửi đi payload = { "name": model_name, "change_type": change_type, "record": serializer.data } - # Gửi tin nhắn đến group tương ứng channel_layer = get_channel_layer() group_name = f"model_{model_name}_updates" async_to_sync(channel_layer.group_send)( group_name, { - "type": "realtime.update", # Khớp với tên phương thức trong DataConsumer + "type": "realtime.update", "payload": payload } ) @@ -59,14 +49,10 @@ def generic_post_save_handler(sender, instance, created, **kwargs): def send_update_after_commit(): change_type = "created" if created else "updated" try: - # Re-fetch the instance to ensure we have the committed data refreshed_instance = sender.objects.get(pk=instance.pk) send_model_update(refreshed_instance, change_type) except sender.DoesNotExist: - # Object đã bị xóa (ví dụ: delete_entry vừa xóa Internal_Entry) - # Bỏ qua việc gửi update, hoặc gửi thông báo "deleted" nếu cần print(f"Object {sender.__name__} {instance.pk} đã bị xóa, bỏ qua gửi update.") - # Optional: vẫn gửi "deleted" để frontend biết object không còn send_model_update(instance, "deleted") except Exception as exc: print(f"Lỗi trong send_update_after_commit: {exc}") @@ -77,9 +63,7 @@ def generic_post_delete_handler(sender, instance, **kwargs): """ Hàm xử lý chung cho tín hiệu post_delete từ BẤT KỲ model nào. """ - # For delete, the action happens immediately, so on_commit is not strictly necessary - # unless the delete is part of a larger transaction that could be rolled back. - # It's safer to use it anyway. + def send_delete_after_commit(): send_model_update(instance, "deleted") @@ -95,4 +79,3 @@ def connect_signals(): print("Connected generic signals for real-time updates.") -# File apps.py của bạn đã gọi hàm connect_signals() này rồi, nên mọi thứ sẽ tự động hoạt động. \ No newline at end of file diff --git a/app/views.py b/app/views.py index b74f7325..59120c27 100644 --- a/app/views.py +++ b/app/views.py @@ -1041,277 +1041,6 @@ def set_token_expiry(request): return Response(status = status.HTTP_200_OK) -#============================================================================= -class ExcelImportAPIView(APIView): - parser_classes = (MultiPartParser, FormParser) - - def post(self, request, format=None): - excel_file = request.FILES.get('file') - if not excel_file: - return Response({'error': 'No Excel file provided (key "file" not found)'}, status=status.HTTP_400_BAD_REQUEST) - - config_str = request.data.get('config') - if not config_str: - return Response({'error': 'No configuration provided (key "config" not found)'}, status=status.HTTP_400_BAD_REQUEST) - - try: - config = json.loads(config_str) - except json.JSONDecodeError: - return Response({'error': 'Invalid JSON configuration'}, status=status.HTTP_400_BAD_REQUEST) - - model_name = config.get('model_name') - mappings = config.get('mappings', []) - import_mode = config.get('import_mode', 'insert_only') - - header_row_excel = config.get('header_row_index', 1) - header_index = max(0, header_row_excel - 1) - - # LẤY VÀ PHÂN TÍCH TRƯỜNG UNIQUE KEY - unique_fields_config = config.get('unique_fields', 'code') - if isinstance(unique_fields_config, str): - UNIQUE_KEY_FIELDS = [unique_fields_config] - elif isinstance(unique_fields_config, list): - UNIQUE_KEY_FIELDS = unique_fields_config - else: - return Response({'error': 'Invalid format for unique_fields. Must be a string or a list of strings.'}, status=status.HTTP_400_BAD_REQUEST) - - if not model_name or not mappings: - return Response({'error': 'model_name or mappings missing in configuration'}, status=status.HTTP_400_BAD_REQUEST) - - try: - TargetModel = apps.get_model('app', model_name) - except LookupError: - return Response({'error': f'Model "{model_name}" not found in app'}, status=status.HTTP_400_BAD_REQUEST) - - related_models_cache = {} - for mapping in mappings: - if 'foreign_key' in mapping: - fk_config = mapping['foreign_key'] - related_model_name = fk_config.get('model_name') - if related_model_name: - try: - related_models_cache[related_model_name] = apps.get_model('app', related_model_name) - except LookupError: - return Response({'error': f"Related model '{related_model_name}' not found for mapping '{mapping.get('excel_column')}'"}, status=status.HTTP_400_BAD_REQUEST) - - try: - file_stream = io.BytesIO(excel_file.read()) - if excel_file.name.lower().endswith(('.xlsx', '.xls')): - df = pd.read_excel(file_stream, header=header_index) - else: - df = pd.read_csv(file_stream, header=header_index) - except Exception as e: - return Response({'error': f'Error reading file: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) - - cleaned_columns = [] - for col in df.columns: - col_str = str(col).strip() - col_str = col_str.replace('\n', ' ').strip() - col_str = re.sub(r'\s*\([^)]*\)', '', col_str).strip() - col_str = ' '.join(col_str.split()) - cleaned_columns.append(col_str) - - df.columns = cleaned_columns - df.reset_index(drop=True, inplace=True) - - # Caching Foreign Key objects - related_obj_cache = {} - for related_name, RelatedModel in related_models_cache.items(): - lookup_field = next((m['foreign_key']['lookup_field'] for m in mappings if 'foreign_key' in m and m['foreign_key']['model_name'] == related_name), None) - if lookup_field: - try: - related_obj_cache[related_name] = { - str(getattr(obj, lookup_field)).strip().lower(): obj - for obj in RelatedModel.objects.all() - } - if 'pk' not in related_obj_cache[related_name]: - related_obj_cache[related_name].update({ - str(obj.pk): obj for obj in RelatedModel.objects.all() - }) - except Exception as e: - return Response({'error': f"Error caching related model {related_name}: {e}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - objects_to_create = [] - errors = [] - - for index, row in df.iterrows(): - instance_data = {} - row_errors = [] - is_valid_for_db = True - - for mapping in mappings: - excel_column = mapping.get('excel_column') - model_field = mapping.get('model_field') - default_value = mapping.get('default_value') - - excel_value = None - is_static_default = False - - # 1. XÁC ĐỊNH NGUỒN GIÁ TRỊ (STATIC DEFAULT HOẶC EXCEL) - if not excel_column and default_value is not None: - # Trường hợp 1: Không có cột Excel, luôn dùng giá trị mặc định tĩnh - excel_value = default_value - is_static_default = True - elif excel_column and excel_column in row: - # Trường hợp 2: Có cột Excel - excel_value = row[excel_column] - is_static_default = False - - # === BỔ SUNG: KIỂM TRA VÀ SỬ DỤNG default_value NẾU CELL RỖNG === - # Nếu giá trị từ Excel rỗng VÀ có default_value được cung cấp trong mapping - if (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')) and default_value is not None: - excel_value = default_value - is_static_default = True # Coi như giá trị tĩnh để bypass Section 2 (kiểm tra NULL) - # === KẾT THÚC BỔ SUNG === - - elif excel_column and excel_column not in row: - row_errors.append(f"Excel column '{excel_column}' not found (Header index: {header_row_excel})") - is_valid_for_db = False - continue - elif excel_column is None and default_value is None: - continue - else: - row_errors.append(f"Invalid mapping entry: {mapping} - requires excel_column or default_value") - is_valid_for_db = False - continue - - # 2. XỬ LÝ NULL/EMPTY VALUES (Chỉ khi giá trị đến từ Excel và KHÔNG phải giá trị tĩnh) - if not is_static_default and (pd.isna(excel_value) or (isinstance(excel_value, str) and str(excel_value).strip() == '')): - try: - field_obj = TargetModel._meta.get_field(model_field) - except FieldDoesNotExist: - row_errors.append(f"Model field '{model_field}' not found in model '{model_name}'") - is_valid_for_db = False - continue - - # Trường cho phép NULL - if field_obj.null: - instance_data[model_field] = None - continue - # Trường có Default Value (từ Model) - elif field_obj.default is not models_fields.NOT_PROVIDED: - instance_data[model_field] = field_obj.default - continue - # Trường KHÔNG cho phép NULL (Non-nullable field) - else: - # === START: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable === - allow_empty_non_nullable = mapping.get('allow_empty_excel_non_nullable', False) - - # Chỉ áp dụng bypass nếu là CharField/TextField (có thể lưu "" để thỏa mãn NOT NULL) - if allow_empty_non_nullable and isinstance(field_obj, (CharField, TextField)): - instance_data[model_field] = "" - continue # Chấp nhận chuỗi rỗng và đi tiếp - - # Nếu không được phép bypass HOẶC không phải CharField/TextField - row_errors.append(f"Non-nullable field '{model_field}' has empty value in row {index + 1}") - is_valid_for_db = False - instance_data[model_field] = "" if isinstance(field_obj, (CharField, TextField)) else None - continue - # === END: LOGIC BỔ SUNG CHO allow_empty_excel_non_nullable === - - # 3. XỬ LÝ FOREIGN KEY - if 'foreign_key' in mapping: - fk_config = mapping['foreign_key'] - related_model_name = fk_config.get('model_name') - key_to_lookup = str(excel_value).strip().lower() - RelatedModelCache = related_obj_cache.get(related_model_name, {}) - related_obj = RelatedModelCache.get(key_to_lookup) - - # Logic dự phòng để tìm theo ID nếu là giá trị tĩnh và là số - if not related_obj and is_static_default and str(excel_value).isdigit(): - related_obj = RelatedModelCache.get(str(excel_value)) - - if related_obj: - instance_data[model_field] = related_obj - else: - # Kiểm tra lại trường hợp giá trị lookup là rỗng/0 khi model field cho phép NULL - if (pd.isna(excel_value) or str(excel_value).strip() == '' or str(excel_value).strip() == '0') and TargetModel._meta.get_field(model_field).null: - instance_data[model_field] = None - continue - - # Báo lỗi và không hợp lệ nếu không tìm thấy object - row_errors.append(f"Related object for '{model_field}' with value '{excel_value}' not found in model '{related_model_name}' (row {index + 1})") - - if not TargetModel._meta.get_field(model_field).null: - is_valid_for_db = False - - instance_data[model_field] = None - continue - else: - instance_data[model_field] = excel_value - - if row_errors: - errors.append({'row': index + 1, 'messages': row_errors}) - - if is_valid_for_db: - try: - objects_to_create.append(TargetModel(**instance_data)) - except Exception as e: - errors.append({'row': index + 1, 'messages': [f"Critical error creating model instance: {str(e)}"]}) - - successful_row_count = len(objects_to_create) - - try: - with transaction.atomic(): - - # === LOGIC XỬ LÝ CÁC CHẾ ĐỘ NHẬP DỮ LIỆU === - if import_mode == 'overwrite': - TargetModel.objects.all().delete() - TargetModel.objects.bulk_create(objects_to_create) - message = f'{successful_row_count} records imported successfully after full **overwrite**.' - - elif import_mode == 'upsert': - for field in UNIQUE_KEY_FIELDS: - try: - TargetModel._meta.get_field(field) - except FieldDoesNotExist: - return Response({'error': f"Unique field '{field}' not found in model '{model_name}'. Cannot perform upsert."}, status=status.HTTP_400_BAD_REQUEST) - - existing_objects_query = TargetModel.objects.only('pk', *UNIQUE_KEY_FIELDS) - existing_map = {} - for obj in existing_objects_query: - key_tuple = tuple(getattr(obj, field) for field in UNIQUE_KEY_FIELDS) - existing_map[key_tuple] = obj - - to_update = [] - to_insert = [] - - for new_instance in objects_to_create: - try: - lookup_key = tuple(getattr(new_instance, field) for field in UNIQUE_KEY_FIELDS) - except AttributeError: - continue - - if lookup_key in existing_map: - new_instance.pk = existing_map[lookup_key].pk - to_update.append(new_instance) - else: - to_insert.append(new_instance) - - update_fields = [ - m['model_field'] - for m in mappings - if m['model_field'] not in ['pk'] and m['model_field'] not in UNIQUE_KEY_FIELDS - ] - - TargetModel.objects.bulk_update(to_update, update_fields) - TargetModel.objects.bulk_create(to_insert) - message = f'{len(to_insert)} records inserted, {len(to_update)} records updated successfully (Upsert mode).' - - elif import_mode == 'insert_only': - TargetModel.objects.bulk_create(objects_to_create) - message = f'{successful_row_count} records imported successfully (Insert Only mode).' - - else: - return Response({'error': f"Invalid import_mode specified: {import_mode}"}, status=status.HTTP_400_BAD_REQUEST) - - except Exception as e: - return Response({'error': f'Database error during bulk operation (Rollback occurred): {str(e)}', 'rows_attempted': successful_row_count}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - if errors: - return Response({'status': 'partial_success', 'message': f'{message} Invalid rows were skipped.', 'errors': errors}, status=status.HTTP_207_MULTI_STATUS) - - return Response({'status': 'success', 'message': message}, status=status.HTTP_201_CREATED) #============================================================================= executor = ThreadPoolExecutor(max_workers=10) def background_generate(doc_code, context_pks, output_filename, uid): @@ -1545,29 +1274,6 @@ class EmailTemplatePreview: @api_view(['POST']) def preview_email_template(request): - """ - API để preview email template - trả về nội dung đã thay thế placeholders - - POST /api/email/preview/ - Body: { - "template_id": 1, - "context_pks": { - "contract_id": 456, - "customer_id": 789 - } - } - - Response: { - "subject": "Thông báo hợp đồng #HD-001", - "content": "

", - "recipient_email": "customer@example.com", - "replacements": { - "[contract.code]": "HD-001", - "[customer.name]": "Nguyễn Văn A", - ... - } - } - """ try: # Validate input template_id = request.data.get('template_id') diff --git a/app/workflow_actions.py b/app/workflow_actions.py deleted file mode 100644 index 8f521607..00000000 --- a/app/workflow_actions.py +++ /dev/null @@ -1,375 +0,0 @@ -from django.test import Client -from app.workflow_registry import register_action -from app.workflow_utils import resolve_value -from app.document_generator import DocumentGenerator -from app.jobemail import EmailJobRunner -from app.payment import account_entry_api -from django.apps import apps -import re -import datetime - -client = Client() -# ============================ -# Logic xử lý Map Expression ($map) -# ============================ -def handle_map_expression(expression, context): - """ - Xử lý biểu thức đặc biệt để biến đổi danh sách dữ liệu. - Cú pháp: $map(data.installments, {amount: amount, due_date: $add_days(created_at, gap)}) - """ - # Regex tách nguồn dữ liệu và template - match = re.match(r"^\$map\(([^,]+),\s*\{(.*)\}\)$", expression.strip()) - if not match: - return [] - - source_path = match.group(1).strip() - template_content = match.group(2).strip() - - # Lấy danh sách dữ liệu gốc từ context (ví dụ: data.installments) - source_data = resolve_value(source_path, context) - if not isinstance(source_data, list): - return [] - - # Tìm các cặp key: value trong template định nghĩa - # Hỗ trợ cả trường hợp value là một hàm lồng như $add_days - pairs = re.findall(r"(\w+):\s*(\$add_days\([^)]+\)|[^{},]+)", template_content) - - results = [] - for index, item in enumerate(source_data): - # Tạo context riêng cho từng item để resolve - item_context = {**context, "item": item, "index": index} - processed_row = {} - - for key, val_expr in pairs: - val_expr = val_expr.strip() - - # 1. Xử lý biến chỉ mục ($index) - if val_expr == "$index": - processed_row[key] = index - - # 2. Xử lý hàm cộng ngày ($add_days) - elif "$add_days" in val_expr: - m = re.search(r"\$add_days\(([^,]+),\s*([^)]+)\)", val_expr) - if m: - base_key = m.group(1).strip() - days_key = m.group(2).strip() - - # Tìm giá trị ngày gốc và số ngày cần cộng - base_date = item.get(base_key) if base_key in item else resolve_value(base_key, item_context) - days = item.get(days_key) if days_key in item else resolve_value(days_key, item_context) - - try: - # Chuyển đổi string sang date object - if isinstance(base_date, str): - base_date = datetime.datetime.strptime(base_date[:10], "%Y-%m-%d").date() - - new_date = base_date + datetime.timedelta(days=int(days or 0)) - processed_row[key] = new_date.isoformat() - except Exception: - processed_row[key] = str(base_date) # Trả về bản gốc nếu lỗi - - # 3. Xử lý lấy giá trị từ item hiện tại hoặc context chung - else: - if val_expr == "$item": - processed_row[key] = item - elif val_expr == "$index": - processed_row[key] = index - elif val_expr in item: - processed_row[key] = item[val_expr] - else: - processed_row[key] = resolve_value(val_expr, item_context) - - results.append(processed_row) - - return results - -# ============================ -# CRUD thông qua API có sẵn -# ============================ -def deep_resolve_values(data, context): - if isinstance(data, dict): - return {k: deep_resolve_values(v, context) for k, v in data.items()} - elif isinstance(data, list): - return [deep_resolve_values(item, context) for item in data] - elif isinstance(data, str): - # Workaround for resolver bug: handle strings that are only a placeholder - match = re.fullmatch(r"\{([^}]+)\}", data) - if match: - # The path is the content inside the braces, e.g., "transaction_detail.id" - path = match.group(1) - # resolve_value works on raw paths, so call it directly - return resolve_value(path, context) - else: - # This handles complex strings like "/prefix/{path}/" or normal strings - return resolve_value(data, context) - else: - return data - -@register_action("API_CALL", schema={"required": ["method", "url"]}) -def api_call_action(params, context): - """Thực hiện gọi API nội bộ bằng Django Test Client""" - method = params["method"].upper() - url = resolve_value(params["url"], context) - save_as = params.get("save_as") - - raw_body = params.get("body") - - # ============================ - # Resolve body - # ============================ - if isinstance(raw_body, str) and raw_body.startswith("$map"): - body = handle_map_expression(raw_body, context) - elif isinstance(raw_body, dict): - body = deep_resolve_values(raw_body, context) - elif raw_body is None: - body = None - else: - body = resolve_value(raw_body, context) - - print(f" [API_CALL] {method} {url}") - print(f" [API_CALL] Resolved Body: {body}") - - # ============================ - # Execute request - # ============================ - if method == "POST": - resp = client.post(url, body, content_type="application/json") - elif method == "PATCH": - resp = client.patch(url, body, content_type="application/json") - elif method == "PUT": - resp = client.put(url, body, content_type="application/json") - elif method == "DELETE": - resp = client.delete(url) - else: - resp = client.get(url) - - print(f" [API_CALL] Status Code: {resp.status_code}") - - # ============================ - # Handle error - # ============================ - if resp.status_code >= 400: - error_content = resp.content.decode("utf-8") if resp.content else "" - print(f" [API_CALL] Error: {error_content}") - raise Exception(f"API Call failed: {error_content}") - - # ============================ - # Handle response safely - # ============================ - if resp.status_code == 204 or not resp.content: - # DELETE / No Content - result = {"deleted": True} - else: - try: - result = resp.json() - except ValueError: - # Fallback nếu response không phải JSON - result = resp.content.decode("utf-8") - - print(f" [API_CALL] Result: {result}") - - if save_as: - context[save_as] = result - - return result - - -# ============================ -# Gọi Utility / API bên ngoài -# ============================ -@register_action("CALL_UTILITY", schema={"required": ["utility_code"]}) -def call_utility_action(params, context): - Utility = apps.get_model("app", "Utility") - util = Utility.objects.get(code=params["utility_code"]) - - module_path = util.integration_module - if not module_path: - return {"error": "utility has no module"} - - from django.utils.module_loading import import_string - func = import_string(module_path) - - resolved_params = {k: resolve_value(v, context) for k,v in params.get("params", {}).items()} - return func(**resolved_params) - - -@register_action("SEND_EMAIL", schema={"required": ["template"]}) -def send_email_action(params, context): - tpl_name = params["template"] - tpl_pks = {k: resolve_value(v, context) for k,v in params.get("context_pks", {}).items()} - - Email_Template = apps.get_model("app", "Email_Template") - try: - template_obj = Email_Template.objects.get(name=tpl_name) - except Email_Template.DoesNotExist: - raise Exception(f"Email template '{tpl_name}' not found") - - runner = EmailJobRunner(template=template_obj, context_pks=tpl_pks) - success = runner.run() - - return {"sent": success} - - -# ============================ -# Tạo Document -# ============================ - -@register_action("GENERATE_DOCUMENT", schema={"required": ["document_code"]}) -def generate_document_action(params, context): - code = resolve_value(params["document_code"], context) - pks = {k: str(resolve_value(v, context)) for k, v in params.get("context_pks", {}).items()} - save_as = params.get("save_as") - - print(f" [GEN_DOC] Generating for code: {code}") - - gen = DocumentGenerator(document_code=code, context_pks=pks) - result = gen.generate() - - formatted_result = [{ - "pdf": result.get("pdf"), - "file": result.get("file"), - "name": result.get("name"), - "code": code - }] - - if save_as: - context[save_as] = formatted_result - print(f" [GEN_DOC] Success: Saved to context as '{save_as}'") - - return formatted_result - - -# ============================ -# Hạch toán -# ============================ -@register_action("ACCOUNT_ENTRY", schema={"required": ["amount", "category_code"]}) -def account_entry_action(params, context): - amount = resolve_value(params["amount"], context) - content = params.get("content", "") - userid = resolve_value(params.get("userid"), context) - - return account_entry_api( - code=params.get("internal_account", "HOAC02VND"), - amount=amount, - content=content, - type=params.get("type", "CR"), - category=apps.get_model("app", "Entry_Category").objects.get(code=params["category_code"]).id, - userid=userid, - ref=params.get("ref") - ) - -# ============================ -# Tìm bản ghi -# ============================ -@register_action("LOOKUP_DATA", schema={"required": ["model_name", "lookup_field", "lookup_value"]}) -def lookup_data_action(params, context): - model_name = params["model_name"] - field = params["lookup_field"] - save_as = params.get("save_as") - - # Lấy giá trị thực tế (ví dụ: "reserved") - value = resolve_value(params["lookup_value"], context) - - print(f" [LOOKUP] Searching {model_name} where {field} = '{value}'") - - try: - Model = apps.get_model("app", model_name) - obj = Model.objects.filter(**{field: value}).first() - - if not obj: - print(f" [LOOKUP] ERROR: Not found!") - raise Exception(f"Lookup failed: {model_name} with {field}={value} not found.") - - if save_as: - context[save_as] = obj - print(f" [LOOKUP] Success: Found ID {obj.id}, saved to context as '{save_as}'") - - return obj.id - except Exception as e: - print(f" [LOOKUP] EXCEPTION: {str(e)}") - raise e - - -# ============================ -# Quét và phân bổ toàn bộ bút toán còn phần dư -# ============================ -@register_action("ALLOCATE_ALL_PENDING", schema={}) -def allocate_all_pending_action(params, context): - """ - Quét toàn bộ Internal_Entry có allocation_remain > 0 (type CR), - group by product_id, gọi phân bổ cho từng product cho đến khi hết. - """ - from app.payment import allocate_payment_to_schedules, allocate_penalty_reduction - from decimal import Decimal - - Internal_Entry = apps.get_model("app", "Internal_Entry") - Payment_Schedule = apps.get_model("app", "Payment_Schedule") - Product_Booked = apps.get_model("app", "Product_Booked") - Transaction_Current = apps.get_model("app", "Transaction_Current") - Transaction_Detail = apps.get_model("app", "Transaction_Detail") - - # ---------- Lấy toàn bộ product_id còn entry chưa phân bổ hết ---------- - product_ids = list( - Internal_Entry.objects.filter( - type__code="CR", - allocation_remain__gt=0, - product__isnull=False - ) - .values_list("product_id", flat=True) - .distinct() - ) - - print(f" [ALLOCATE_ALL] Tìm được {len(product_ids)} product có entry còn phần dư") - - if not product_ids: - return {"total_products": 0, "results": []} - - # ---------- DEBUG: dump trạng thái trước khi phân bổ ---------- - for pid in product_ids: - print(f"\n [DEBUG] ===== Product {pid} — trạng thái TRƯỚC phân bổ =====") - - # Entries - entries = Internal_Entry.objects.filter( - product_id=pid, type__code="CR", allocation_remain__gt=0 - ).order_by("date", "create_time") - for e in entries: - print(f" Entry id={e.id} | account_id={e.account_id} | amount={e.amount} | allocation_remain={e.allocation_remain} | date={e.date}") - - # Lấy txn_detail của product - booked = Product_Booked.objects.filter(product_id=pid).first() - if not booked or not booked.transaction: - print(f" !! Không có Product_Booked / Transaction") - continue - - txn = booked.transaction - txn_detail = None - try: - current = Transaction_Current.objects.get(transaction=txn) - txn_detail = current.detail - except Exception: - txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by("-create_time").first() - - if not txn_detail: - print(f" !! Không có Transaction_Detail") - continue - - # Schedules - all_schedules = Payment_Schedule.objects.filter(txn_detail=txn_detail).order_by("cycle", "from_date") - unpaid = all_schedules.filter(status__id=1) - print(f" Tổng schedule: {all_schedules.count()} | Chưa thanh toán (status=1): {unpaid.count()}") - for s in all_schedules: - print(f" Schedule id={s.id} | cycle={s.cycle} | status_id={s.status_id} | amount_remain={s.amount_remain} | penalty_remain={s.penalty_remain} | remain_amount={s.remain_amount}") - - # ---------- Chạy phân bổ ---------- - results = [] - for product_id in product_ids: - try: - normal = allocate_payment_to_schedules(product_id) - reduction = allocate_penalty_reduction(product_id) - results.append({"product_id": product_id, "normal": normal, "reduction": reduction}) - print(f" [ALLOCATE_ALL] Product {product_id}: OK — normal={normal}") - except Exception as e: - print(f" [ALLOCATE_ALL] Product {product_id}: ERROR - {str(e)}") - results.append({"product_id": product_id, "error": str(e)}) - - return {"total_products": len(product_ids), "results": results} \ No newline at end of file diff --git a/app/workflow_engine.py b/app/workflow_engine.py deleted file mode 100644 index 4a10aeb6..00000000 --- a/app/workflow_engine.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.db import transaction -from app.models import Workflow, StepAction, Rule -from app.workflow_registry import ACTION_REGISTRY, validate_action_schema -from app.workflow_utils import resolve_value - -@transaction.atomic -def execute_step(step: StepAction, context: dict): - #print(f"\n>>> EXECUTING STEP: {step.step_code} (Order: {step.order})") - - # Evaluate rules first - for rule in step.rules.filter(is_active=True): - if not evaluate_rule(rule, context): - #print(f"Step {step.step_code} skipped due to rule failure.") - return {"step": step.step_code, "skipped": True, "reason": "rule_failed"} - - results = [] - # Lưu ý: step.actions thường là một list các dict - actions_list = step.actions if isinstance(step.actions, list) else [] - - for action in actions_list: - action_type = action.get("type") - params = action.get("params", {}) - - #print(f" - Action Type: {action_type}") - - if action_type not in ACTION_REGISTRY: - #print(f" - ERROR: Action type '{action_type}' not registered!") - continue - - try: - validate_action_schema(action_type, params) - handler = ACTION_REGISTRY[action_type] - - # Thực thi handler - output = handler(params, context) - - results.append({"action": action_type, "result": output}) - - # Lưu output cuối cùng vào context - context["last_result"] = output - except Exception as e: - #print(f" - ERROR in action {action_type}: {str(e)}") - # Raise để transaction.atomic rollback nếu cần, hoặc xử lý tùy ý - raise e - - return {"step": step.step_code, "executed": True, "results": results} - - -def evaluate_rule(rule: Rule, context: dict): - for condition in (rule.conditions or []): - left = resolve_value(condition.get("left"), context) - right = resolve_value(condition.get("right"), context) - op = condition.get("operator", "==") - - #print(f" Evaluating Rule: {left} {op} {right}") - - if op == "IN" and left not in right: return False - if op == "==" and left != right: return False - if op == "!=" and left == right: return False - if op == ">" and not (left > right): return False - if op == "<" and not (left < right): return False - - return True - - -def run_workflow(workflow_code: str, trigger: str, context: dict): - #print(f"\n================ START WORKFLOW: {workflow_code} ================") - #print(f"Trigger: {trigger} | Initial Context: {context}") - - workflow = Workflow.objects.filter(code=workflow_code, is_active=True).first() - if not workflow: - #print(f"Workflow '{workflow_code}' not found or inactive.") - raise Exception(f"Workflow '{workflow_code}' not found") - - steps = workflow.steps.filter(trigger_event=trigger, is_active=True).order_by("order") - #print(f"Found {steps.count()} active steps.") - - outputs = [] - for step in steps: - res = execute_step(step, context) - outputs.append(res) - - #print(f"================ FINISH WORKFLOW: {workflow_code} ================\n") - return outputs \ No newline at end of file diff --git a/app/workflow_registry.py b/app/workflow_registry.py deleted file mode 100644 index 80ab6ec3..00000000 --- a/app/workflow_registry.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Callable, Dict - -ACTION_REGISTRY: Dict[str, Callable] = {} -ACTION_SCHEMAS: Dict[str, dict] = {} - -def register_action(name: str, schema=None): - def decorator(func): - ACTION_REGISTRY[name] = func - ACTION_SCHEMAS[name] = schema or {} - return func - return decorator - -def validate_action_schema(action_name, params): - schema = ACTION_SCHEMAS.get(action_name, {}) - required = schema.get("required", []) - - for key in required: - if key not in params: - raise Exception(f"Action '{action_name}' missing required param: {key}") - - return True diff --git a/app/workflow_utils.py b/app/workflow_utils.py deleted file mode 100644 index d08ca2f1..00000000 --- a/app/workflow_utils.py +++ /dev/null @@ -1,652 +0,0 @@ -import re -import math -from datetime import datetime, date, timedelta -from decimal import Decimal -from django.db import models -from django.apps import apps - -# ============================================= -# CORE RESOLVER -# ============================================= -def resolve_value(expr, context): - """ - Universal expression resolver with support for: - - Literals (int, float, bool, string) - - Template strings: {key}, "text {key} text" - - Dotted paths: customer.address.city - - Math functions: $add, $sub, $multiply, $divide, $mod, $power, $round, $abs, $min, $max - - Date functions: $now, $today, $date_diff, $date_add, $date_format, $date_parse - - String functions: $concat, $upper, $lower, $trim, $replace, $substring, $split, $length - - Logic functions: $if, $switch, $and, $or, $not - - List functions: $append, $agg, $filter, $map, $first, $last, $count, $sum - - Lookup functions: $vlookup, $lookup, $get - - Nested functions support - """ - if expr is None: - return None - - # Direct literal types - if isinstance(expr, (int, float, bool, Decimal)): - return expr - - if not isinstance(expr, str): - return expr - - expr = expr.strip() - - # ============================================= - # 1. SYSTEM VARIABLES - # ============================================= - if expr == "$now": - return context.get("now", datetime.now()) - - if expr == "$today": - if "today" in context: - return context["today"] - now_in_context = context.get("now") - if isinstance(now_in_context, datetime): - return now_in_context.date() - return date.today() - - if expr == "$today_str": - now_val = context.get("now", datetime.now()) - if isinstance(now_val, datetime): - return now_val.date().isoformat() - elif isinstance(now_val, date): - return now_val.isoformat() - return date.today().isoformat() - - if expr == "$now_iso": - return datetime.now().isoformat(timespec='seconds') - - if expr == "$timestamp": - return int(datetime.now().timestamp()) - - # ============================================= - # 2. MATH FUNCTIONS (Support Nested) - # ============================================= - math_functions = { - 'add': lambda a, b: a + b, - 'sub': lambda a, b: a - b, - 'subtract': lambda a, b: a - b, - 'multiply': lambda a, b: a * b, - 'mul': lambda a, b: a * b, - 'divide': lambda a, b: a / b if b != 0 else 0, - 'div': lambda a, b: a / b if b != 0 else 0, - 'mod': lambda a, b: a % b if b != 0 else 0, - 'power': lambda a, b: a ** b, - 'pow': lambda a, b: a ** b, - } - - for func_name, func in math_functions.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - a = to_number(resolve_value(args[0], context)) - b = to_number(resolve_value(args[1], context)) - return func(a, b) - - # Single-argument math functions - single_math = { - 'round': lambda x, d=0: round(x, int(d)), - 'abs': lambda x: abs(x), - 'ceil': lambda x: math.ceil(x), - 'floor': lambda x: math.floor(x), - 'sqrt': lambda x: math.sqrt(x) if x >= 0 else 0, - } - - for func_name, func in single_math.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 1: - val = to_number(resolve_value(args[0], context)) - if len(args) == 2 and func_name == 'round': - decimals = to_number(resolve_value(args[1], context)) - return func(val, decimals) - return func(val) - - # Multi-argument math - if re.match(r'^\$(min|max)\(', expr, re.IGNORECASE): - match = re.match(r'^\$(min|max)\((.*)\)$', expr, re.IGNORECASE) - if match: - func_name = match.group(1).lower() - args = split_args(match.group(2)) - values = [to_number(resolve_value(arg, context)) for arg in args] - return min(values) if func_name == 'min' else max(values) - - # ============================================= - # 3. DATE FUNCTIONS - # ============================================= - # $date_diff(date1, date2, unit?) - if re.match(r'^\$date_diff\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_diff\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - raw_d1 = resolve_value(args[0], context) - raw_d2 = resolve_value(args[1], context) - - d1 = to_date(raw_d1) - d2 = to_date(raw_d2) - - unit = resolve_value(args[2], context).lower() if len(args) > 2 else 'days' - - #print(f"[DEBUG date_diff] raw_d1: {raw_d1}, raw_d2: {raw_d2}") # DEBUG - #print(f"[DEBUG date_diff] d1 (datetime): {d1}, d2 (datetime): {d2}") # DEBUG - #print(f"[DEBUG date_diff] unit: {unit}") # DEBUG - - if not (d1 and d2): - #print("[DEBUG date_diff] One or both dates are invalid. Returning 0.") # DEBUG - return 0 - - # Ensure we are comparing date objects, ignoring time - d1_date_only = d1.date() - d2_date_only = d2.date() - - #print(f"[DEBUG date_diff] d1_date_only: {d1_date_only}, d2_date_only: {d2_date_only}") # DEBUG - - if unit == 'days': - delta_days = (d1_date_only - d2_date_only).days - #print(f"[DEBUG date_diff] Calculated delta_days: {delta_days}. Returning {delta_days}.") # DEBUG - return delta_days - elif unit == 'months': - delta_months = (d1_date_only.year - d2_date_only.year) * 12 + d1_date_only.month - d2_date_only.month - #print(f"[DEBUG date_diff] Calculated delta_months: {delta_months}. Returning {delta_months}.") # DEBUG - return delta_months - elif unit == 'years': - delta_years = d1_date_only.year - d2_date_only.year - #print(f"[DEBUG date_diff] Calculated delta_years: {delta_years}. Returning {delta_years}.") # DEBUG - return delta_years - - #print(f"[DEBUG date_diff] Unit '{unit}' not recognized. Returning 0.") # DEBUG - return 0 - - # $date_add(date, amount, unit?) - if re.match(r'^\$date_add\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_add\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - base_date = to_date(resolve_value(args[0], context)) - amount = to_number(resolve_value(args[1], context)) - unit = resolve_value(args[2], context).lower() if len(args) > 2 else 'days' - - if base_date: - # Ensure base_date is datetime - if isinstance(base_date, date) and not isinstance(base_date, datetime): - base_date = datetime.combine(base_date, datetime.min.time()) - - if unit == 'days': - result = base_date + timedelta(days=int(amount)) - elif unit == 'months': - month = base_date.month + int(amount) - year = base_date.year + (month - 1) // 12 - month = ((month - 1) % 12) + 1 - result = base_date.replace(year=year, month=month) - elif unit == 'years': - result = base_date.replace(year=base_date.year + int(amount)) - elif unit == 'hours': - result = base_date + timedelta(hours=int(amount)) - else: - result = base_date + timedelta(days=int(amount)) - - return result.isoformat() if isinstance(result, datetime) else result.strftime("%Y-%m-%d") - - # $date_format(date, format) - if re.match(r'^\$date_format\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_format\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - dt = to_date(resolve_value(args[0], context)) - fmt = resolve_value(args[1], context).strip('\'"') - if dt: - return dt.strftime(fmt) - - # $date_parse(string, format) - if re.match(r'^\$date_parse\(', expr, re.IGNORECASE): - match = re.match(r'^\$date_parse\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 1: - date_str = str(resolve_value(args[0], context)) - fmt = resolve_value(args[1], context).strip('\'"') if len(args) > 1 else "%Y-%m-%d" - try: - return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d") - except: - return None - - # ============================================= - # 4. STRING FUNCTIONS - # ============================================= - # $concat(str1, str2, ...) - if re.match(r'^\$concat\(', expr, re.IGNORECASE): - match = re.match(r'^\$concat\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return ''.join(str(resolve_value(arg, context) or '') for arg in args) - - # $upper, $lower, $trim - string_single = { - 'upper': lambda s: str(s).upper(), - 'lower': lambda s: str(s).lower(), - 'trim': lambda s: str(s).strip(), - 'length': lambda s: len(str(s)), - } - - for func_name, func in string_single.items(): - pattern = rf'^\${func_name}\((.*)\)$' - match = re.match(pattern, expr, re.IGNORECASE) - if match: - arg = resolve_value(match.group(1).strip(), context) - return func(arg) - - # $replace(text, old, new) - if re.match(r'^\$replace\(', expr, re.IGNORECASE): - match = re.match(r'^\$replace\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - text = str(resolve_value(args[0], context)) - old = str(resolve_value(args[1], context)).strip('\'"') - new = str(resolve_value(args[2], context)).strip('\'"') - return text.replace(old, new) - - # $substring(text, start, length?) - if re.match(r'^\$substring\(', expr, re.IGNORECASE): - match = re.match(r'^\$substring\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - text = str(resolve_value(args[0], context)) - start = int(to_number(resolve_value(args[1], context))) - length = int(to_number(resolve_value(args[2], context))) if len(args) > 2 else None - return text[start:start+length] if length else text[start:] - - # $split(text, delimiter) - if re.match(r'^\$split\(', expr, re.IGNORECASE): - match = re.match(r'^\$split\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - text = str(resolve_value(args[0], context)) - delimiter = str(resolve_value(args[1], context)).strip('\'"') - return text.split(delimiter) - - # ============================================= - # 5. LOGIC FUNCTIONS - # ============================================= - # $if(condition, true_value, false_value) - if re.match(r'^\$if\(', expr, re.IGNORECASE): - match = re.match(r'^\$if\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - condition = resolve_value(args[0], context) - return resolve_value(args[1], context) if condition else resolve_value(args[2], context) - - # $switch(value, case1, result1, case2, result2, ..., default) - if re.match(r'^\$switch\(', expr, re.IGNORECASE): - match = re.match(r'^\$switch\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - value = resolve_value(args[0], context) - for i in range(1, len(args) - 1, 2): - if i + 1 < len(args): - case = resolve_value(args[i], context) - if value == case: - return resolve_value(args[i + 1], context) - # Default value is last arg if odd number of args - if len(args) % 2 == 0: - return resolve_value(args[-1], context) - - # $and, $or, $not - if re.match(r'^\$and\(', expr, re.IGNORECASE): - match = re.match(r'^\$and\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return all(resolve_value(arg, context) for arg in args) - - if re.match(r'^\$or\(', expr, re.IGNORECASE): - match = re.match(r'^\$or\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - return any(resolve_value(arg, context) for arg in args) - - if re.match(r'^\$not\(', expr, re.IGNORECASE): - match = re.match(r'^\$not\((.*)\)$', expr, re.IGNORECASE) - if match: - arg = resolve_value(match.group(1).strip(), context) - return not arg - - # ============================================= - # 6. LIST/ARRAY FUNCTIONS - # ============================================= - # $append(list, element) - if re.match(r'^\$append\(', expr, re.IGNORECASE): - match = re.match(r"^\$append\(([^,]+),\s*(.+)\)$", expr, re.DOTALL) - if match: - list_expr = match.group(1).strip() - element_expr = match.group(2).strip() - - # 1. Resolve the list - target_list = resolve_value(list_expr, context) - if target_list is None: - target_list = [] - - # Ensure it's a copy so we don't modify the original context variable directly - target_list = list(target_list) - - # 2. Resolve the element - resolved_element = resolve_value(element_expr, context) - - if isinstance(resolved_element, str): - try: - import json - element_to_append = json.loads(resolved_element) - except json.JSONDecodeError: - element_to_append = resolved_element - else: - element_to_append = resolved_element - - target_list.append(element_to_append) - return target_list - - - # $first(list), $last(list) - if re.match(r'^\$(first|last)\(', expr, re.IGNORECASE): - match = re.match(r'^\$(first|last)\((.*)\)$', expr, re.IGNORECASE) - if match: - func_name = match.group(1).lower() - lst = resolve_value(match.group(2).strip(), context) - if isinstance(lst, list) and len(lst) > 0: - return lst[0] if func_name == 'first' else lst[-1] - - # $count(list) - if re.match(r'^\$count\(', expr, re.IGNORECASE): - match = re.match(r'^\$count\((.*)\)$', expr, re.IGNORECASE) - if match: - lst = resolve_value(match.group(1).strip(), context) - return len(lst) if isinstance(lst, list) else 0 - - # $agg(list, operation, field?) - if re.match(r'^\$agg\(', expr, re.IGNORECASE): - match = re.match(r'^\$agg\(([^,]+),\s*[\'"]([^\'\"]+)[\'"](?:,\s*[\'"]?([^\'\")]+)[\'"]?)?\)$', expr) - if match: - list_expr = match.group(1).strip() - operation = match.group(2).strip() - field_expr = match.group(3).strip() if match.group(3) else None - - target_list = resolve_value(list_expr, context) - if not isinstance(target_list, list): - return 0 - - if operation == 'count': - return len(target_list) - - if operation == 'sum': - if not field_expr: - return sum(to_number(item) for item in target_list) - total = 0 - for item in target_list: - value = item.get(field_expr) if isinstance(item, dict) else getattr(item, field_expr, 0) - total += to_number(value) - return total - - if operation in ['min', 'max', 'avg']: - values = [] - for item in target_list: - if field_expr: - value = item.get(field_expr) if isinstance(item, dict) else getattr(item, field_expr, 0) - else: - value = item - values.append(to_number(value)) - - if not values: - return 0 - if operation == 'min': - return min(values) - elif operation == 'max': - return max(values) - elif operation == 'avg': - return sum(values) / len(values) - - # ============================================= - # 7. LOOKUP FUNCTIONS - # ============================================= - # $vlookup(lookup_value, model_name, lookup_field, return_field) - if re.match(r'^\$vlookup\(', expr, re.IGNORECASE): - match = re.match(r'^\$vlookup\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 4: - lookup_value = resolve_value(args[0], context) - model_name = resolve_value(args[1], context).strip('\'"') - lookup_field = resolve_value(args[2], context).strip('\'"') - return_field = resolve_value(args[3], context).strip('\'"') - - try: - Model = apps.get_model('app', model_name) - obj = Model.objects.filter(**{lookup_field: lookup_value}).first() - if obj: - return getattr(obj, return_field, None) - except: - pass - - # $lookup(model_name, field, value) - if re.match(r'^\$lookup\(', expr, re.IGNORECASE): - match = re.match(r'^\$lookup\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 3: - model_name = resolve_value(args[0], context).strip('\'"') - field = resolve_value(args[1], context).strip('\'"') - value = resolve_value(args[2], context) - - try: - Model = apps.get_model('app', model_name) - return Model.objects.filter(**{field: value}).first() - except: - pass - - # $get(dict_or_object, key, default?) - if re.match(r'^\$get\(', expr, re.IGNORECASE): - match = re.match(r'^\$get\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) >= 2: - obj = resolve_value(args[0], context) - key = resolve_value(args[1], context) - default = resolve_value(args[2], context) if len(args) > 2 else None - - if isinstance(obj, dict): - return obj.get(key, default) - else: - return getattr(obj, key, default) - - # ============================================= - # 8. COMPARISON OPERATORS - # ============================================= - # $eq, $ne, $gt, $gte, $lt, $lte, $in, $contains - comparisons = { - 'eq': lambda a, b: a == b, - 'ne': lambda a, b: a != b, - 'gt': lambda a, b: a > b, - 'gte': lambda a, b: a >= b, - 'lt': lambda a, b: a < b, - 'lte': lambda a, b: a <= b, - 'in': lambda a, b: a in b, - 'contains': lambda a, b: b in a, - } - - for op_name, op_func in comparisons.items(): - pattern = rf'^\${op_name}\(' - if re.match(pattern, expr, re.IGNORECASE): - match = re.match(rf'^\${op_name}\((.*)\)$', expr, re.IGNORECASE) - if match: - args = split_args(match.group(1)) - if len(args) == 2: - a = resolve_value(args[0], context) - b = resolve_value(args[1], context) - return op_func(a, b) - - # ============================================= - # 9. HELPER: Get context value (dotted path) - # ============================================= - def get_context_value(key_path): - if not key_path: - return None - - # Check if numeric literal - if re.match(r"^-?\d+(\.\d+)?$", key_path): - return float(key_path) - - # Simple key - if "." not in key_path: - val = context.get(key_path) - if isinstance(val, Decimal): - return float(val) - return val - - # Dotted path - root, *rest = key_path.split(".") - val = context.get(root) - - for r in rest: - if val is None: - return None - - # Array notation: field[0] - array_match = re.match(r"(\w+)\[(\d+)\]", r) - if array_match: - attr_name = array_match.group(1) - index = int(array_match.group(2)) - val = getattr(val, attr_name, None) if not isinstance(val, dict) else val.get(attr_name) - try: - val = val[index] - except: - return None - else: - if isinstance(val, dict): - val = val.get(r) - else: - val = getattr(val, r, None) - - # Auto-fetch first() for QuerySet - if hasattr(val, 'all') and not isinstance(val, models.Model): - val = val.first() - - if isinstance(val, Decimal): - return float(val) - return val - - # ============================================= - # 10. TEMPLATE STRING PROCESSING - # ============================================= - pattern = re.compile(r"\{(\w+(\.\w+)*)\}") - if pattern.search(expr): - single_match = pattern.fullmatch(expr) - if single_match: - return get_context_value(single_match.group(1)) - - def replace_match(match): - val = get_context_value(match.group(1)) - return str(val) if val is not None else "" - return pattern.sub(replace_match, expr) - - # ============================================= - # 11. SUPPORT $last_result - # ============================================= - if expr.startswith("$last_result"): - _, _, field = expr.partition(".") - last_res = context.get("last_result") - if not field: - return last_res - if last_res is None: - return None - return getattr(last_res, field, None) if not isinstance(last_res, dict) else last_res.get(field) - - # ============================================= - # 12. DOTTED PATH OR DIRECT CONTEXT KEY - # ============================================= - if re.match(r"^-?\d+(\.\d+)?$", expr): - return float(expr) - - if "." in expr or expr in context: - return get_context_value(expr) - - return expr - - -# ============================================= -# HELPER FUNCTIONS -# ============================================= -def split_args(content): - """ - Split function arguments respecting nested parentheses and quotes. - Example: "a, $add(b, c), 'd'" -> ["a", "$add(b, c)", "'d'"] - """ - args = [] - current = [] - depth = 0 - in_quote = None - - for char in content: - if char in ('"', "'") and (not in_quote or in_quote == char): - in_quote = None if in_quote else char - current.append(char) - elif in_quote: - current.append(char) - elif char == '(': - depth += 1 - current.append(char) - elif char == ')': - depth -= 1 - current.append(char) - elif char == ',' and depth == 0: - args.append(''.join(current).strip()) - current = [] - else: - current.append(char) - - if current: - args.append(''.join(current).strip()) - - return args - - -def to_number(value, default=0): - """Convert value to number, return default if fails.""" - if value is None or value == '': - return default - try: - return float(value) - except (ValueError, TypeError): - return default - - -def to_date(value): - """Convert value to datetime object.""" - #print(f"[DEBUG to_date] Input value: {value} (type: {type(value)})") # DEBUG - if isinstance(value, datetime): - #print(f"[DEBUG to_date] Output (datetime): {value}") # DEBUG - return value - if isinstance(value, date): - result = datetime.combine(value, datetime.min.time()) - #print(f"[DEBUG to_date] Output (date -> datetime): {result}") # DEBUG - return result - if isinstance(value, str): - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S"): - try: - result = datetime.strptime(value.split('.')[0], fmt) - #print(f"[DEBUG to_date] Output (str -> datetime): {result}") # DEBUG - return result - except: - continue - #print(f"[DEBUG to_date] Output (None): None") # DEBUG - return None \ No newline at end of file diff --git a/prefect-ui.log b/prefect-ui.log new file mode 100644 index 00000000..f8d6b34c --- /dev/null +++ b/prefect-ui.log @@ -0,0 +1,17 @@ + + ___ ___ ___ ___ ___ ___ _____ +| _ \ _ \ __| __| __/ __|_ _| +| _/ / _|| _|| _| (__ | | +|_| |_|_\___|_| |___\___| |_| + +Configure Prefect to communicate with the server with: + + prefect config set PREFECT_API_URL=http://127.0.0.1:4200/api + +View the API reference documentation at http://127.0.0.1:4200/docs + +Check out the dashboard at http://127.0.0.1:4200 + + + +Server stopped! diff --git a/requirements.txt b/requirements.txt index f330c3bc..47d97af9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,6 @@ num2words mammoth paramiko channels +prefect croniter uvicorn[standard] \ No newline at end of file diff --git a/rundev.sh b/rundev.sh index 0ad8820e..c481feda 100644 --- a/rundev.sh +++ b/rundev.sh @@ -1,11 +1,30 @@ +#!/usr/bin/env bash + +# Chạy Prefect UI (background, port 4200) +if ! lsof -i:4200 > /dev/null 2>&1; then + echo "Port 4200 trống → Khởi động Prefect server background..." + nohup prefect server start --host 127.0.0.1 --port 4200 > prefect-ui.log 2>&1 & + sleep 3 # chờ 3 giây để server khởi động ổn định + echo "Prefect UI đã khởi động (truy cập: http://localhost:4200)" + echo "Logs: tail -f prefect-ui.log" +else + echo "Port 4200 đã có Prefect server chạy rồi → bỏ qua" +fi + +# ======================== +# Chạy Django API (gunicorn + uvicorn) +# ======================== python3 envdev.py + sudo kill -9 $(lsof -i:8000 -t) 2> /dev/null + +echo "Khởi động Gunicorn..." gunicorn api.asgi:application \ - -k uvicorn.workers.UvicornWorker \ - -w 3 \ - --worker-connections 2000 \ - --max-requests 10000 \ - --max-requests-jitter 1000 \ - --timeout 1000 \ - --log-level info \ - -b 0.0.0.0:8000 \ No newline at end of file + -k uvicorn.workers.UvicornWorker \ + -w 3 \ + --worker-connections 2000 \ + --max-requests 10000 \ + --max-requests-jitter 1000 \ + --timeout 1000 \ + --log-level info \ + -b 0.0.0.0:8000 \ No newline at end of file