From e77813a45cc1286984e2cebfde774a2f8641fa55 Mon Sep 17 00:00:00 2001 From: Ernesto Herrera Date: Wed, 27 May 2026 06:58:50 -0600 Subject: [PATCH] feature/analiticas de saldos --- app/Manual_Descargas.docx | Bin 0 -> 42804 bytes app/app.ipynb | 431 +++++++++++++++++++++++++++++++++++--- 2 files changed, 400 insertions(+), 31 deletions(-) create mode 100644 app/Manual_Descargas.docx diff --git a/app/Manual_Descargas.docx b/app/Manual_Descargas.docx new file mode 100644 index 0000000000000000000000000000000000000000..8dce6bd9376de3f42c00effd605ba1efcbe65b69 GIT binary patch literal 42804 zcmagFb9^OD*997TV%wN#V%xTDYhq(!+nCsPa$GWzM*Afyw*U`yT%O?gc_R3$+qq{I=+ZTx=l2{bq#hV*|sAhCO8hM3CzAcXL`SZgWt9Qb=FgrQrhla(8m!+C-|txKGC^Jc(@e#oZndWn-fa-p z4smABrkl=nOpWOTqOLUZPDyhE*)nMF+|DYHyk_MLm&DB1AePAg0nQ@Uidlk*Y9;l=pclRQGXiR>o#r#cvoPm_>!J zR#C z-+474&q$gknM&(D39kUDE6(86^98oJ97~wD=WVfgeV=dikvQ)aRYn&`{=bc)JGm79 z)7R*2fB*r(etqiwG_iK1r~7-XjGvGMWkB@1`)NaQAh8=KfK&EGF`~X$62B-vi_xJs4*XqyV#??Np;=OuD1r23=Qxna#P+3 z)UZyExF><rK6xRE0BMx5@Sek%z2kS(s=Il*7YX@N` zD&52qUR>F_XYCsP#Mk5`uJ6Zx5^Im7lqPG;fWck>FX4c4CnRw=6-z+F7uvMY8#OG+}K<}qpNyzxuH*M18B>_=_NrDkKy zk~h@Iv=-~-e-BV-Fszcr*EFC03J}s)fQ;>o2DkktAX~hYKXq}nl193qcSP5_p>{2=Oub$;OFR9=U$c0+d za@nx!FRUG!o7-8VbtLoSb24sWszG^g8e^wOG4RP;&xIdy72+rs`vdA`@mYlrG8?&2E#ltgV&{+brt$}>%GDG@8fc}x{7i7H7?d* zKtPE9bzBVW?f)4T1%YXyb?ln>%;vdKV#zqLR)!rP z@8?i7j$r!xFs^#8*M`y8MFCUD23d0zw6W*BVR?sjn8xbxp@Y?lLt;m;GVoF~)d3_O z2feA$%6SM{Xs0wa`KnP*;(EjJYkWw1M>*;#rxkeqjRw!6c3FF`KW`@dB4;kOPyJU( z@{LOysR}>j!plM5tIf;ZOQv{O#CGpbG?J6P!Ha?zyfcA~xtCi|E{BR@$PnT%=yzO= zn}b=xdOF?L86PA4HnY*f;GQMnKgFl9znO)5>WA6GFe=#M_xW=#YG0mpLAeTq5#DfG zPpDK^sup?^bspQLE->_)g9Y?mh%$0yh9t?-z^>;GDM#IZ%^|ksp}v$W7GCNB9_)on zaOn+Q`1^_wq$XZ$LQ@)wZ|$hv?HL<8Ttejys5)@?&ED%lRt-ubzXk4c8xpDa;dmEAYM@1$d$r z52n1Bi{=i&qdE3FBHgtO!{OGmGY{;ED+IewOFcAx$~m<4`$*&%zJH7LW=az8gxV%4 zS4bv?il?<)TuT3{1Ye}h6={K0)eU#AH$w5iycl=9QDwcyvD_(7l5ph zoK~c6i7%VT*WCS4=Y2WflSenkc{Dx=4{n^H}y=dmmmi^2#u6IHciVWZK`$4T{_0N@egvpnjudwrjBVPB^}Mmy_FLM?!?2 zExx>xbtn7TtX`KdXiC6%F*Qg$S_BM(z?gf7$}2L&UhK_`)d$on+8rA%;DN(%hWYB&A|;woIPr~PhSl~09FeB3Qx9<0%& zwHbC^j*B^JC!%NDPH)dV3!ygk}&EN;5O!Yd7va=#BU?95lGoK25)yl4V|A zzU<%|dzjHXp^y+=bV*i&oro(&gnp=7@V73goO0GL->wdW?8uqa`oNG* z>7{}B^9gjeq`nPqo#5>|A$oB|Lc+ig-fx0PkU;wNsyXXD#WY(q2fN*e*5klVNOd$L zZn(oBdm`ciRNAZ=W82R{$BGLTY;qfOF`LX85z~1Xeuc9mn3QhgA-xs61iyB5IiZ96 z_t|7t^qu!@{H;HdPN5P~5 zo+N(kePdO{y|KdLXSS;N<|!L%+HX$NdD{{>gukFJ7ELMzhN(>>OdP3*9}I{LA(&sX zp&>~t@TYLt&BhJITuwV*zY(sReP`@uui9rsoaDX%6`;5d)@jBvJC6=N(4qR1`6nz= z$lnxk7&zASCn8tCWbbTA3do<;UaI$xuy^C^O`84ZV%TU%!dePt2;56kcnCsyGbMPS zD37Rh=xngQP@H`?EMY-^i5!SPb= z`dGe@E8w4G5OUXHM^JaNMdzgQzkE%APe>R+48B3Mm}J*|L-_$Oc0+}k3+E>X#kb@o z2VC+ExGQi-og*y#4oBX&EY$?T-8d(Vl8ut(ai?y@Fwip~&JjT~_Vq3iHJj2LlxT!N zyJXQqPkT2O;1PdPYFd)rxFQCXM_hHBuhlpDD2fFkb{Zzkk$UXnw@_GlLBBXd= zjWL88QJ|18$Q&|UU$82?SI1C_G!a55MbffZ#8NqQA7z!HUkk~RUYx3q+@K%ekyl{R zGjyomPYC-#-N}Kg4oF1AXwZcSb=e6-DIDkDUvDYuL8EntJZJ(OpkF^zgL1{u3S0IZ z-IQ}c@mkSXm9mKm`}~K(3{?go|d3S7~74&`8%5+_LK; zYL-Eaal^qG>Jjh1DJ;=8L-6*Sf_1NOJBOA1;wjeR{$r7U&OaKt#?(*mh8_#KOcN-w<-2^`%n1W*67QjOk?=-#U9geZdEaFr z_%u`%MR`Cy<%C-(kcXy1hSjUdRz0X+oiec}p;CORSP3v8axPNcK7Y}uS83+@`|j=f zyK~UFKkl$#V-I%wWh1?vABBIi(S9%cR=k|Z6dU=I6kIEB&RH=KeQAunPv z3d6;nn>WB~?W%WN%ULF~T=6axZ2x{1kP!r>92FV;BsZc(1ehruWa39;8sr_&4m_eL zDx}ztc*04)BEhC&1V4uwVLv_p7JebnNfuaQklO@-*IO#_7R8aH{Deuk?b}$HGHJYy zeu@`{>O59Z82Bx(aU=3GDioqHAZ}XuB7Dj}q)ER8v3vu?`oM(~R(=gZ5{}vHM7!W) zQISNe3g02Z`Ln!Xz=l;0ud=Kie50W-@#N0+Ppc82^q&(Dj{Fr%y%5vGv`f@RMrgvd zcK9Mzvc;$_u{sFXXbxoGbRd`pFZeD7$^=zh&~wG`jtcpa(G-$f7rcY?wCt>)-59^| z8}GUd;qcQG(Ob|ynv+h#nF&1My9|GRiv_hDxp9lzfsickB% zGFP_-T^f3bUQ<}x1G_La6KU}zH+F>xFE*Ih;PLE_q?mM3JbNWLq;pIL3?y|ZD%YBT zz58w8#^#g-(6l}oYz_S~iHH~(($z_+1|rH6YBntI}M z>*qqwnMz^G?YfUWgvslRuQbZv7_*sGT(1yq%aT(ZF^!0Z+yW6nalNC@f330$s7JG= zut%D90H;r(h$i$*L}7)Aj}xX(2pD&N0&2wyn}r<=&-&vnqBa)%E8z^ds>}_c`xh|3 zb9>6S9YU8KTqaeV@0x$;7thH)jxCRfB!p$%aYvu4R&Nj|omsI<+Jff=+08QFey)j# z%o_vY#LyyDAbM`#2HWDt&F5AQb)cnu827?)^X?(Y)?Y0olN? z2D@;Vbbzy8e+JsJjY}2w$k(qmAe2rq2+N5SH8Tz+<8fk2dwr4(l39AY3uDoSEKmMH2_^v_$3khg7HeE9ldj-sx8B2gTJvHd7X zQ4w=aWSZ14Cq_wo0*H+t!J@AI$!%WKnpY*ARe9s(^O*FJr5c-TP;t(yCfcb(Vxb29rHRb09)xV><1!n*p1 z_6Ru)Gn*ze+uy_1*-FAqz`1E6PtWuNLc%ZNP*#O7x@{BGUdqNoecu(&#!Bsdv0_`W ztF2Ns_gy8YT{z3CK#pnQa>OkBWwrny`=`IKtI7!WG^|>}`@8$L+8x*TA(vo9z_PbP zDs4(I7Kr=uUBlMIN-edYYIfnA`AIrC?A>Ha@v;&0H#YFG27I^JkuM)RADWg=N=Xwc8nuvJW{tWkE#Kp@O-sJG>2doHgw6_8b z@X>Qyt-CNFQmbXbY-gJ<@bw1TUgFkUqT{`=o_~na(aku$>%(OyDdK4w=LHDnn=Y{3 zID&r0Ay5i~=FPj3%%Vhi!yowy3Wlq6B(nzBD2iEP>5NB|z#tJ)tG#vvA81zRd8mjC zfQ-6drFn&vfyXio%t+JvshX0H=c|OO;3}`1-xArIVatame7CxBDk!U;W4_S6PD+r% zlI9*@z(tD?wjAZ%K7JBnSGBK$9l_kkT`kEf!W;SyoGUWmK}qR{`Zg|1xtTd2WpEIYnuPoj7@zRtTI%`{9b&3a=2G+FETO<=5@U4(NaLa`$-XH0$mfVSD zQFnk*=!#{x8sZbYf?JN`Dy)LjOviCman4`h*#sdY>A9h)zRcAD>y7Q-Rb6mnhn;+Y zsm~?0TvpTw=vQ1h&2`4h6!xs7(aw7_Mr&j;NwuQVVWW(__E5-o*sC*fcH=8|&QqCy zYpELL8Dzm6vPmHg3}IDJDjFb1g5&N<>$}L@4{dskTAWf5xY$K!)aLu$2V+HmO^`IE zLJ7B4@zcN7kX^*?o&W}wNE_dIyViCRuTgeP!@Hhm{2J5<$Qm+Y`tGQ{!Z)CWf`Nmw zh9w@JbNs2?0SHy0!8EhJwM5oT*zqovo(+TBqxH;y4zo_)r#4+Yy?S>=7rY$7y+Je~ zv58&Nkys;5$gC50T1fhUceI*~cVhl}y3%*9rrlLzUB*6e zt_4u=DgL5DNT4NOcK_Pb2CD+Mo!8Lhy$aS6EhSCVW}Wift+>iUL5FX#4}St$o)rC^ z=z?Sb4h2%*PofungN>E4(OW3&@v9O-d91X(F?r#(2Wlv1O&}DGS)DEN?{C9AwW=Qn z15_8*{0^cwQ=@c>=3?KM^-39v73s3HJewfQ>17huKw4`uHnk=>&>R=EA)_9xNd>j? zH3IQJ`$rXV)+h@#?umO1wLS7M^aa0%yuSpGeuuKPmMOBtO;@Wa77#NhVX+@gQL)lB zbAt6TdN6KQkyME~{xhfHW9B*M|2|AnHlUP(nO0`370|IY^ll_=|y&1}2= z+UX`Bk@fakd%}oPA|Sv|fqP!qnTu+^udD4$U~eSP2KT8A8}q(c^TX}D(6u8%ET;^TU!jy0RRLK2HIj;%sB%(c$%%4s}0mp!z`iw5ej* zYy}zh>ay^&7q&%}W*4Z#IR8j4^SRU4Go1GG2mbs!yP`6nFs?rtLXIo4m3dXe9jm@g zWp|2!Yw04YO$ZJa>$sY3lE;|<2&l_+rW)fP$9LUcD7#(jDaF(o zQZ5$e$icKl?B4y!`G2CZ{$8nD^|_!rP}=lTNrfv>-r>gd_Y06}nrIpi-K!UPVC+4{ zo9G-t^(mI5regV(blLp=lbj2?T8CvRD9!2Y1U=NYvf!;6v!M3LVxy@U{q~lfhGtYOktF-R+fy|lqBERYP z23lr_wlwjU`ZOR2#z?J9iI*TrA$(YKLI3;OgGG~>CvBIz0V~n~JjtrM%ET+>;l0sw z9j*j-(pkymlyiw~T+AyPSF92lfn@L7UF4LSoa32`Cd)vSn<2U5_}+YS>R`|aQYzFO zlF1lNx@W`I&wW)QBZE4WtE61@?%Vq)CJ*Ql<)^pyf=Pu0-P~*n)KPZE1t)`VB}y{i zjGG2F+6ouFK`zvKFf0VPMxyTi=m@U8B}pG@p2q>tR!sv@#h(qUWa>+ko&K>GZ6GEqHBRWPhn%p>1$Bpu zbAqUlh2WFqFoTC{9l(O%%(UlhQ@#ca{|u^-Z!EihawJpxfRj^`81IOyhXAyL(XKFd zETcxt5plR&!^6TmE(>YyDoN$h>huniA5UCNCe;j|doCPZl}y>hRiOc&%IL7dX@fWq zj%7k+I+L`w1|X6cLB5B^vetY{owgjhlx z@6QUeVv+i^VEu=*3Wncu5=pm}C>fr92b!`2);RxaGkiFB89M`rcr^Iki3Bm_>5BC@ zW^2qg^N)<1sUm?XmvLNeRN-tvd3l31kZl(@n4cUh= zC7dj(mIk=2z0laJx6nl(Jn$3J*gO1Zzur$QL!nNfOhUdNTTGbPYm6*lLVKQuR!ABh zRcT!|sI0J`KD=NovgK@lu6OC(y{t!t?fz6S!JL*%1|#RSzB08E$$FGR)2;v2mHQiV zOwZ@!p^7{}>;SOPG|$#&X?D`VM9Vq~5K}`;BxGo>2lnlzC6F)a%{t{oz+=j~4f$hdhpgA)svLvVU=yoOE-4JY1 z;2DP|-dh!$D`u{!q$``1%us0TME(JLbhPxD#gF$kg}_{CcG#a(MavO&@q?P+xWG8L zE3?QyJ_S&l&Od^FXeEuDAH7po{}EVItNzGvEP$jhMh%E!w;Y+^r}gyy#pKpr(Xlin6Ob*3qyByS*drv{K2 zN0e|ZcCJP<%u?kEjvz~S9g>8QJURpRB~@VfE-*n+PlQB>4J&{`@9ni-(IZP^b3|c9 z*rnz>IWaXLSZ~4>B5G>%BfVQO748vC2>ZQB%1e3Dmp1Iu*V#U0IQL;@Z91Xtz^S7I zceZMjEZOH!kk>=IZs7qs0FX;8ue&wL7{zgSMx}FH6B zugAUXF_dt(p$h)9BzR|72-WPo z6iG%;A0^kx=zT3n8$X2!AQ@%$DZ46&Jq}Ltw>p8$H&`v6A*qxOcYjD7=lf(*^nrEG zn=n4%ysV_)i`v@T@bBBidI0Tjhe2yoTL0mLoXPpaw}nn4FU|Kx|KICPwthIsj9-Pw zv9CfT?pGo5pL)}Ol+pid)#*P<>WKn!zkXl_Uw**!1|^^t*+yWU<&n347p*Sz3KCt3 ze-PT=)LJ*D5!&(JePs2{p|F{qcu-o{;KHPCkPKjt1VKQZ4U#uO<~Bn}Iqj|&R}rwP zB!uRy>0#|y;ue&dKCewOuQ-%UlzD@xZ6)ikgCT9qwdHR`tBNW(5><*1u4B^#(GZ@F zPc;b1$#(K+C%&GjYPspCu-f8VPj~FulC4(_=tTmnU7FX|Az>lY-?=>ebN^(F>-!F= z3uD?ak1JHP@Bb#F9fhn1ruvu(+Q;;yZ!x-Sx?lt(?Aj%iBE(o&lc}@~dQ;34A;3;n zm#!3O6@)cm#cp{wo!IXo3+?0y3BtF_8(ECI!-1!6r7)%@cFNOxm5S_7NMVESxKg5e zpuZ-?zH&ER4aV{OYvzB%x(xDwI-DK|$WR3w2;twcc64&LHgWu?vZteyup?UM-PM(s zulKfR6B`ILG-(SI?}BUK*^ym)$t1%xd-mrKhI!1no-Y{@KY@y(imGH;d>vPBHo833 zzFJm-vdQ~V_?@f`E{{JMAF$sV<_L|f`@FNL*YtdyUmf#s z4d?r`SGVg!dTiglJ-BznIxinK*Lw^pUhMN}>{@<%DCF0Z9{uX;7b(|G-;HyGv7z&qb#L}R?|gz2Jm+c~UvC@OcyfyR z;`{KD`jYz&e8N7o&t6}gx{mEy)<0uPeuH#d4m9e>IgieGy-K>jaZATz8@r3}`AO^X z&f!6Fn^3+>$3SAzgv7;=HkITccm5K>*m_g@)R+dcRwL zcS_fOtMOR|Y;yBdx?Wy{q_ACNv&o%)dh1^L@Koj4E`Fl)cP2-+taP2r9e4}7+V;*viYO1SIN=VNx^sKTW0 zK-H!0N`5maN7ybwU{ECJRUt5};O!Uo4yE!6o%L~c>AV{{w-}&+(N%s93z${*$=f1H z%Asr#-lu@Um;Y+&q2^PlvHSXUK=a;jiS?=TG%mgb$!jaS>TCjn{k^D-^nCLjz)izP zkX$|GI=!YDtH{_J(ma%~R0%(Cd*63lv*}%hYNTOl_?EJGxbNUtfnHGZ5`%Qr`nqUv zt4w9tpJw*->l2_;wJ`O1KKo+-=v`e#x7uAlj%iKX=u4aC>`v(dU@SOdZsU zTc{TG*>~VphM##$sqzbU`Op3nneP?BqFhFEN`xiYpHM66@R~9ac7CKdU}1cN0>;kn z_xA2<5}b&&PjwS7|JR(7NISI{`Ya1TY;$tv-tYCee)BIkFovIm5^YOZF(ZN-9*_BvqkRM3XsG%qryW&Je1ZB2?+N}q5TjYlt=&r;MvjU(ZgtghI$jTRo zQY4{94zQ1J_9M*P)8UGVLoCP!lm~5**#T>?k7)5D%HE(O1||T&hiL_G&|?E)0pJC{ z|8|;xb)x^f6KtE*4p`t{=T|jilhJ|EyTcCmN^F4q+xIzh{Ff==*LhU`m}-7`3;Oc= z-%S4l3apgk(vrySe*j!6kbJek{KFy{0B)T7zbO8vt5}blD)fMque1IuF&CUKK8lE6 zT2UMJXbE9y{|7}{LMRG%Sg3zk&n37vA_D%N_5U^%I0`jPy0mwq=w@>YYt9pP;;TA) zUp@o-YWQjrEVL(eOTviwr%)uumb5gvb#D5$qG&DIIN}Pc&mB0lFAt4bf6zqo&}k*V zT@I|j-aEJI*baCKy!9=PyX2ZLx!r0#4bJ9NZ@X0=&-2P1?jMZgZ9heA^650+3ll`! z?$9msCuv7tvczG3zS_eH(YjCRBhsvu;$?0QtP!F^9RF;0=k8x^--*&|roJw|(P~$0 zuieqAa4R`A61LPLfQh;G>coxk&<=KadyKvhT%EWW+kbH{7`SD8UYdYOag|P3htxnY zC>e4MNpR20HvU|)cy^Qi@O9TqZ0hxd`}A5T*j(qjARQ4YeZ(aOID3!Hxj=^1(e0u= z6b@XZpGi7a`P#b{IwW?$%-3j>H8*>3Wk1l@*6(*v7B&E&C+XMMQPb;%>b~u?G16{; z_0&oxiMXAl+!yJ0@i737XP@gL*|Nll?c#5W5@&A6*}Do(WCTWI?>&{`XF|r$oHlgj z_Z8dk2`J+F(ZZ;lS<@o3*_Tu9U+#10>@p$y`}}#LXO+@F{BS9tUIAZq4>-LY$zB zYJ_FNb2{}@#7LSR+qEx3n*=b7CEVps&l%L#^89B=W^Ud*dRq{s3RdQaj`_?t4D8cg zL6}Ud?k#OlFunAj-WK7wL+ysGuXgrk zhwpxubNh!AXBQ`~G-fs5gPvY(1R&{6++MA@#8*Hsg|Zjw$9SV`5LvAut32s))WYCX<tC`@AL9=FBwp4gYA|a9~-Eq?EdNqo;muzqL zROY*@+vanZiHFzry++e{`@(>2zRs>rmLc%d8BhwiNqScNZ`La66D0vCXkp*e;o+OU zpS7u6E4}ehXrYW)aUL1WMdGthJNVRY9Mu$6@!W$cZ?opZ$U2dhm9TIIsxVY(p+(pO zCN3(~og$qbyc;!iuDV^2)aMyiOS$?ey{?G!GvCLZme=)Q&dDe25(>8%tJil?T#%H0bJD| zx=Y=L0&8J8ZsAw4RMtvSCaWZTxcQt17GV{9W1IU9q|4|lA>*l`kvc$_E5O~z)_6`~ z+&{zgL`v`1y_Cb-ioMHaoa;a@y#P|W7Ki03$6|X{b#4W9n2iOjvJ@~h&$5PypEW5F zd2Q?GBdIG&_f3h=Pm)Z63TZr{?m9XPx9sAl?zp!0?Z+er<>_y7F^A9E_CtdYjjIh$1*PQ$+#(+KO>Dt9ENDax~64jA`=vuWMjwK zj{~q)m#xVyrYuT%ip3u?U3#*yZJ!KL3PgS$cyE-I?{OxQGN7YJr9LRFbGT8TLS3}9 za*Cn{_mbETFM%=!o&mSgu5#f$M4n5Hn6yF})lkFoOU#Z2@Gdk`TYU6X(NprJ&tZE& zgarmG%p@o3*bZm2L@2n3M^KmZE;*l5*~>qo*04QjqKJCCSPpr6?0QEZc9jWfAp~pG zXq0A@$QEIRR9wNAZrLekr3Ht}l5bV@XFUA&5;d~Ymt=LF~Y$qHr;H$elD@835)ZsKi9=UCp3rl&VhwKfW) z1Wr|UVr2Iqplms(mf3OqW8OhqBZ{>STf-9AuJ!G4XQqL>I>1TDF&VyYb(&0EoabCB zd_t_qLYw%{#7rM$bW#D1o_)5D@^c4_s1#`HBT;jp#iz(k zB={*`NYOw!_DRia+N!5P*DwJYxX6rBA6{*~I}1nlY{A_PCY@`+w)&0+AfJ)%-^kcp zT>Mpjny8dh6cqcdn`&iv?kRq#DAT1JR(&S7f5c7?^@&I!SP1rhnzlbF zz>h}I&R9dx?!XUa@v#F5C37jQyhFq;6-H6mZh)yN?I_B7tLfm#29@e}2XfoNIDIEJXh~17{>Q?`V_BpkOkV-7E z2ToXSb6nWb1iozMapsqwtya*ZiL@zn*p$cLTVT2zG`Vbehi<@j-CB4mv=M)BG?M>h zaBTJ8m*Bp>~U-3%CI%JVe4KBdPIvUrxTK3q2mTpjr(O3 z{iTWR_rx_w9aKl033LY*s4U2oE8s17^euDQ>~DxH?glxY7-k?JpTv+-Ro#18eDv_v z6_VyAeY`XL6Iq{fAxNS?yKp6-cX9wl1S#e8IS8-KKa?2FCCGJPD5^4LX0`TBlv=QU zpICf8xH&qf^x`5P@IaU`8<}rQD!Rp4FEmW>BR1ukNqSA@{p>^SZ7B0bxU)?MxpB7~ zm=UqgsJCoN;?J3;1qaA?ql^}E2qUDFVVuXG$66N$c(`|%&XizmmVBGqja(at& z#J>~Y*|LJ(cvu?Dip>2r2N%jwp#R7Guc}MA1${q*n-?NA52#A94l&|Fn;SbVOcL$564ea!P zQDILWEA>rMcyRpwO$hIvZB79?bPRY@sAe-OdYW)(-t?A1@wojpW#9KPs3?a`}qOFmyYO+i@=P|1?dpS8z_{Lj3yt` zxE$x%DS0brw0G6~=8{LP!X3POtzVHa-*=#6sIl_r2$Cf+$;H^3Q;VhC%I6tRtUDyg zcr00^w_~#ioM@vJ9AW~hNA!OuuV^mI=0WW)>u$X+#?^~~7F1hmUXW~>C!JWFIo>CI z3?DLIw{nL20UmCTi3tJk+-ShCti=!YAo&5|e!aVrqEwAz#d#qk(-X8HC&(L?Dlwc~ zaY{(Ble)j{hbIx*(5FkF#CsNvfBbah+iqt1g#$+VZyYq>k$>aNy3L62 zKUr`qgV8K~6^>!QB!R$uNh);mKt$t_j<&T$l25r(E%%sn{{f2zj27X?IS3jAiU!OA z`1Fw$O$X0u65+4QDmXWWVXV6J%s(Ao{zWpY#zG4cZ`UAs&-~E(67IL>nEs@VC&It6 z9r&f{&0g?32)eaw2nzInqNNi54>Vc4|1UIB7)ISKnYR(=y)%xL>LJ#a$ zZEq3&8x9$-s%T=u%ZdXhBt#!oCN$s(t?uKB!4m}o9$=B7r1b_lGbyF%zOAE!N3c0k z09wZHc{gi%G+-4^5KBx1RCr~uxvP&?1}qD7uaz&?F7ZtXRKw~YG{Y{2?cXvx%Pf#$ zz4BGcifgClmr!E8WD#=8^P!GlP=V<~2$YZzbD_k=)c!M+f?_sGNHx162#)y~T3~Zz zfcY|f!EFhVZkO=?E1F;W{13S{wm5&b5$>8+{qMo!h$P;MIW3-89Sb?!S8@v($oHS3 z-z2N1&V=<^_LSN0(i(9^d_Snn;=z8)1UUe1X`-VARkln?1}Ozd!TMJcB%%tT@wLK; zrC*Xtc41P6`refJYFBl7@t$o8Vmj+x3POhuiSEX!t%+Ctx}GQe7}+kr^&SB!H4XRq zM%zpBxUkrfWMvm>^5aNeMFnviNEa?o&lIS*nnh>?M^aOUyZ#4RMPI^p0jWl|yo@Ye z1Zrp@VO`?27puBGLV15WJE0<>5~MJ^{*7NKa4nj&$=p zDg6??E8 zF$|{|x>1LFds8=L2(o5x#)yC}R{9;q8pL0~UxXNn+l>l>1Zv1|6A(J!6f_`j+=P*1Ue zZUYqR-~GE>}($Y zSGNnomu|T$B6KBfJuVG6?vSi+V*ltiS779aH~u=nS?fhM5E?S*7%-rwoeP`Vyc*V3 z*4E3+zWx`MtJ9z4K^!|%S)^B#-{g3CHG^x>*TFVJK(>9OkEw-~&om#!O z48%%{DQc`y)zPm)nVDP2!O7T0Wd5{D3!Piv+QvIX&b3?i#^AAf)0IsY(LR0{(+;dw)3{W@~#YIg=pNt6W~!vSKI z))vcm)S1`?+M7lS%|WfDI)GVgWehX=S=*E2jnTEz76VhC$dj+b?6A!k@7%}+H6kp- z9FY&tSceHGOLH!gILz)bGEUA%|2C3NE;oKXU})ehF{ZOqd>J|ZhtcH!HZpN^m+ENc zc97l4?C1D`A|}TY4klmFI#;@+@uu`D?+rJKh)z5K1+rfBP!41=`$Oq%c zPVu*0M@j-Yq0#?*Da_FjD~G+Ag<()JhJPUg$8ZknSi{+fO%!5V*N z!r%6)LeexD=>wK2ipek&ubs@nG&mmJFvo>F&3~ADV+?gXDfTp()NmgUF7z3P_|xjp35>=yAJpJqCdAoi4*Ki1GUAVv^&1a zwS0|GO&QC)0D1=IC*qlFvC*FEKP$AyhwCH#*yjbprI`V$DDona@c&iVpv+%RKZ6}V zwrEm|!VmFh!t<xVP z1G#NIwV|BevBFCu(4oSj-fPpG7DRTXn*k}n2Cl#{?e`HpDf#A z@Eo7z-{9VOf5GSc{{gpwo9NGj{Xf8&)sfvgclX(vwf_bFLtU|R4z06y-UcX{feNOU1M`z zS57d2L3rLTvWyUA5?H#f|CGw6lVd#!&obv(fHX0w+(ykZAD5Qp)7i1hSar!I957e< zfFsF(PB+}>Y9xD9)8tv;{k8y^u-!ZCY~nofsRXu%c1lhNbH4DQ39D|H!y>v)#A z%V!?dQnh)rbFiJ1qr2tpRhj$m&>bhydw~La@masT1mLsUUT@*`LB)o_`PM>kAwdeK)wo4!FC3cMq)Ogd1lhhCaTKoBz8_%AtFGP)$l> z7>2x^$~SuZR|b=75C8$nc90=Tr#@qbqj^9doTq#_pC77xhNd5`IEMIFnxX3l8{ZFm z6W|emG1n&Ogh}7c9ef2cJ9g0HHLO89a`T_$jAFmYu|W_4vR#)me9KiSa|*nxds_Gf}*d%0FMO3O?@mx?3+|p>2(8C zSODGYx!D7-S9m9Z7~!w$MYlbvC=YaB!D~PH3LaPQSMYG?fd6~&Rw)+$6+CPD40=G3>n(eC8B;x*3#Uy7I$bnu9j~jt>){`#mI(KWS6|xTWByG$Irdt_re)GM65~ zmdNfZU6LY@_2AX|k=hkjh|+OsB?#y?Pc5Ng-48~k7@V_)|A(-5fR1cg*oI@fTk4s=K;&?W)}dB6m7`gSh_S zKdW%My3|NrgHIJDkFKK4q?>+C%%{SyVBaN?pf+I`f#9K^VmY6*y@l`ya#Pf=uPbhz z%f42EzD`1a>Em+Eriv+Z}aTE5T`^(5ecqxUsJ0W2gqX z58&PL?0*QmFgFC-@uYOQI(QGO%ai07T=LO`cb+T?SM1&mz%THrNS}PA?wnS#)e;`B z0v)$y!h*+>4bqT5pkSmdj+MTvj_IUY$nX7K5EezVivG)DEsL=WBQ2-C`!(Rl#Z%U$ z63Pt=&>G6kNgi_8)AsVf^}&V~Z$;Da>u=kbg2`891sFFiH}f2qPo7atdG%wu;;<)Y zzo&19H&0=8$?GA^0{%imbo8a6!A|wX1n7(1!cJN%KIq?f?}MM%HYZEr4>(|22-|u` zJfsbLzW8Yx_~5gGFbSZwiSmK*x|BquE@~S202YranfjDcg7eaQA={m4aAZCua-i1F zr$Ymj0o|m-X;Cz+vZCf9Bs>Aa)As@C14+s5E+#sl;H@OPDZ^``7_SN*46NA!$0rOg^W`eh;YC%C_~(A(;zemlA^~P%+M^qaNjc0F7fsdE ziw)d7f)YHO_h{`b#hr1IsMOVtFk5)AbeusEP=qfFN#SsKpId?~Jtl2loU0iQFgq@_ zkaiVHK0=KyMKyOWfOl6pfg>=ss?bBu3k9X|5H7k~J}$bG{}GJ>miWl6vtzk0gQp$S z`6MJK#veq*=Xx#54pBT_RP~U~yni?NR5ER9k&DO&d3*w{d@RDoj{YK(>7%?ZSvNO6 z+@xW8vrhAn``oP2Q}r6duK555}i>urMIIWG>nP@=FIo4cVqe=7ek2R_O7r1$b9@ zN87Qfqhe3AA{hE>tInn6E|`q=<)>h9fgmY9*GVma;I029c+1qH=O4kBpMqZjf)h># zJu5xO zienN$rv&Tshf7|UAt!*aw5DXs^!PUaSUe41UGBetNoxW&_l=wYN%Y9|p$UNt44!BU zT}Mr-YE4<%?A*AAj~hp7H@@GSW!Qwa=%eN zT;jTuW@~ep*h5l3I!`wNyo=n~Hw0wUs9ZXhY)Js5DFGtEs^U<)gyr4b_6qFbKF@&h z@c`#Te?|Ior^oAz&;DUwvQ<>`ug3g8X0vYAnPQI-UfT?c{>uGFQ=8!t(4sOuQxFyA}>1h+k+>*?F_niW+5cRz6cVQ&g>$m5a=yrz93SHrJB2FQ@pDYg0L8wp{Bv4Iw zJa#J)a8MylzfTJ*@%jvuAFYOn=hF-vi zg5|17Ic4qIBka~1XoqUKfCcT^gKyCA05ksffk=298w79uyWgnlFzxVEQ(^4d-Exe8 zqXH4*$v8JCIO)AQ;d<-?cC~|cqb?BWT;jdc6X&3#maJj`bNyDahFVZjt^@xQU=5%l zFxrFUFMzeT%q`BIEBao0p6XE2dyXswnj%!ZsuiOA?$jvL9SCAhn|8iu(4lli6NW9$ zN@nyew=2dF5Iji2et=LpJFR98ntv25>ap4nWdIRzZuI`X+U`%C9DocJJXHiZV+{`d3mNpbXd(Dg2nsCk6{URo*%fn%VFZ(UumYQkR=HaS5I_s?W zcnOx+ZrtT&R16h}RGHta*nh)(uB5(For0G&f8!Y_`Ps;>C1u8PEbk4SW>da# zmRku&BN>DV2w z(otww{1ZQaFO6#{(X5pXjB)B}^>#e@t6*)I>QHG&RThC&*a?zQbj!` zs5mgylE?09Ya!rd%4H}lb847c zA@iy-rk=_jNYw~-z=Y*ys%OAB3?~lMHO7JkYzD5+SQ`cpaLLHcg7rBHu%>R?%XNek zPntmuJVFSx$GBhHcm6iyul#1r_%AAK82}YTK7a~q+@J=_?eu0s4PX<_gkU_BARVjL zCb3gB&eV8da++5K!WQXUIb3Sueru=k7b48jQHRV-mcS^r5=?8gVS@5wg&&FL(oT!{su5@xyh@z|Bzq*`nQu zc-egVCS5`&cr@`9ibymk2sG(!q^{i|I^+EP|H-JMvgqgkBk{-xo>c{7oX>Rf{AW)Z z(yEbhj~CN!M*L*!cLQURa$4@#A94dbs=2o(wRP!#RErSJEZUD;D^TT*uD+?z{INb> zWl87DWNf;n$6zzuUxPPBlG9&$y0bxxWl?=vsHub|))z9uPyowg`Xm3wwQ9l?<^#@> z80lLbISx{IDRC=)R*M^K5O54JcvOeCy&eHgb_+_zx*40K2hsx;01bv(KzF>N(|rV$ zgDZb|@J@*p4)`sW&n+l3!J+Qfv_Mh*1FmeA`;9~hJOBm^9g2FG@De9T&;KnEj7GHq z8XaUT-P7?(vjUYmq2A-G2)J7|M*z&5AI!wV`7y3C6zO2L>Nx>w|S+84NUg{AVNS(W`IS6#t)f@tZxzIsUZHaq35jJI+GSf$ zO0uA;2NiiD!M^zHa$aFbT`*!0?8Je!g{*hZa_++s$6v@4R(QZb$c*FDAH(|}B`zz0 z)2H_f-C6`^ZhoBD4imo?b2(oVu=&e4KK&@V-#~ljrPkOD9U*!~zZDQ<0B2?NbIX^f zl<&Z1_DFs{DBHMM@6PeI1QE|1%K3$6ov^xI~IkpXaiutyqoc-Mh9QCp-Seyn=V38 z$pEg6uT+P%I4_tLOn3L~;N#+t53<~YSSA60(EbLp`U^8EE0C{6W{yPL!X<{LLQ*V7r7E^Y zw*b`h?Jx29$H8*n4;Euzy1})d2CRyJB(4^z=6wvRvO-~LNP^?MW6TSHHX)@X{bPhP zC55buvG- zgr0lA92Xl9kYg1D5a7;Se{Yfad)dCKxs#cgiHWhH)88xjy|t6`#cR)Ap3obP2jsGn zfQU!NyeCmW26e`EuTt+H`OC`YwbLr&j-{!a0E_qqv;9zpqa}xf&U{j+1hHdUSR98C z!XDp!-`(6hU%Xq^E7(sSHsLv0vQ-erTy;5Chg>K1I5}Utmd+0JTUN;j4nwrXin#iHV^`Q5>sMjb-*H+Q7M{fvtR z$IZ6+Rhmpra_EK6Yx{;3CSi6wkF>`tz1ZHH@5j=*b}cWT4MFdI(ZK4BPci`)-s*cT zA3hHOHzDtb^DECr`9oV<)h3+_V%b8~$QyY@S2>D^!1!X@g13W7%kr6N+X9IvGdKR% zDS=;@%)U?6tELE$WkU;ZZ5em#x+CAa7_v4uc_t@Ho+@wmZVcX@yc}BI?>7l+A5yt) znV6p5eQMtB`%ZuNK3p)b+dY#mDg5wPRoU#oc(!?HbyLH)F}gDEl38ptdQ>W#DnnzP zTo(Aas%ml?$FP0*_D-);=NikANUsSC1>1N))IU%8<($v4mw z>y_LarDQv#R1=Q?Gu8JqA<~ZbN0k8?zE>6nHVy8!TilOYaAj)-U6>#u{ty`jHC|Xy>rObXrPT{J z@y1}3tdobuB2%jYFCU>>|Im2v{GAEi^w@mC+}?DLu^xwjY{2~S(zvn|Zxkxr`e@&w zTj@H+!yiu%?m2N*01_xy{R>8obXgO;9NDTCWD(6()90(`YNel0z?*6}PLDXPUZkDG z!Irl&4|6D0DSm1n+F8`NevFHR&0r`>wfL3{v>NFe2xmF64e-RHI>=&Jqo2pueodfI zpslJ^^gQ)b2n7!Sg{2?!xD z2mr?RUt0f>^c&=u4%rq^3>r|(@}FYewl=U^jA9L+IO`ysaI>E{|5@NaJXed?1LnF( zHUPy?0L4D7Ay_(;Nj)Rn-&{w%1n(2?YkqmHl8SE!^6{GYr4yiQ8oL7>I-x^TnVO>3 zOWJ|I)vJ4Q7)|XC1hip zQFvA*0o+cDfb;GNJi*WT&6Rx}Sw2Ri&51kQ#S?`TK zzsWh*gk-jJySoGHY#-65xr?b}o|ZM?bJEeZrHajD@72~vxq+sNqmfx#O~hE$r|xz zWg2;&%!|P2xs>*tnt|1kf*rg*n*t5XWkWg!cr6phcwQM{1sM}D_}A+qm}g_m+OQR9 zN5;6kfSDKB#uWM&slv<2k@E5pyXi=on-ZP;hF|%9j>xjbTN-jBNtz+y9a?9~pW#^T z#LK}SK8JVehBpVYPz6z1gCeqfSiELwSw=*l8t7Jz+`royxO{;W)*tHQ(JFl-3f>QurA+FUhujn2N$O_BAFvP{BD5_kZiY22?5+X7Um{K^_kyth<4Jz8 z70XiWO$4NJK!h0q-9J?+s${I_J`_%A{7F78vJ|FW#Qsu6V21x&kMb33DB8EXn!>g| z=Y<(#FUcN(qTYmd^ywVVsk1Q04_$NGPcJ4$6K6 zc?X1e0ZPGXEUHS03G&WOI#9sHta%X|*JSfXX*EYTaEN!VO}?)0Vh-=7$aVA`u(P0T zHz}(#!4YfHu$zk^={o zJ_|3KHXotFn-mn=dU+{>yK%RRljX$Jhzad0GhG%vU{_9R|DxT*-npy#3VW_)<$xrc zUxqS)fNfxP%gzu2r~7mZQI*)@sdiLG#QVBOHpE*VMl=h@L==T-y3bc*In_c7*UYJ% zFznI31SqpCZB%@oKE#aF3TLFx(|F@BHvCaydY=BXFIcee#HN*)|cKGru8L<7~?a^h)bs zb@A5^)9jX0?77jkSAd5L;a9dDRcqzp9>@Q=?oi=iy>%;(J$k%c*6T{DOK@<=6P5bY zlV<$o{A$0oXyDmVWBBgU6;y`&EHSR)Kv}e4o8A#$APb?n)#|i<(U{!wFn3HeX`-;r ziZi8t(X?mv=BC!bVIz~CvFyRdIcje>I(ai07JtIYbYYE8M}G~i@<^d*pP(Y`cM$=b zb;#0f@v27jyO*2suRIYsOtpo~{G_%zi&4TVGoMi&rOnJF)XX%3JI(W%QYzo$nZcrV zjp!)NlNtBfTZ=ap6mTx5qkWXR*7{LrquILj7`goq7>qazgPooglh0EKV5=a_`9EzYc5cJKDT{f}5WZCpkg z1tzVO5>*jGSL__`e(hQfw_Ym_;k&gb%hrnMxR;znx)F|jbyM=uY`M>rROXlrCLW)|{p4&nr|&ycwyPMgwetFGh@ zjZ~pSSh6D(i+@m(<5w-?Cc^-Yzc*))pCTtRkhk0-41n(_Gvo-T@Tj6qf0;J6%~dIK z)O^03pL;x_;xUe4p^AQ!Sr-Y85^;&!)g3KM-Hsc9mprbK9`bjH55mnvJIQ1ciGOK6 zbyx3*!6<8=t|;omFnS)*bMR=ZjXEwF1rw`ReYpOnj@xVfb>CC5%-_2rT`BnVap%d?oE%`R-%*Hd;D=zua6|mW!gq3or&o3`tKI%CwXNb`YO|m*;ZL} zcS&aBY$%CAYg?nyRyDT7F@eKF52bORaf|}$Gf8n~1_j^tOxm=jBl@BhpM4LGqqwWf z-$h5U^=8^q$%RFTVrQeCtm$8BN;kzpge;z49YaI*=BOaj&vqR&-7t@1WNQW|u#MI- zymrz_h4{Z{Dq99E?U$M#WE3h?Brb4c|K3LwJ$o&jouu+M(m(CIF!mW=E%7aG@}wS8 zzt1u)oUeWc*@As^PQ&nf9UOcM7<=0K29h-%rPA1qhj-;`-3b9W(+pkEO`gQZNKWYs z{;~2K)|pm4J`Q@|>NnyLNx{5Fmx(kF9pLn^|sOk6K3SA_o4>57+Q zA){sZO`#yPt<-kg4l56@CPD^c@WDMIhu#9rQhJ4Jhesz@75p@Kd%}jGJ_TLZUQ^sx zgl=k=v#Tuz_tVU274Ysu@Jq^K498@`OW5;XlPKZ_r|sJ3n_Um$oeEd zJ1yocdfLlv_m*Xh7O(5WqJw2K`j+7m2*4wafZW`w+=6Ct*P+LvVtBL~r!GNy1dXnm4bZOdt`~x3Ooj^^R>T!gKgFT!&$c{3HWcJ?w@c zPf)nVNTFtqw}S;Av|!RNRW6Pa+Z$@!0Bw;JTN_3!(Jr$c*aI6*yoMJLOF6P1Yw081 z4<@qeUBzu28E1VR{+=QT>elmrSxvID{mbe@;Zi&I&ps(u{c8&bSG4oUUwwjG8$ZF; z(yiF%WLr7T?RLI_-rLwY%fB3Mxb;}^Km7lzlVy##$#z|HU~A^UKm_&o+=XrCSbEZ=X?H*H|tqvgkGJNgr*MGu@UhB;vAmKs6pXt4Nvt?jouDmh}A)& zwRD}EW6q!L)hX3wfR@+*w1ndMgk0Hr=V%dEhU$Z1t@+|lsYjcp=z^va#VL3}dvqhKPfvTg9s*~!KWQNa`gFDImt>aHGPu(m}A0DF5ac#U7T4D;sqiIAW!)Zk8)!ICA~; zS0uIA<{QRE1hvC`nwGb14ZIuIskQ)#+Z7|GYD=*K9oukc^AYszY&0Eg<^f~y-i zelyIB${h`(dw1%#ktB|M?G2~Kk%x(x**+mg70H<%lHrK96;iepeobB^x&Vo8meA=hwr3Zs3xxa8DDn>=> zyL2Ue(K4%LSztVpH2FTbkXytfLZ|M*h5{vGARrMI#(uQ9Y4}~r*5%QqiV1=< zSwui+*lU})rG$Rxu$A#r&j324SoN3C#B^aPaQ zX_}8nB-Pre)kUS+=3(6#wJsXqgD$1?B%;$hXyOYx<9b0*NF+e`)%J8p%3(m=({ON1 zcdX$e97dQcpN_7{BBov{P!XTih_Kw2ZlH+kKp}+}n zTUumuZssZsCYXbfHb)s+1F~lw0tMvoB2>vEx^)!-Rr|~*3^o`9^%&7HHf|TYXnfEl z`GQiXS>RrRHQ}GXq*5qiJBzr8wDOC?L3rZX_kSKF$o$_=t*fkK_F)7Yb1zFpFHSNCzw$ z!3~ozAfhJ1y;c!0(hZ~@C}I#)5ilLb^r^2FD$y`L0ix7Q{)@j*en8hV#EZ-1xVLD4 z^+Jf&C0t<(h5-?S@UQ}(W9NGglxo+FS|UTGPPbT@qhmcSu?InMGY4~7BCD|kGX-j< z^gto2y$1SKVUop&e4O}8v8PG|PIEb3v>~Q|49%&IUgD+0>ni}BZ z?+x(80Z}sgok;$CN?0t-!Pq~M{0E}pxb-eHW=oKO{c^Ff435q5#C15EKO4;F|J-1* z_#}@**Yp6laF4^JI#(o*DI-#&s%d6 zxP6a)Oc=x{ZafV9XnOn))KSg&mwUlcf?lA5U|?$Ai$4E8zw91l!gm^km!W~a8 zZX)?HhnuKO*OJ`wMD#YKbx=F>6TL@fnOfsWL^$$S=19dsf40v3uG7I()oV`~$7~rP zWL-~tfx|C|E}fB#ag{0~d4n?5T8jPkF^wQ!#E(wo4;>$A*3RMj>T%YtX)KYAb~k3Y z%IFyQ*q%At!MUNe(}}g9q0QDI!d87eF>+X)drB{#8xLe1-`{;vDNEUJl88E8OgbOD z1dzLN2FkMg5abOa<%d#mt#Q^;^DmmLX6=7Xu)Y}W}|%D%6rIk+?Bb8&kc zbbIsX$(EBo{@R>~n=oHkWh84drpioNvrV%SfBkGs*LxDWv>CR%UX%;yYwZ1(m8_r{`U9suLxC} zSXI6_awbAQXwSae$SbCI#&M<6>5k;|hg2EKjh-YoKvfay_>$^$y(XA>#hUA<4*;~ai9?>n*W{WnYN65-%BSy}?E+e-fB^Tdgm+uIPcu&~a@uVbWkpZNCoD)lW=UW zm$Qu98z8lm^p4c@%jeaH=ZY$K>)u9vUj^fcj`z6P4r3hwF<@P{x0Nw7m`B;bo@S%K zYZ?y9l5e^r8Oq)W=z+}p+{&jT=)g(!{}(^U2>Y-)J^$^;)))ILX(Rl}37N*9yBp)H ze94XEyPfSlSgxnOT6iisRSQ?eNtZ35-axWDqLwR95q|Zrl)$~PK&9#x3ZNCpVTVLs zFT0CgRDKD8ZTqSjq>^7TGceZBQ8A?!C4p5V>3#5ll<^Hb1SaD)e2ZRqG73JZ7f()8 z=V7%9yUf2eGQPJgj?MSv0Arce>ufu|HE^mV%G7&j0ijXm2!6vyEwww6S7{E?O6q`f zz}{mXHh$CE9>CIzmWCE)tivos8YfCwX?b-9Qp}98yNzNMWpV5$4>P#~0!5X9OCP1$ZHZx(6Ax;xwt#f=j z0V?1o-u{Dc9%gb7_7!R37#M;Xeg=+`QJQs^M($ibi6YbtU?T4?jDJiV3yQFQngFQ! zf18Nx7p?o+q4T!^rX1)QIGT0>lhu6YW2A}GV5(19F7&?me##VNcpaqhGvj#nfH4MN zw_qW08Zfx{50lOmGZ!jXfSyl&2v*`W{othn@t;MVL-*vbAmhYoKX%aau(AI(k_k6BfC4B#xfJMV?sj{`#0CKpCL0<1A=y? zOfn7q`l?&q#>FgGji*|L&$qVQx3{q!f%G%PIwN~2Tk}-Lj}@DS;jEvsL$fz>N5SK| z{HOP+eLcta3*p(t@FxAZbRznS4mBY)9QdK-rgk_Rf#{wqr3%Vuy4R~BBIjI!f^Z;G zgc?L;?$1?QP56XVo~KI-9v|-;krS?>4SX>m+56PszIzP`&Raruu$XD$cCgNza^7}I zVM-o7J642bm}E`JIsOOK%y{QMjaFnr&cH$9e{BAQ*rPFl>NKM0L`E0Lx|hMW98fWc zp{vSo^ad9yYzU)Qtkv!y$?Hts3%Luq;{xMQR*}qYpPP<}qaDd~jcSmm?_!+y(e55~c2F&-e#Y0o$dybic#2Vs@h5i9IlpGt}( zqh#=#hTFf(r^I7=%C)4~UwDu_PH1Wklg_$URF((cr_(aS*{3tAo}{B80N&rXB22(c z_;hhi&5ZxI3wPik6CJCtJ}Kqb_v=@A7sY>kDCHaie7rmU%g6i3S}ill|3idxo_dDw zz}>TalA@0HUs0f@d`5Av_%De2Vn7tA|BomJ_4k4Y_8<;JYvZpiyvW=~$F5dz29_*3 z_2HWMe1KdkV%})eEvD1PEx4qA)>~V*fVFi^u3w<(j<3A2ZPv)LOCEfWGvm&};k5;} zu#nY%bKkh$K*X!*UFfSQ(Bvk-Z2t*Wun9p7C8h zY1|VfQ;=kW`n7UI^&R01)t$orWd|ruaSY7~>wP>G!T3N1iUr9;veqTM4o}1(U^LF6 z054du@5i*Fzc_76E_y=k2}YHn~7nmfdvu9hKFP!RlZ%UvL&xV!HLx!B0TYXVeUw9Aok znLi!X1CD-OdbbvR8EFEVy^)fC>V77e6fga8}BLk4M}SaMSLb0whf5G@Lp z6PL(RT#YngH(VHvut}c?m2ZohxliKclIW;Rk5)#o8Hhjt$RjcI?hNNAFf5g&84XI4 z5xDztLZtP+EsWYkuuUL>_TwNxHIOaZ&9ggGTBZvFLK9I`{ho1xrP%O2Nh_a{wjW0} zC@GxHV4tEQCs7;IvOfgQ0KOlGuKReXIGt{>7!7eynE~G^jP0jRQ{jfg{}PLXX8GQ! z&8MR3FI`1M_0DReoqm*&w+%n*YxVum>ypzokQ0)zL@%nHsU%rxZ(^h$W4{7jbmtKXB{V>V7k6c$N*X{27?COHNBMujkwP`yB|>!9S7L+rqCM;gn}2OF40a^TXakAT0w{1ku^}UkPMg$9+xt914xd~llUK!NkINvGH1$^iIvVknjX8n zSY$Mr96A@jY3^gV@hmrsXq15=7rg{wMj8f1{j2p@wd(sKc`>!`u421#y*uDux(mEm zrjQP2l5`dhKNa@CXZ8q9ZQ+ZK_k<$(IF^HV3RV>~1#K{F$P`q%Q}ZJ9F{W`O*Vc3k zE}MffkEi2!5sQik&FUJu-t{i|>@xKl`tH6&*gPfzDW+$R)%>V%t;SvgbRivi* z!~B|hgf}|wgk(W9pFlAmNzs?X>X}fgOgh`D>|pGFZ(K_S%gT_f zQ%Lqj8E9Rw!f9ks-%U8Y@t~R{XN=wGO$vfIXqAzIJ$)*^MB7>+8+Jo@CPGA?vz{&lK| z`V$-q0$Ikj5!L5J?n=aHaW@v3!!}eseIl$xj(4gSC&<>;cdH*i2$lg5AGUB`*Hfv95aJP@+%FaUqJIR!}@V|Q9&E0qo#n*4L@E9a|M zHW8-m&EK>nB_isvXn)air|yjFNqQj_djl*97kuFQWzoAy`F^&>VQ|36bfbHANa@QKy=H4v@Ee8n$Lt zV=eaqseEvS{Plf`)S_>+<_!^PyRoS=9hFiZnA|FA&LnDfaCIz;U?mlfEJ;X*)^h6H zG$}P9<7zN7vx`)#{Z6Go+}pi+0oT7gb&sed!HvFKUml0Cm!amykyu4P`5umd@%d2% z-VzqoS?3@iD&t03beO;ut^77g7WUh|jrtQqI&J48ar<0LLV^&+Nwnh^CelQLkc&G= zUb2kr2*e{Y-Xm!7y?8YfqI$BgTR??!S&tXHd;Z%{b$5%dvz2aztm3~(7lI^5U& zmA6!yv^N7F-*GXE>n}d`Ews-pWQHhr5OdIe6XW&wFyh$(GW!oJr2db6P=6;T_?laG z1xb-M@RZ=1~MN=_QrJIduszEFkjQ1Lc4o{do%47w7EhF7Hu+KMGy;1kQW(vO0B(a%1 zZjxKKAr+48>`P(Xt%p@{ODuS|84+_3=@$q*OZ>WUrYVA#%X&Ah*%*zaOXK0&Dts|( zvpPf=U|=+?--@sn@Y{^{4hlJf0sP=a{@;Q7igl~I87;8H)w>>V6*sB}1^ByST%GB* zDEkx|`xWkYMi})}%R|cGRIw{-Y3<6_L-D5~z3)Af?UJx}m(5LA?j)`fGQq&k9j4ay z*{gv_zVUSn#}!bEih3sIn{>CY%%A%Rjt_uJD?S z#Qf&g7>eAFTRKm!0)b^6T{8~=Fh!p*MSsI+%N9K;j^TgS)66(9M*_>Ia@=TH*$x6g zW6L@cx)?BAha$A~){OoBvCZnbY)mA z6V@hwaLx&B97)cJd$@w)IWILxfew5ba#*TWQ+{11*G}nm!^y%;_DMtf%fmSxd!?9R zW!+S8*{itu;Cl5US<1Z$1DQ3i8{-N`Cnm>Pr6!#o=mp%W@&XCPs5_Sy5)KCqLztWZ z>OPI-w_C!wi%9~WMHoC;;`wO?@%w7OUNqi-UtSQil5Bb3YxjeUtlh@)2Po8q8b+<* z*f}ywqiMgoQ6W_5i9%s)8mKdpv3&W*=1Bjinsd8_cI9 z1#{QwP@(jPjO6SqB{|822+dKd3d*4DxGV)kLy)bC2%W!%LAJ)uc*j#p<_xCC4|4?C zO$2qU*dBJ7UF8jVE@iV1!u0`!9{4Jz-12VC@;H5v^07@3nA!4Loq{{0(J@}N*iJv z=zuasy)Z*QVJXN#v3Z(vzd&AtR08L9 z?V3JuhE(^AdFimdO%1P;DhI zYm#q=Rs&AINGim|B*H~SRjC1xG_Mx?wVf><;BU z3A^e@*AKv_M2%}(C*d@=QC_w+F9Ym-n6Mg}uMUOizg1u)#siYs7yWlK4k(bhnAMKC zwOC2;qf&U|rYwyPy&hzg6xLJ}0u?SVI4yg2KUziOzGcAt2vQ;OAG44uQ8HW>O>Em5>2KQ|*7Ztg2p8CF0N20a+vnc;UXB&EL} z+TuboY)GG%Fyj(p8x`jm!^E#*K4R0zi<}$sYWEWIH_OoQ?kd*Ekg$~Zuu6NjF@Y4MbUEoDjooK&}c^5tB-C#7L*keXI96| z`yn_w`->uCl%U|t0p8X zp|2Gu>h~l$5jWSW+@|gz9czlYS8qo=Y)*KS6_63$f^F!^Xc$FyGiLgisL~rmzl?wk zYHV<>1tHl?WwX5vc`Kr#ZQ5{ynUN5dU9zm}Yf3ve2ju<*-x_ipnjC;k=sb-tw- zmOia3CMKfxOWOI`Bn2GFnZsLq^7zaQ5KB@n&nqDVKA^9)3YQ6+r&Z)l&A!t%)0ovc z&!YbMU0p7d(pRt>18$7X(oXJ3wv`*Z;Dgf?TFggrT~uKev}9UzvOxCeSAtY#H!dt@ z0LOhWhcv}>btV@?{1dN%=?sJ`YcpzvarEa(VO|#a*P6u6*$JY*W=QH7R=SDV_)&l( z%EvkrjAPMOlDkO&7-ILqV$!Cq)tO@A`FbcnTL|%JXJcgRlf-2CmRwhUrl(evP0`jy z0&qzB+}N{fx(JLX&~eoY1|>-JVx7(7YH8z0zM-fYZ3|ZqcUDUtC7?Cd4Npg}`Mnv? zob>dNz#oZPs_!jlbJNS|`inKrsk? zh*S88gb93oo`{U8!@xMzE_Fl%40A~ejWH4lz%+QB5*%ZE-F>Q9;|u`F2nn(;Zpu{6 zaR36NVuto;1e@-I=$wAa_Z@tLn5K>xkrDqlccw+j>q=nxtR2>IU|u8f_W%xz5n+Go|E zvhMJk6RmTzNUwR@1iH~)W|!)Sta{_y7b_O$XP{bdAnRL>IJjK7P14oyktCCpvs3iz zr^&8P$NnKXh75R@9QZ^7ei(*;86Z(A8fx~NnWZADSAAB-q_A*CDm{K~KWy*T_p@Do zHa$9vRA3LJFZ=sm%?&EQ;(t&zb^mCIfdLD!Ea9qoEa5+Y@{IwUZrXeqWrjG(H z#C*Co{8YV2a;Ei@i>`sy!8%ilddX5l_+gjsmdjo<{uiw!LN7Zq1rF8$QSgeQaXZJm zZU=Gmovh0Rdd=?m$ywjlPlpFZ)hdj`fGolm~ei(*TDRxWlXF#ni~7y z1O4fY=W%!*idYPfoZv6db{XIdb8V3^X=rcG}flz?=1H%K?gr5i7e zgh*XN@KRnHBn7_v{GR{!WW8DI&a69Ye!nyO%Pm%B!tJpnc4M=g{AwJU7;%VsgL8RL5>YF>o$_Q)e4#2n_*{i1QnSVuNws#Q?J3W2 z{a}uNFopWc{Gi6!0%k9_#gP#_jR9m*KDsP82r~kjd6gd|NRq}|sr}Nv;_z8$@>NwI z)6jf0dn4c7(eoEjt>CXhc@w2n4U|jteF7@|5QZeg5f4{%H16P6e!l3DwMn~QP6+v8 zllHz_APWnxpA0Bu=UaERVkI$%hgw8K?6N$&cE$QNTW+X<5~5jHUiJ+%ol_~8+D?yE za%2nhohy#K;99xAl8!+8gTJ|AkgAZ5xfndfZ90h}2ZngMTd@}jcde<&WWkcIZGR`HR>Ua!$VvSi<(2JkPG9GNvt5Q#G*yte_3 zU>MoqPUM`bWDEM<#MTbvH2Odb>C!H5j|N*aBLg-!R$+09fKvqS>+Q`V&S>@EXr*;U zi;{DgX-Pb&>){>#KDSI{>IWw^Ir-dJJn}5z=H{x^6l=2Bx!!Lh~bq^;v=Q%NRevBaiL`71;Rdt znz+8;e75|kmv|mOaUd;f-dDW1)kY=W~UH|+!d7~SGsZ|TcX;Z zwJg*#vBxbeB5^PJ`A0v%QOtQCNXg-GPK-{+H!l})?x1X|!>e}=?Kn@OS_WjE+&!{L zIg=L&dqVxeu}Q=^hs)cGn<4l}p4m%!5% z3k#)$me~Go8|-+3^k}=l}rz?;f*qcJg>>YUW`1S1GBLv_7XL9?HSP zw^1zVIWHWNfrNN$gJDf$*c8410?K+~aN@pX+sw_U#}4s4`}GPPXRvx<`|RTka6zbK z9y0O>w4(QuS7!HTw^Vv9Z#Wv6p0TmD9`=&EaP%Ya7W+XSKf3N33o~bpy~33XB3R4B z+gxtu*86P5(yE=-mRrSy;pbcJx$I#`6oWQU$z;|aONu^HFO=>E;p32CI<}bT~W!N)JMO%&3r zICi4qVzSEN7OML`u}!tCzed_0uW;50nk{QEd>1OZ8?5g%Sc<8c&sS94Vqw=sfO&e` znQ&$FPRtAL$*GJLMC%B??CX!EJycJhF08uR3IpLMtpm`~C>IZk{(Ns?f36e@zeOg#>6yL*i#p9NLpRpG zYj2?GO8o%(Uh_bGjU>k2%x#tvRWtUEF)8a}6pOms7D#!C5oL((>WN>EvBuFCXgD?8 zO@yn`?YZ8jhBp??S}FGV&_2*N_zpBj{uTZjn_*UNAIQ34KVN6qie86G;*BcbI$77- zyoruyz?$018BW{t3;{CMhG$7lc#;^ah#icQG)G@z*S5^zjeA3UR2aTAw=#_iM#Us7 z@!W?8xZhDHKI;Bz$3Y210|N(gljYg7aCAg`Q?E2H$IChuNf*M49Y=1de0U_o`{5DY zXqlI^MSp&H!7HagpGf$OqeyA$$ACmGmlW;`-7mWfUZkg5*FS!YW!u_TW(go28P(12 zYt3{puTM3XJZHZm{NZ0Uqo9xg4-!C1@)(PCTaiyrElpGXu)6C(4rR*H3oFf2{mdo| zrO+Z!ek${srC!e{iz1Xq8guE;rWG{81r#�=y>1S`0L5K0gMjR`kA)NwYZ< zr`svIfnZFOBFDFslCR9-=DOcreFLXY_EpJo8$G?={PJz6j{4RBu?Owe75{GQhWpO{ zn)MQT^~O2R4+t1e?p+JG-fJ=LF$ly}_z11bzdhbqggT4gpN2C(Z^c3mS6Bx4Mg%|J z0qm)%vKcoqk~cCIB9Lq_cnmyp-;qJ$3i=UJ)pwvo`|f!l_6(Iv{)m}=L@Td%IV%v_ z7%4xL7S_7yLv9wWF;iH*0BA`eRRAmhJce&uU_e`V=hCBA?Zg)1lPtMK&b3?&rBhCI zqb3dJntr6W9}{OC=4 z$(3!#wvq&Nm9lV5tSFPo==BXv04K~aSR@~K8tx#0d`#DrgTyy{`7~)3N{e*GE4S-? z(&9qzHX_>N6LlM1<=0ot4vbhu#8Tm&8lf@Xvg&+qDDVi{uyuRQO1E#6g3@@a7&ZNaDcA3B%2 zuS?6rlYBx~VeeUe=CK$Pv8n3YS8y`zGAdiuXt+Vkh!TC6isK|m~>EtXesS|jqoC&N189FMHrZ1ts6W8hyt;VO*| z&tCLm&7nFsgpNG|7)CpbdG)qaUemnPAJ0go>*JWbV8WGUh8P^yqZeqx)2J%)8{1}2 z5@aItJ_b8#zj%UE!T&`LHQwiyTZ}cf?Z-7K`oLTn8M@*k)AP?2dz%*9@{J2qy!MWw zb7bM`l4Sfk>E>~-JTf;c*sQSE7`RQCku&MzELa#YsdJy3afdY>1=&w zG(QUQW5Z5AvfTq6{mdURD*0AuQvdjVJ(B~?Eneid&hQfD&xtQDP;9;oHsK+|tXBfq z(cIJB!`V^S(Sh66!_x8Bv{w_;fZoqT2)OWnK9V$2Di%TbSp%0DuMoXf;d%)Y?pm)})lBKQ(nICZ$GOZE(%%45n(g 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 5%)\n", + " if tot_i[j] > 0 and iv[j]/tot_i[j] >= 0.05:\n", + " ax.text(xpos[j] - gap/2, bot_i[j] + iv[j]/2,\n", + " f'{iv[j]/tot_i[j]*100:.0f}%',\n", + " ha='center', va='center', fontsize=7, color='black')\n", + " if tot_d[j] > 0 and dv[j]/tot_d[j] >= 0.05:\n", + " ax.text(xpos[j] + gap/2, bot_d[j] + dv[j]/2,\n", + " f'{dv[j]/tot_d[j]*100:.0f}%',\n", + " ha='center', va='center', fontsize=7, color='black')\n", + " bot_i += iv; bot_d += dv\n", + " ymax = max(bot_i.max() if len(bot_i) else 0, bot_d.max() if len(bot_d) else 0, 1)\n", + " # Totales arriba, rotados 90 grados para que no se traslapen\n", + " for j, x in enumerate(xpos):\n", + " if tot_i[j] > 0:\n", + " ax.text(x - gap/2, bot_i[j] + ymax*0.005,\n", + " f'{tot_i[j]:,.0f}',\n", + " ha='center', va='bottom', fontsize=8, color='#1976D2',\n", + " fontweight='bold', rotation=90)\n", + " if tot_d[j] > 0:\n", + " ax.text(x + gap/2, bot_d[j] + ymax*0.005,\n", + " f'{tot_d[j]:,.0f}',\n", + " ha='center', va='bottom', fontsize=8, color='#8E24AA',\n", + " fontweight='bold', rotation=90)\n", + " # XTicks: solo el anio, centrado entre las dos barras\n", + " ax.set_xticks(xpos)\n", + " ax.set_xticklabels(anios, fontsize=10, fontweight='bold')\n", + " # Sub-etiquetas IMPO/DESC abajo de cada barra (mas separadas del eje X)\n", + " yoff = -ymax * 0.045\n", + " for j, x in enumerate(xpos):\n", + " ax.text(x - gap/2, yoff, 'IMPO', ha='center', va='top',\n", + " fontsize=7, color='#1976D2', fontweight='bold')\n", + " ax.text(x + gap/2, yoff, 'DESC', ha='center', va='top',\n", + " fontsize=7, color='#8E24AA', fontweight='bold')\n", + " ax.set_xlabel('\\nAnio (IMPO = importado solido, DESC = descargado con rayas)', fontsize=10)\n", + " ax.set_ylabel(ylab)\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " # Dejar espacio extra arriba para los totales rotados\n", + " ax.set_ylim(top=ymax * 1.25)\n", + " max_leg = 30\n", + " handles, labels = ax.get_legend_handles_labels()\n", + " if len(labels) > max_leg:\n", + " handles = handles[:max_leg]; labels = labels[:max_leg] + [f'... (+{len(fracs)-max_leg})']\n", + " handles.append(plt.Rectangle((0,0),1,1, color='lightgray'))\n", + " ax.legend(handles, labels, title='Fraccion', bbox_to_anchor=(1.02, 1),\n", + " loc='upper left', fontsize=7, ncol=1)\n", + " ax.grid(axis='y', linestyle=':', alpha=0.4)\n", + " plt.tight_layout()\n", + " return fig" ] }, { @@ -2121,39 +2257,194 @@ " return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", "\n", "\n", - "def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n", + "def cargar_saldos_vencidos_por_fraccion(fecha_corte=None, fraccion_digitos=4, top_n=30):\n", + " \"\"\"Saldos vencidos agrupados por LEFT(FRACCIONIMPO, N). Top N fracciones.\"\"\"\n", + " if fecha_corte is None:\n", + " fecha_corte = _dt_sv.date.today().isoformat()\n", + " fdig = int(fraccion_digitos)\n", + " sql = f\"\"\"\n", + " SELECT LEFT(FRACCIONIMPO, {fdig}) AS FRACCION,\n", + " COUNT(*) AS LOTES,\n", + " COUNT(DISTINCT FACTURAIMPO) AS FACTURAS,\n", + " SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOMN,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VMN,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOME,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VME,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(PESONETO,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_PESONETO\n", + " FROM SSaldoTem\n", + " WHERE FECHAVENC_ISO IS NOT NULL\n", + " AND FECHAVENC_ISO < ?\n", + " AND FRACCIONIMPO IS NOT NULL\n", + " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", + " GROUP BY LEFT(FRACCIONIMPO, {fdig})\n", + " ORDER BY SALDO_VMN DESC\n", + " \"\"\"\n", + " df = pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", + " if int(top_n) and len(df) > int(top_n):\n", + " df = df.head(int(top_n))\n", + " return df\n", + "\n", + "\n", + "def cargar_saldos_vencidos_por_unimed(fecha_corte=None):\n", + " \"\"\"Saldos vencidos agrupados por UMEXITENCIA.\"\"\"\n", + " if fecha_corte is None:\n", + " fecha_corte = _dt_sv.date.today().isoformat()\n", + " sql = \"\"\"\n", + " SELECT ISNULL(NULLIF(LTRIM(RTRIM(UMEXITENCIA)),''),'(sin UM)') AS UMEXITENCIA,\n", + " COUNT(*) AS LOTES,\n", + " COUNT(DISTINCT FACTURAIMPO) AS FACTURAS,\n", + " SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOMN,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VMN,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOME,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VME,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(PESONETO,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_PESONETO\n", + " FROM SSaldoTem\n", + " WHERE FECHAVENC_ISO IS NOT NULL\n", + " AND FECHAVENC_ISO < ?\n", + " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", + " GROUP BY ISNULL(NULLIF(LTRIM(RTRIM(UMEXITENCIA)),''),'(sin UM)')\n", + " ORDER BY SALDO_VMN DESC\n", + " \"\"\"\n", + " return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", + "\n", + "\n", + "def _fmt_sv_df(df):\n", + " \"\"\"Devuelve un Styler con formato de dinero y cantidades.\"\"\"\n", + " if df is None or df.empty: return df\n", + " fmt = {}\n", + " if 'SALDO_VMN' in df.columns: fmt['SALDO_VMN'] = '${:,.2f}'\n", + " if 'SALDO_VME' in df.columns: fmt['SALDO_VME'] = 'US${:,.2f}'\n", + " if 'SALDO_CANT' in df.columns: fmt['SALDO_CANT'] = '{:,.2f}'\n", + " if 'SALDO_PESONETO' in df.columns: fmt['SALDO_PESONETO'] = '{:,.2f}'\n", + " if 'LOTES' in df.columns: fmt['LOTES'] = '{:,}'\n", + " if 'FACTURAS' in df.columns: fmt['FACTURAS'] = '{:,}'\n", + " try:\n", + " return df.style.format(fmt).set_properties(**{'text-align':'right'})\n", + " except Exception:\n", + " return df\n", + "\n", + "\n", + "def _fmt_eje_y(metric, ax):\n", + " \"\"\"Aplica formato $ o numerico al eje Y segun la metrica.\"\"\"\n", " import matplotlib.ticker as _mtick\n", + " if metric in ('SALDO_VMN',):\n", + " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n", + " elif metric in ('SALDO_VME',):\n", + " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'US${v:,.0f}'))\n", + " else:\n", + " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + "\n", + "\n", + "def _fmt_etq_metric(metric, value):\n", + " if metric == 'SALDO_VMN': return f'${value:,.0f}'\n", + " if metric == 'SALDO_VME': return f'US${value:,.0f}'\n", + " return f'{value:,.0f}'\n", + "\n", + "\n", + "_TITULOS_METRIC_SV = {\n", + " 'SALDO_VMN': 'Valor MN (pesos mexicanos)',\n", + " 'SALDO_VME': 'Valor ME (dolares)',\n", + " 'SALDO_CANT': 'Cantidad disponible',\n", + " 'SALDO_PESONETO': 'Peso neto',\n", + " 'LOTES': 'Cantidad de lotes',\n", + " 'FACTURAS': 'Cantidad de facturas',\n", + "}\n", + "\n", + "\n", + "def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n", + " plt.close('all')\n", " if df is None or df.empty:\n", - " print('Sin datos para graficar.')\n", - " return\n", - " fig, ax = plt.subplots(figsize=(10, 5))\n", + " print('Sin datos para graficar.'); return None\n", " col_x = 'ANIO_FACTURA' if 'ANIO_FACTURA' in df.columns else 'ANIO_VENC'\n", + " fig, ax = plt.subplots(figsize=(11, 5.5))\n", " x = df[col_x].astype(int).astype(str).tolist()\n", " y = df[metric].astype(float).tolist()\n", " bars = ax.bar(x, y, color='#1565C0', edgecolor='#0D47A1')\n", " for i, b in enumerate(bars):\n", " lotes = int(df.iloc[i]['LOTES'])\n", " ax.text(b.get_x() + b.get_width()/2, b.get_height(),\n", - " f'{lotes:,} lotes', ha='center', va='bottom', fontsize=9, color='#333')\n", - " titulos = {\n", - " 'SALDO_VMN': 'Saldos vencidos por anio - Valor MN (pesos)',\n", - " 'SALDO_VME': 'Saldos vencidos por anio - Valor ME (dolares)',\n", - " 'SALDO_CANT': 'Saldos vencidos por anio - Cantidad disponible',\n", - " 'LOTES': 'Saldos vencidos por anio - Cantidad de lotes',\n", - " }\n", - " ax.set_title(titulos.get(metric, f'Saldos vencidos por anio - {metric}'),\n", + " f'{lotes:,} lotes\\n{_fmt_etq_metric(metric, b.get_height())}',\n", + " ha='center', va='bottom', fontsize=8, color='#333')\n", + " titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n", + " ax.set_title(f'Saldos vencidos por anio - {titulo_metric}',\n", " fontsize=13, fontweight='bold', color='#0D47A1')\n", - " ax.set_xlabel('Anio')\n", - " ax.set_ylabel(metric)\n", - " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v, _: f'{v:,.0f}'))\n", + " ax.set_xlabel('Anio'); ax.set_ylabel(titulo_metric)\n", + " _fmt_eje_y(metric, ax)\n", " ax.grid(axis='y', linestyle='--', alpha=0.5)\n", + " ax.set_ylim(top=max(y) * 1.18 if y else 1)\n", " plt.tight_layout()\n", " plt.show()\n", - " display(df.assign(\n", - " SALDO_CANT=df['SALDO_CANT'].round(2),\n", - " SALDO_VMN=df['SALDO_VMN'].round(2),\n", - " SALDO_VME=df['SALDO_VME'].round(2),\n", - " ))" + " display(_fmt_sv_df(df))\n", + " return fig\n", + "\n", + "\n", + "def graficar_saldos_vencidos_por_fraccion(df, metric='SALDO_VMN'):\n", + " plt.close('all')\n", + " if df is None or df.empty:\n", + " print('Sin datos para graficar.'); return None\n", + " fig, ax = plt.subplots(figsize=(max(11, len(df)*0.6), 6.5))\n", + " x = df['FRACCION'].astype(str).tolist()\n", + " y = df[metric].astype(float).tolist()\n", + " bars = ax.bar(x, y, color='#6A1B9A', edgecolor='#4A148C')\n", + " for i, b in enumerate(bars):\n", + " lotes = int(df.iloc[i]['LOTES'])\n", + " ax.text(b.get_x() + b.get_width()/2, b.get_height(),\n", + " f'{lotes:,}\\n{_fmt_etq_metric(metric, b.get_height())}',\n", + " ha='center', va='bottom', fontsize=7, color='#333', rotation=90)\n", + " titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n", + " ax.set_title(f'Saldos vencidos por fraccion - {titulo_metric}',\n", + " fontsize=13, fontweight='bold', color='#4A148C')\n", + " ax.set_xlabel('Fraccion arancelaria'); ax.set_ylabel(titulo_metric)\n", + " _fmt_eje_y(metric, ax)\n", + " ax.grid(axis='y', linestyle='--', alpha=0.5)\n", + " ax.set_ylim(top=max(y) * 1.25 if y else 1)\n", + " plt.xticks(rotation=45, ha='right', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " display(_fmt_sv_df(df))\n", + " return fig\n", + "\n", + "\n", + "def graficar_saldos_vencidos_por_unimed(df, metric='SALDO_VMN'):\n", + " plt.close('all')\n", + " if df is None or df.empty:\n", + " print('Sin datos para graficar.'); return None\n", + " fig, ax = plt.subplots(figsize=(max(11, len(df)*0.7), 5.5))\n", + " x = df['UMEXITENCIA'].astype(str).tolist()\n", + " y = df[metric].astype(float).tolist()\n", + " bars = ax.bar(x, y, color='#00838F', edgecolor='#006064')\n", + " for i, b in enumerate(bars):\n", + " lotes = int(df.iloc[i]['LOTES'])\n", + " ax.text(b.get_x() + b.get_width()/2, b.get_height(),\n", + " f'{lotes:,} lotes\\n{_fmt_etq_metric(metric, b.get_height())}',\n", + " ha='center', va='bottom', fontsize=8, color='#333')\n", + " titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n", + " ax.set_title(f'Saldos vencidos por unidad de medida - {titulo_metric}',\n", + " fontsize=13, fontweight='bold', color='#006064')\n", + " ax.set_xlabel('Unidad de medida'); ax.set_ylabel(titulo_metric)\n", + " _fmt_eje_y(metric, ax)\n", + " ax.grid(axis='y', linestyle='--', alpha=0.5)\n", + " ax.set_ylim(top=max(y) * 1.18 if y else 1)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " display(_fmt_sv_df(df))\n", + " return fig" ] }, { @@ -4205,13 +4496,22 @@ "\n", "# ===== Saldos Vencidos - Grafica por anio =====\n", "w_sv_metric = W.Dropdown(\n", - " options=[('Valor MN','SALDO_VMN'),('Valor ME','SALDO_VME'),\n", - " ('Cantidad','SALDO_CANT'),('Lotes','LOTES')],\n", - " value='SALDO_VMN', description='Metrica:', layout={'width':'260px'})\n", + " options=[('Valor MN ($)','SALDO_VMN'),('Valor ME (US$)','SALDO_VME'),\n", + " ('Cantidad disponible','SALDO_CANT'),('Peso neto','SALDO_PESONETO'),\n", + " ('Lotes','LOTES')],\n", + " value='SALDO_VMN', description='Metrica:', layout={'width':'280px'})\n", "w_sv_eje = W.Dropdown(\n", " options=[('Anio Vencimiento','VENCIMIENTO'),('Anio Factura','FACTURA')],\n", " value='VENCIMIENTO', description='Eje X:', layout={'width':'260px'})\n", - "btn_sv_grafica = W.Button(description='Ver grafica saldos vencidos por anio',\n", + "w_sv_agrup = W.Dropdown(\n", + " options=[('Por Anio','ANIO'),('Por Fraccion','FRACCION'),('Por Unidad de medida','UNIMED')],\n", + " value='ANIO', description='Agrupar:', layout={'width':'260px'})\n", + "w_sv_fdig = W.RadioButtons(\n", + " options=[('4 digitos', 4), ('6 digitos', 6), ('8 digitos', 8)],\n", + " value=4, description='Frac. digitos:', layout={'width':'260px'})\n", + "w_sv_topn = W.IntSlider(value=30, min=5, max=100, step=1,\n", + " description='Top N:', layout={'width':'360px'})\n", + "btn_sv_grafica = W.Button(description='Ver grafica de saldos vencidos',\n", " button_style='primary', icon='bar-chart', layout={'width':'320px'})\n", "out_sv_grafica = W.Output(layout=OUT_STYLE)\n", "\n", @@ -4223,11 +4523,26 @@ " import datetime as _dt\n", " hoy = _dt.date.today().isoformat()\n", " print(f'Vencidos a {hoy} (FECHAVENC_ISO < hoy y SALDO_DISPONIBLE > 0)')\n", - " df = cargar_saldos_vencidos_por_anio(fecha_corte=hoy, eje=w_sv_eje.value)\n", - " if df.empty:\n", - " print('No hay saldos vencidos.'); return\n", - " print(f'Anios con saldos vencidos: {len(df)}')\n", - " graficar_saldos_vencidos_por_anio(df, metric=w_sv_metric.value)\n", + " try:\n", + " if w_sv_agrup.value == 'FRACCION':\n", + " df = cargar_saldos_vencidos_por_fraccion(\n", + " fecha_corte=hoy, fraccion_digitos=int(w_sv_fdig.value),\n", + " top_n=int(w_sv_topn.value))\n", + " if df.empty: print('No hay saldos vencidos.'); return\n", + " print(f'Fracciones con saldos vencidos: {len(df)} (top {int(w_sv_topn.value)})')\n", + " graficar_saldos_vencidos_por_fraccion(df, metric=w_sv_metric.value)\n", + " elif w_sv_agrup.value == 'UNIMED':\n", + " df = cargar_saldos_vencidos_por_unimed(fecha_corte=hoy)\n", + " if df.empty: print('No hay saldos vencidos.'); return\n", + " print(f'Unidades de medida con saldos vencidos: {len(df)}')\n", + " graficar_saldos_vencidos_por_unimed(df, metric=w_sv_metric.value)\n", + " else:\n", + " df = cargar_saldos_vencidos_por_anio(fecha_corte=hoy, eje=w_sv_eje.value)\n", + " if df.empty: print('No hay saldos vencidos.'); return\n", + " print(f'Anios con saldos vencidos: {len(df)}')\n", + " graficar_saldos_vencidos_por_anio(df, metric=w_sv_metric.value)\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", "btn_sv_grafica.on_click(_on_sv_grafica)\n", "\n", "tab_sv.children = tuple(list(tab_sv.children) + [\n", @@ -4236,7 +4551,10 @@ " 'Resumen global de saldos en SSaldoTem cuyo FECHAVENC_ISO < hoy y '\n", " 'SALDO_DISPONIBLE > 0, agrupados por anio de vencimiento. '\n", " 'No depende del rango de fechas de arriba.

