f}9?VE8*gF$a_zC9siRcWm3Y}>6*<@FLK(SzSAQ0`J5h*`I0}P;;_@k2M=|7!VR3yZ!H0)UnUG6?E|?u-J~vF-z_*@d?s{$
zs(C}c`oEex3ADf*ImC1IiN36RlG531MV)Nz3bI3BObY4X!o!?&Knf)V?+bMBo!l)$v4YPtcsYAL)N#oao6XKBklIHH7rY2ZT
zR#$z}7mvQv;~-xpW}`*ch`EI=(PSYLZ(n?z8wCz&pR26;%?;p
z{$mSGkXp+CY;#Hf+R8P)oy-1GcdO|I3^F^buKU(-n#d?v7e5V;>9xg~dkM6?+|fb+
z5qUP?_gffx-MNy@B$`P`#E&dCT&`y}z(aSXF>vj
zdjE0e-zSsCquE#O{_qpC7z|KvE`j81oLS#qhUWu?FqFt}^P?UIpX@?p`iJg4MFWq(
zcJu)SFC5*H9!4w``-j_eHl2jQRM{sK)0VXx|4D+KQ;Z2ISr+0RpvpPxWO&>$z?`y=D4%CPbb1u
z*V;;Q2KiyAQ$z7PYSyY|Iq@JTiwgpQ$^Qt9`M1D?eG0nr5v&LV#r^|A`Kc>-fV(Jv3}8LP+g{~U-p}ig!hfZ3N{i+h{`>=^
z7+Ldi7d4BVY!s*?^0u5j-ElS}D{H*$5dT*oCX*mz_2EN&H@!TF;Ow`VtZ%nPHQ$jY
z_&GWifxMp3Anxsc4astly2G9Q7~1-A5khUp#0rDC0mBhwbf$=J>4%P{vuTzV!So!^
zx279^xo-_bQ2=}z=9i?>DOg+-2rBb8*DA>W7M%+NpMhWZ3ZGd3>i3V$|0PcZ8v;RV
z|CkW|z3@2yggOuS+gWf;E88uXfX`VnS7C+S&~>7tk`12fa*6t`|uI|oGGs==UqpC
zuuhn@t!=FC2cfM_>~gp1*b6~i4uDD~{8ENAo_B}%Z&?DGlEg+`M~aXK`N9uD$x}z@
z8w+mtegeVzPJ~T<^dEJ
z27-zIUva((xx*Ix1>^CL$-hk`FpbB#6v2OksrcVv);>)>lMnIV<^ho;-9p;_zg)g#p>O`)}5;uIRqu_l#qSRw~NHKi%zZK
z0uMPT3d$AcDl-5}$BU{)p;tv4@u<=YDDDXa7yJXt`}e?t|0T9p92LC+p}QsCSQfGN
z;I{w{>CT<-y}UiaU}laWrGOtZGE;x41n%?)g7&|ueQ|9dqhIl<5_ri!CjVXR%Rfqf
z!Yuq>VgAHJA~g^K@{f(+r`q@d-k|HGcA>f{Hpqjf`h$-pNm?!!m%uoD5_8-%i%<9!
z_qauXdK+g#DT~H=&gW-c<{7>4SAw3Gt{{-BzZ?bA32d=#)U{bM{kp{>A{a49)3XN#
zReN+)#j>w_CyUIu$dcA3f%(R;t9A!+GGZ?Z2CH3{=t3ko^s_XjGQH;^O#*^!E+}E*
z`~sGR9AcH_^_zg?!WVB?Ah9-Gf>0nXL|3xJJBwPDqpMvSL@8hW^m&VxL0Zx%&Qw{c
ztk9O9?%j3-kv0(>0MtDm_b!R2{MkEDS`#RpvE=oEl5x&%>^K<`Bo4bNE*f9#7wvX%
zoqQKbzm`=?rZVSyU>2Q}9t&y^v@2ofJDTW_kSmiYRnV@`3*kluKKKOJ=x;j6rz5oX*<(&BQ;
zT$~d;oR?WR{BQDp0Q{uFwe_>fdX&|8;Vgy$5!wd@Avp`&=$)j`i)-@?L&w$cytB%*u
zuv(_>?Xay`w>2S_#_$ct>Rt8F!qdEr3cd0$(v@nAV2MIp0`KVnB+q_emPMYBmMgo^
z4gdJ3ur#j8a&Co)`o!f_&g1l>;UySysN5gyJ`bX_!bg24}GPYv$)J7ZItTq{77hb!aVk
z^NBDF7!W4kTp0+N;cnS#1u&nXF@zQoe^&bK40cb;(rsLUb**;WlVmW}Bk!+gIvDFY
z@#4OUZeZ0q6t^)Fitzo+CIqQW94QFk(54KxZvWQF$^gm*Tm~{N
z9zQ}NjLhigI5x5Lw~jMN*LTY0-=`k4n{^(!Gx+TV(9?c~%_Y)-Da^1}C~?LHomh}s
zmCqZlRgxjr0+%E}2^MavAw%atGM;Gq
z!88=mD)`}E6XP_LluUksRHc+SCC(`M^|NK?$_N@IG19?k4PQYOYZ4EAdmJ+InUfhf_nZO3}du1DpuhEK16z)3Fb+)-=
z*0F#}UynEWt-*~zQmtsR!^Y{FH^JwxDtUBC&Av;dRFr7p*m~$CQR`aS_})d@=*F8s
zeVBl#`l$3K}vxbPq()
zX%141wQ%@TY71d#wYHseH2>I6rzmvCb{vj}L_K9@&prF6iAZOSMnswsL0+9F+4>DBtfnKFUit(m0kg?T0z8{CjFw6fi0mE
zPS``va~AvzQx+74tlyJWEjX?AsqVe0hZRW>znEO1RQoW$kDuaB)K7VYAcf+E7H{nP
z+SFfE$m@3y9Jit_Lo>OcfwS=TQek5A97%gQr2@Wf#OM>mLsYa02mI#SR
z3AurFfh_l%v{YfJW(tX(beM{M#tVI_3l>H@rH!+T&+H?y*9{~*B+
z>MHq{w}S2#UI@&k+?^{be^5c4en7Esbsz-ypXaj7=N14oz}$uLh0d;*
zN7?wtdO$@JlQ@INiqO#LmCcD`P}?a`Gb;oY-3RFIcTe`S-C|E{cOG^QI0X2jw?>_F
zcfOyUXZmB?M?vBahhrS)jK&!Ee!t$}d>&$4O0>q3)ebCz95?J@N~UOw9bN$&0lL%Y
zOzPhz-rF;9lVL29?)5Gbu3-v
z!TX0M-aZESz0;iO6(37d>(}@)tK#xau7mOcy7rS7>p;Wb=KU=4Q>nk
zf+F6;K^7no^}A7XmD;|Z1cDUI*n%fsEA1DE0tal0gK5H(
z=~kQZJ8`Tt+Pdt!oVZ{6r#f6ewk$!!sWr}jU3G`^fIo`*$**Ke~c8)iXGVB|gggPQ_P#Z8HhqKi6
z2w(H0LmD9NcYt?bmgi0!wFOyfE%Q(RzwO$ZP7amRt
zL3_^!#9z7>Q{Nj3gO~%~_GpLEDQ`O>{3Jtf>{4DmPFtyneCLXh6dUa&
z%|)q{^B#u^igIhWLLYAFoua`rE62i|`M=a6QWZxWRz#7J80)a>=3sWj&Uw0w6{7iS
z2hVP<;Xixw+KFf)(S!s(q-qjy^I&=gz3GV2SnF7f2I-Y708Tq_3Iqf(i|TQS_k(JG
z5JPkzEX&zOR&1{QuwDFWlvvIQBY}t_DhWS;W=M&&f(zSbNk(K-7?~6&!@|B=K-(>p
zK`!iv%$oUd&E_e>gs;!5&OG~(1I|W9=_+nBa$E1`)y~dStI-ENfG0M#m&O8THqp8o
zMiM$A(BrhcrN{yyPo6+}yf^Q?As*`ek*GSF
z+r3IGNbR!-@alx_X{k!W=E4ZbLn^re8(*Sl^TgWW|{@>L*i72PdZe>gc`#
z<*#Y?D70s*niFLhqo2rvu1)8R8DbhMg5Y^xG)GNtQw?RhTW9`m
zVa$=(G~Q|n9-$n^z60|@MpV$!n!(jBW90`Qwey{21_k&KI{SU0!D+3;<3q+ZSA+y7
z0Xu=$iy(*4mg_H4Tn1+z+eWH{dAlIA(qkaBa~*`QJzXExx(RCiY6oTM42~)NJ5OJg
zP^ZhHTJW*Obu8Zy#=X@*u9o?C2xh}Bv!zgB-|3y5s|qe$UsuZ*ynMZRwC+0GVUHdd
z*1*PBk>btLGQ^2ST{`0MMi59(s$lWfQ|{!(7U$Y}LuMWpNYysBbG--_a~ShId2wZl
zgrigBQ+>OI(C5WkzAFWV`Qvc3M3+1@A-rL1Wv^_D9WHJ-qYeM46>kqO1V54;ADl53
zNWOPIqq=+g7h55+^=bb=iJ?m?+Z4#g$;}V;y01qRbbQ|jupK|D&0X#DnI;EZEN4ia
zHvsv`J!a*4nMR$O19&aSd%?w?-lye{X_d}cFw@o_!(t!X0=#p`Sn!GxlV~vKPB4fX
z#X`D)5PAR1UP@2^_@A@*2u(!!rJ3zgcFKSz!xin)^E?_gfXLpcj=qr%%H68JLt=U<
zKQ>?eaY);B4p0B^o!mt3>oaJgOh2B#td6{Vw2@3VZ_5dIf_BPD@i$0vV-EnNX7B6U
zaIaJR$Lag)if%V*X=SBR8;EZ=!OL1h0FmIDDF*8>>SFPigvOpFlo>VU$kNli0?kqx
zGaO1*h1gy0XRlN4IZP>1pWs6o^f@u!hR9@zq;I=pnsgS0-`bp6UA%CUoN>tStEk{=(}>c
zbB4o+^%xr&zq_E)Tt??ejoG7u|41ThsuU35YfdBnx>YPG9tbh@sqg1u1nuXU04~6v
z#GmfPS%LTJ>gA{36`aGa4=BMw=43
zJ;BdK6wFSm1%T#!)Ew4O2zu3o@9oOJjKzxiAJQ^ADEm%H9hv+r~Z(v9}Emjaol>>RhFVJIJ#J22!L0}gomd_Ud2puJc
z$5U=X*gzgbwEE%@>nBrbCYTkLQKqNqyTMyNfy3HPZW88oqnLC@GfNY_h?@a}gR2;X
zRJsuTYE=IO)JgpHLK->aqTtSq`WN=6<2dJq#E+Es0Jr&FJdXf=Eq4KV~gT+oF$<@pHL!V9)zkU9|GHU*^b!u#{
zIPBw?0t1LKPJc0V+(O1C4_@i)srxpIbIWfKu}(B8Rm2Y{JPF>Ti_m4Za<2$rh>?eR~SrRT=?+#sfZQ
zZ_2!f!ghwj_JVOj)1eWy)ZHDdO1yQ
z>*y88r4`fdrfjJiWB&A~ZkApb|IZx|mO~!9^LJRPMTa_)eh$(Bj~fM0H1^jU@C9f+
z5tsD(#Za$w*n6^;rK0_*?Y3~5%k5IDtIDkY$2NFmY`0(4fn{k052&{J!xlP8`lpHldz!l!cNCcWX?UOspx&(B)K>o~1XfUu@l!-=_)8UmGik`ZNoz$l)p!&7dM<^T%ME;uBsH4(Z;QMK$M;iFmC?
z?`B0lHh-?yFEM{a{XkHQ%^r!r6ylc?PJ-)IQV5&Obv$~->U(_hX$GPSozz~nAp>ii
zdKyN;`_}C0zDXywhtj=ajs>Xo=1@twA
z9!5SSt=B;NqKo~f?Et2Thg?Od0l)+6SeeDELyOhq^pMVpe6-HbvRYWkDMT43kwm9d
z63W3}PpoF?97$jg11Q&G-LPTf2YtXuE;mC-+~W?d!R8WhdToA~7)G^37i0FXBp-lE
z#2m{&B0^=vNM+x$UjXz-`Rj|vS4(PdeI-oyH#IVDL!-euTg0*Nf4`zZGqt(
zJo67Ph`N8X$cQGFjB9&`eC=z1-(v00kzVxX*Hv!g(loimE<%b>tNR%U!Epu9)-@7A
zWKm>$ygsT*>~hV)(3FR=bJOiXO9T?56OuPyw0B|XJyABFG^>ECP1fxXa3zDmMc$B+e
zl~O){zLO_^2w6A^JV-VuFrC!KUqefxIki01{omOaax&n%ZqYP-y{ZrspBg3V^9x3u
zQVGASbYDNsHp=;)o4PcAB---gKg=(Cn^1F7y(jgg*_~2+7=H80xdG(Pk)gf71|T|F
z=&RiFEJfrGVn_ODo?&dO8%yxtu7@BbEVFhIF(XPrW+ot^S~I8A9(vSvka1$Jej^6K
zwQewDSU?MB@_V|gnmb}Q=W=gFC-cf`Bt=le6xip+sU!Z(vfCP^)
zIuC!6ERlJoKSbXU>nRUz9<4r?M2}pGrBPApVe{ER;f}IT@(MHHrwma26RgcRdoe7f
zxyB*0n+-VmL*?@r93I}{TdhJA_3ng;F(Q{llVIyW
zlWu5C8yuS6nfhg}U&mOhF%UHTfTXp*2>7|XM!OV#L4y?&e0`F;Jo<~mV;(U^_(yCbBv;SdRYV3I?~5@x63jAO
zzUBnkf3$Re5ij%C*Px9<0*VjGuObJM)Kq46<@E^oO&oP&J497pC;Qyl{ZY@4UP
zwuiD78xPmmwWB*kq6TR&P+R%)jfd={A4__8{w~QKK
z0zAlC1xU7E@R{xij9&31_6%^9wzPhAwx&OY?REdPjY>6Y(ry5WkiWsagbB`IF}*0f
zU)cXG@fOnio@7*-K)J?v+4~)@JUE>$k>RO|(jW-}nXf)1PU&4fO31TY+|ALD$?e&afeg%49l8G}G#dT(nBBfu6s`jNjJL)f%42r>Av7yfasG3#JVL`%MZ@cmfgPJGw8D`Rcg=i0&Ch%iY93W6O*VXqp%Qh=6kFN4p-br
z*+29R5g)bN2y8nS=E`E&DFtkP8WMI*@~h@r)xf66$tbVMqD_6AezU0JS&vP99k)n2
zMN4|m-9C^R+ZL?KfQv+r?!Mm;p!htM-_b^X*wC(7jf#E^NGk|6|8?}1xqn@afnTyj
zCO(^JPgkUCpIyxtabLsyDNpJcv!zm{Pltf{4Xwu?p*}(V5Gv`Dnby9f}-_2Dr_}A)$$`_&?UPN$VAUTqhRN=bBNN9z1P8klHis
zbHMBsjd!-|7J;4xKBLyNpv>Dl<^=Dn<0ErR*?!F_b>5B)2_0U{G+Dkoy7>Wpm|$b}
zV*|;A9S>}N7C@NDmbibYpFU!C18G_V7J;gBTU+O=W4KlQ_(n?Lq?kY77*!
zL!1vzo$CRPktDD8TQ=vkrZ_l(7s)uJ6fGy-;&$X-y`;S&ni|UC3MMG}N$*wWcWf)x
zQ!s3-Yb^8=tYd4%n6xV=slUPaS5@Yr
z>zZqxJG53bWF6}c>E*~hSyQw9Py>q~0uM@7gbD*2uBYZU;zKaN36~?+yg`38%H8<(
z>pYP4nkQ6{B`)|RA(#dCytWF;+xhJo{SKc5B7dG)5~;o8gG%!lfQjJvOG6w1P~J4N
zl@j9PtQ`7SLDZ#F=H|_UcD;h~wBJpLGKP}HctIKxD|Tn$v4NyoD%RrQCj1Dd7#5j4ZOgZu!9vb3Eh8B4(iD
zRww*35$kcxJEtD~QC2b=z+jQy8jCNb06xH2k--kDGlk*qe%FY>aT?5V$fYiw1wMr!
z9>DZ9y$Nb#xf|*|
z$_K)=FEE4nk-j(Gh9RcU?A{)2la0g=Q`3!5RNkW}?xJ>sSj)IzK5HFup^fmT^B&x^
zHuU)XqZysLo*bOk@WbMt-)@%B94|Oz?tU+|ZG_I;
z!dUlg5y)xCQ=93^KxjQk01ywDso+=tYQTD>YL_>^p1G=`*F
z`DF@D<$KytnZ$~Zyx+oWX+g2sH{iP%qh432e*5=LE>>e3+UtV&70pX|BZOf-?X4aJ
zTG2ewqx|}Q4i<@f5vx~5%c%rt=4#=PAkU@-uYj9|
zRT#|}W{bVQB{QyB0$w871EW}$V0$|ky!!>#UgEe)T*X-aVZ
zHIqj+>eSO*4};Aczo^*<;eJg!sWLw#{;K#JP$cbe2S^~VL^klDU*RHEF
zVR?n_nSqSL=<*cmT}DQ0G^E+m+VN9{{w7Ouv_Vfk)qCw=kt9ck4^@cZ6ceu`uJfNz
zV>i;AaVmYSx}?@VIp}wbtvRe^H3ZUZM+I|KU?5ejB3y-*8IG_C!w@|=Q;lPsQ5Uq<
z(Fo^Zv{eBD8rA9*Ac|rZfhiceHzuG9|zIL?2BHOOIfNC%t3F7Ilh>77>nbNqUMecY8k~!X(1)
zd3KPg5RRs-Wc_LemBxv~yR^b|?1AUy#=xrLx%&Pv?{AaQ)Y!Gx+GryQe3T_R0@@xA
zPRTLSfn=;3qtZsd6@;Q*G-Zl(rXSY1g{W9Io6skQ#_s}r%4OD@b46ZcL7CMu7i_Oj
za!+fo54(YeOw_fUwB34zKn}E&A`KQ)3>zEAS@<4c9TBdYV?8cu$7}WdQux>`APrwn
z_|IE5t|9>F8tbi1sW)E$s9d}~RG{4&K5i1s<~f=6#zc796Jb#nA3`JK-@h_HMx}U6
z`ZPusd`3Z`dOYrzp!M+#tpgu9QuwNy$nvw|I0)k}nH@(N8j0i=+eaac4^T}vm4MKTMt}yvJ
zEt7nvZ?bq`Lh5M{cAv%I>WXta)XinWd}K1B2mW+S|b9nrwdR>5#wSXD!Qu`8PCiC{!G^xcL&Jf!d
zKQwDz+I>pxIt6JR{`_EpiqV^NC)AYiW0KcHcj)3sp9dLxZogR?YL)58G*L|`YnjKC
zG=(I{fjV2u8knr&EeL>;z8|QL^8U!jP=e
zS`Dz_1%xXtQZc6@6CvI!F~$$$m*$|Ot~oZGl++#eIef7JlQT8Stg%-j+V?Vc$;~N%
z^Uff~=Uh!ehZ)PcrV^DA`Ai9X)3G@Ei>>`EmfUNOi$tPE&oJb98&Y)W6d@T*GTSb8#d^L5aE;WL3iAclvKj
z>t9#eqO?*zYPvSax1XzCXIp9iI4!nZ80Ep0!Y)jva@$7w
z2-ms2>`igb&8BM1ku0gl?G#j3IXoEaXn&-5YwmZD-xvqxg~68NbwClP{k_px61|IM
z;UofR)SF&d%)i
zpMsf-TA?0&Q;&hH#F56#$5q?#_!DlUc?rBYIQq0m{x(>vXx_z8&;!j6mukxQ8w9pc
zY!_^a(Mp!l;4!Q
z%<EA2
zoLn3GYU^P&xn^osGtZ0(ogZ
zjs;+An+t%WQQxN#QgC@?+UOqr>r?99MvC2S3AJG{(D
zJl|^mjQSXZqy(o{A>n&LzQ#-OJ9*z0J_NzAQF^$^e9*rNBQAWlI#x~uxp_z2_p&pZA{Ly1vePZ*Mpe2t1i1i-FmCDqJ9%GU{8S80Nf$Y&JOID
z(g7Lmqz%^ApMeg1?B|ht$VHSU*s8}R+6I{8^(id<%zKdiT8ZyjERY||8UFn!uv+?y
zrX8vfx3LNt&5}F$j{d%~$bn~|^{uS`sQ%`dtO(~lcbEi}%AJIx+5DZE&UT&;iey69
zFI`(h)kYpd2nhnX5_39fckkC~QZe^Yzb}iGG8c#am57`y>UWt9u5}Ps(X+!RG!{|F
z77@$a#HI#|A76hH_ra4hKOm9=eK{-7tZZp$#K_@zBjy#i*K^QS#!Z8~vDqrG_=)tE
zK!)_bXUr;-`4VszwcTllNv2S8hG+wOC4JZO`1;3jc&_gj0dMXDGmU^q?1-RYnc1~i
z44)CjVV!q!E9khV>yef~-5}XplLgKy@ts6&Qh9k&^yW{U?331E^h~RuMoEAR(HG@8
z(oj^yy&v;2hU1B2cTa)z15_zEx8uPGu)w!c}|J28{vzW?gcepGfWTo2DLu6U7VdG-WSo&_(*R<@`
zHL{&y+G_NP!*cAVdyz^%ctquXc~mMKyFS#%s>-kjK^qK#(1Evj^;`H-ceH$FjLk3Kfy(xOc0w=NaOk6aQBmkc$GR%;5
z>-c`A@Tg@O$-@lP#ab{X$u;8a7#%z%u)cn;wH^TGjAgEUju&ti+bD*vLUgCP4V#%w
z=-;T^=Sj;2#Om4;rxTJvV|3I}x+w{V;e>^R0`bv=HL0T*3TCFEekLmIZWog!mu){b
z;EHxMnrLY@)?&-k9_zexdK_jZG5J
zE)+~H_P+Fe=UX1cfO`c6sSTzx>;X*x6sAGc;y;P55K);qWGAveM;~q23C!(Kl4nC+JE@H0Op@ZKRZ2|-cdj`*$H1_y5fjZ`GADHoi>`zB_=Q_dUW1j*_`+|*76HFFSS}L0
z@LP+Qq#$~*$GJ{^EMWUCZy0Y{;Y)$)P!A*#QEmt&<)mnqA8j81Ce`C&Xn;*?Z^5(=ZsGjwTUIhmtX5vOQ|%b^%A4qTCYkT
zib9GUwiC2J{noELcK(%i>G-;{w{q+td6D(W&ETaV7F-$DYZmkG~Je<>4)(JTi-j%#haHa)PZ;_
zDKe1-t1k|Hikb)JpgZB;U8Tga_U+4a1*vL0<4mWJKENQ!|Mn_^dp5E`y_}f$fh2-<
zDq35QX*7?57adS{z1c_IVB^nC`LaPmr9qurEb
z+VuFfj8QB0%Cge2+ZqEJbz^=i@8vQ^FI!s^MGIgEs4-h&MYujl44QXE*}G4iByL`T
z0>s{brhny`!idffw{`DoBK=B`ZQmSdAK4J*`9WGiH+{9}qVV3+EqTsg%6oq53xfMh
zEuxp#+4`HuPRnp8ruWjxMFCnUXt&*`irS)IFSY``MD)O4yInG>3%*43%!LRkPSQM#?f6jhSnzh~ET%(y$V305jK+3lJqT?6wIRPTXplv6L
zw;K?!o&R$m?Y2)YPJH4PQOpfPRDNKzegKWdjMu8>uN#08Ux`x5*l@J$e+n_a=k9p)
zF!$n29M4pwIS$CPLRl5mF*PcfYhlBv^k3=#aB%I^jvU}^X
zn&Ep496ww|xP*POnwhInB*nz!NiBXMwfWUh@B0pAN2-WbAk1e%!KIgaYsnVmIA?71
zV2(xy>QRLz@s`<_`4fDCV`rbL2iST|?FAwH{TiQj_TofkE+|cN-EVx~1VK@4E2h%W;Y=5NIG8fenm5yL}2KwGlvsFfexpK=-l|VNK
zLn8KCF_;7Mb2m!qcsSblKQ99K+zyEd0L_N|wey66KqT(dt1SRZqgUE`BuAR44L}GV
zrF;q
zhQX%j=0f?AG!b}A_H`<^6%1SO&ch9PL(1%D%WQCeSUaHFA-xIGuuxtcA~-wIuu-xW
zwkBh?>GQUrs4M(PJm$HabTLQ-S6NMHuce8O4vDSG$3?m~rCS8GjR#Gve=FXGj27s%
zfP%bxrP@q$A$+H1eb`6z7LFboC9gt*EZ)58%1J~(>e^IotD$j-yvgD=6_h<2@d@Is
zNH-QA{Drz;wn@?rswM8yIeC0I$l~&a^K~;sQgFS-DUe9Ezoo
zwP7g7u(3i>zv^_r0M-T8F6Eh(Mz-MW2AEB>q
zP!+d4bbJ)m0!4L4U58zZr<(-^ZAA3u9fb@NfdiR|((y)oa7TEI0+^mY
z@|h>z-SUj=HM+XxM5_L-#j-Fnnw8uZFQ
zfB_Xz4)R4TxZXbp!Q4>WL^_}ZCu`O2f(kacHB>PKz2U&AsSbM(VW$$s{R2qfKOc*p
z9`k`yAPlob7kwuaOlOHt3xbNUcrUM+j}^dps?O8Qw2#dh&P_Uxr}6e>xGOGB>b?lA
z^K-}Ks-&PRN|vm+7?d?{^(xxN9RODo;HAQ&X-t?6L-li#sou+vh?*Z<4R8^lLpd{V
zbfpW=-<_-;+@vMb*&Dx%kFMDbdTE`z9LnIQd?0ZF!2gWEhtlBGb0SYAbFAfxGOZpf
z&BbX&U|!dq^{hhAWsmfR$Q}LU6k=t!#>&)BPM01ZAw;I+kB5cwR;pLSWozj%PXUMc
zmoFfOtg5R1^hi4s-125mR$p|8Ym+90_~+mEOs~9IOj3v7?PoMFuIUw(VU5}TY(cz%
z=3XMfy&%vhK;px>^I?cG>TtEV@vk=i^>{jpyam_7qaUNXed-j_#WA;{tAAPc&{O|b
zr~H2$>Y!)Mc|IJ~r>!IiGoT##f6(4o*@uK0Z`2ThJdsN=&pE+w=Bl@
z;{^b8`Wa&DYoCX36C@0_a^Svv5ACAHD
zFX}lbBZisX{T&eis0qLg(_q{9igW%~%jq{Ol~?8Csam(f(|<
z!$|rU_FqoA|FHcB((}Je{}0j~j&b%cQ${Cc bool:
+ """Use the EU VAT checker to validate a VAT ID."""
+ from .utils.eu_vat import is_valid_eu_vat_id
+
+ result = frappe.cache().hget("eu_vat_validation", vat_id, shared=True)
+ if result is not None:
+ return result
+
+ try:
+ result = is_valid_eu_vat_id(vat_id)
+ frappe.cache().hset("eu_vat_validation", vat_id, result, shared=True)
+ except Exception:
+ frappe.response["status_code"] = 501
+ result = None
+
+ return result
diff --git a/erpnext_germany/hooks.py b/erpnext_germany/hooks.py
index 0a967351..26e29676 100644
--- a/erpnext_germany/hooks.py
+++ b/erpnext_germany/hooks.py
@@ -1,7 +1,6 @@
from . import __version__ as app_version
from .constants import REGISTER_COURTS
-
app_name = "erpnext_germany"
app_title = "ERPNext Germany"
app_publisher = "ALYF GmbH"
@@ -17,7 +16,7 @@
# include js, css files in header of desk.html
# app_include_css = "/assets/erpnext_germany/css/erpnext_germany.css"
-# app_include_js = "/assets/erpnext_germany/js/erpnext_germany.js"
+app_include_js = "/assets/erpnext_germany/js/validate_vat_id.js"
# include js, css files in header of web template
# web_include_css = "/assets/erpnext_germany/css/erpnext_germany.css"
@@ -34,7 +33,10 @@
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
-# doctype_js = {"doctype" : "public/js/doctype.js"}
+doctype_js = {
+ "Customer": "public/js/customer.js",
+ "Supplier": "public/js/supplier.js",
+}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
diff --git a/erpnext_germany/public/js/customer.js b/erpnext_germany/public/js/customer.js
new file mode 100644
index 00000000..5a903a9b
--- /dev/null
+++ b/erpnext_germany/public/js/customer.js
@@ -0,0 +1,5 @@
+frappe.ui.form.on("Customer", {
+ setup: function (frm) {
+ erpnext_germany.utils.setup_vat_id_validation_button(frm);
+ }
+});
\ No newline at end of file
diff --git a/erpnext_germany/public/js/supplier.js b/erpnext_germany/public/js/supplier.js
new file mode 100644
index 00000000..9f77d6d5
--- /dev/null
+++ b/erpnext_germany/public/js/supplier.js
@@ -0,0 +1,5 @@
+frappe.ui.form.on("Supplier", {
+ setup: function (frm) {
+ erpnext_germany.utils.setup_vat_id_validation_button(frm);
+ }
+});
\ No newline at end of file
diff --git a/erpnext_germany/public/js/validate_vat_id.js b/erpnext_germany/public/js/validate_vat_id.js
new file mode 100644
index 00000000..6930dde9
--- /dev/null
+++ b/erpnext_germany/public/js/validate_vat_id.js
@@ -0,0 +1,51 @@
+
+frappe.provide("erpnext_germany.utils");
+
+erpnext_germany.utils.setup_vat_id_validation_button = function (frm) {
+ const $wrapper = $(frm.fields_dict.tax_id.input_area);
+ const $link_button = $(
+ `
+
+ ${frappe.utils.icon("search", "sm")}
+
+ `
+ );
+
+ $($wrapper).append($link_button);
+ $link_button.toggle(true);
+ $link_button.on("click", "a", () => {
+ const vat_id = frm.doc.tax_id;
+ erpnext_germany.utils.check_vat_id(vat_id)
+ .then((is_valid) => {
+ if (is_valid === true) {
+ frappe.show_alert({
+ message: __("Tax ID is a valid EU VAT ID"),
+ indicator: "green",
+ });
+ } else if (is_valid === false) {
+ frappe.show_alert({
+ message: __("Tax ID is not a valid EU VAT ID"),
+ indicator: "red",
+ });
+ } else {
+ frappe.show_alert({
+ message: __("Tax ID could not be validated"),
+ indicator: "grey",
+ });
+ }
+ });
+ });
+};
+
+erpnext_germany.utils.check_vat_id = function (vat_id) {
+ return frappe.xcall(
+ "erpnext_germany.api.validate_vat_id",
+ { vat_id: vat_id },
+ );
+};
diff --git a/erpnext_germany/translations/de.csv b/erpnext_germany/translations/de.csv
index b0aae0ee..d9838eda 100644
--- a/erpnext_germany/translations/de.csv
+++ b/erpnext_germany/translations/de.csv
@@ -1,3 +1,7 @@
Register Type,Registerart
Register Number,Registernummer
Register Court,Registergericht
+Validate EU VAT ID,Ust-IdNr. validieren
+Tax ID is a valid EU VAT ID,Steuernummer ist gültige Ust-IdNr.
+Tax ID is not a valid EU VAT ID,Steuernummer ist keine gültige Ust-IdNr.
+Tax ID could not be validated,Steuernummer konnte nicht validiert werden.
diff --git a/erpnext_germany/utils/eu_vat.py b/erpnext_germany/utils/eu_vat.py
new file mode 100644
index 00000000..097f793a
--- /dev/null
+++ b/erpnext_germany/utils/eu_vat.py
@@ -0,0 +1,12 @@
+import zeep
+
+
+def is_valid_eu_vat_id(vat_id) -> bool:
+ """Use the EU VAT checker to validate a VAT ID."""
+ country_code = vat_id[:2].upper()
+ vat_number = vat_id[2:].replace(" ", "")
+
+ client = zeep.Client("https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl")
+ result = client.service.checkVat(vatNumber=vat_number, countryCode=country_code)
+
+ return result.valid
diff --git a/erpnext_germany/utils/test_utils.py b/erpnext_germany/utils/test_utils.py
new file mode 100644
index 00000000..019c8994
--- /dev/null
+++ b/erpnext_germany/utils/test_utils.py
@@ -0,0 +1,21 @@
+from unittest import TestCase
+from .eu_vat import is_valid_vat_id
+
+
+class TestUtils(TestCase):
+ def test_validate_vat_id(self):
+ valid_ids = [
+ "DE329035522", # ALYF
+ "DE210157578", # SAP
+ ]
+ invalid_ids = [
+ "ABC123",
+ "Test Test",
+ "DE1234567890",
+ ]
+
+ for vat_id in valid_ids:
+ self.assertTrue(is_valid_vat_id(vat_id))
+
+ for vat_id in invalid_ids:
+ self.assertFalse(is_valid_vat_id(vat_id))