'),\n", - " W.HBox([w_sv_metric, w_sv_eje, btn_sv_grafica]),\n", + " W.HBox([w_sv_agrup, w_sv_metric]),\n", + " W.HBox([w_sv_eje, w_sv_fdig, w_sv_topn]),\n", + " W.HTML('Eje X aplica solo a \"Por Anio\". Fraccion digitos y Top N aplican solo a \"Por Fraccion\".'),\n", + " W.HBox([btn_sv_grafica]),\n", " out_sv_grafica,\n", "])\n", "\n", @@ -5400,6 +5718,57 @@ "])\n", "\n", "\n", + "\n", + "\n", + "# ===== Saldo IMPO vs Descargas por fraccion =====\n", + "w_frac_topn = W.IntSlider(value=30, min=3, max=100, step=1, description='Top fracciones:',\n", + " layout={'width':'380px'})\n", + "w_frac_dig = W.RadioButtons(options=[('4 digitos (Capitulo+Partida)', 4),\n", + " ('6 digitos (Subpartida)', 6),\n", + " ('8 digitos (Completa)', 8)],\n", + " value=4, description='Granularidad:', layout={'width':'380px'})\n", + "w_frac_met = W.Dropdown(options=[('Peso neto','peso'),('Cantidad','cantidad'),('Valor MN','valor_mn')],\n", + " value='peso', description='Metrica:', layout={'width':'260px'})\n", + "btn_frac = W.Button(description='Calcular saldos vs descargas por fraccion',\n", + " button_style='primary', icon='bar-chart', layout={'width':'380px'})\n", + "bar_frac = _mkbar('Por fraccion')\n", + "out_frac_log = W.Output(layout=OUT_STYLE)\n", + "out_frac_tabla = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_frac(_):\n", + " with out_frac_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " df = calcular_saldos_descargas_por_anio_fraccion(\n", + " top_n=int(w_frac_topn.value),\n", + " fraccion_digitos=int(w_frac_dig.value),\n", + " progress=bar_frac)\n", + " _state['frac_df'] = df\n", + " if df.empty:\n", + " print('Sin datos.'); return\n", + " print(f'Filas: {len(df):,} | Anios: {df[\"ANIO\"].nunique()} | Fracciones: {df[\"FRACCION\"].nunique()}')\n", + " fig = graficar_saldos_descargas_anio_fraccion(df, metric=w_frac_met.value)\n", + " if fig is not None: display(fig); plt.close(fig)\n", + " with out_frac_tabla:\n", + " clear_output()\n", + " if 'frac_df' in _state and not _state['frac_df'].empty:\n", + " display(HTML('

Tabla detalle por anio y fraccion

'))\n", + " display(_state['frac_df'])\n", + "btn_frac.on_click(_on_frac)\n", + "\n", + "tab_an.children = tuple(list(tab_an.children) + [\n", + " W.HTML('

Saldo IMPO disponible vs Descargas por anio y fraccion

'\n", + " '

Cruza SSaldoTem (saldo IMPO disponible) y '\n", + " 'SDescargaT (descargas) agrupando por anio y fraccion arancelaria. '\n", + " 'Muestra solo las fracciones con mas actividad (Top N).

'),\n", + " W.HBox([w_frac_topn]),\n", + " W.HBox([w_frac_dig]),\n", + " W.HBox([w_frac_met, btn_frac]),\n", + " bar_frac, out_frac_log, out_frac_tabla,\n", + "])\n", + "\n", + "\n", "tabs = W.Tab(children=[tab_conn, tab_desc, tab_an, tab_nlp, tab_kg, tab_ctm, tab_sv, tab_datastage, tab_estructuras, tab_valores])\n", "tabs.set_title(0, 'Conexion')\n", "tabs.set_title(1, 'Descargas')\n",