From 0707876a81d38beec7ee154b502a5849c298c941 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 30 Jan 2026 14:31:04 +0700 Subject: [PATCH 01/17] FEAT[BE] :update movement number format in GenerateMovementNumber method to include hyphen --- .../repositories/laying_transfer.repository.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index b3d7e7bc..68867265 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -49,8 +49,8 @@ func (r *TransferLayingRepositoryImpl) GenerateMovementNumber(ctx context.Contex if err != nil { return "", err } - // Format: TL00001, TL00002, dst - movementNumber := fmt.Sprintf("TL%05d", seq) + + movementNumber := fmt.Sprintf("TL-%05d", seq) return movementNumber, nil } From 1a9936eaa133cdc0ba5f9050392f996a2c051c6d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 30 Jan 2026 19:45:11 +0700 Subject: [PATCH 02/17] FIX[BE] :remove obsolete db_lti_erp-202601271102-stg.sql file --- db_lti_erp-202601271102-stg.sql | Bin 323287 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 db_lti_erp-202601271102-stg.sql diff --git a/db_lti_erp-202601271102-stg.sql b/db_lti_erp-202601271102-stg.sql deleted file mode 100644 index 2a8495d814b6d4cd13ac8c613d9af3150290542a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 323287 zcmd?S37llfQ6F3y9Z0JKbl=iL+ST+(qfynXdzO{9HPgF0(oE0pOpjJ8AxbsfHQlvS z-BnvvJ+re2X(eQYkYr9HFdyav8-wBV0S0rJ)0jipoCa}-(-?`9KS%JNC*waa#DB!k zJM}M>o8Y&d%e~Qj*c(hImB+)MwdRRhv-0WqTl+*STDxn!Ke#*{Tt8NM%wyrh?|3@= zBYqwO|5hvV-zUQF8=I%jpFXpBXC?KAX#XYf^~H^?XnQmq9L#%zgYEIo@L)H(EjoH> z>yFORTW^ByRw~p#@Ui+r^zKT3G#iL=*ctiron)7AXcxCmU);I?eYz+{n)p+6YYmG) zw`TMH!A^g=)7u^&Tp3UIp=-U_d_(6{o;m{wnO;j)P;@pt&K>35m_ON?me@0SG2Wp?(VZ# z;CEYk3-HBgczIiWjKu~Ay-OEQ9L}$x z5DAGtec}AwRJ$}H&{g<(3jEifOoSdB&IVIf)KEd>4+0anKvVFexD!{Kl_$V&lf%oS z;kNko;;Fkf&aF#Go&#SgNaV+nEC_vm7JMc8?0ms0gV3+ZcsAdi4rXjmC)h#pBU#vJ zi=y>vHEvX%3SWRU4R*)V8z4$Mg9;Yfy6gIE^VIL zI&*$ga{Ipn4F#Y6LPJPVh%RhwUAnM&G1}hiPy5^R!8E$opWXoTBVatczIo@R^*ezJ zqsi{I=g;hLws^DcyQjk)*nS@Hn%qJ+_-S-O|Rd$^UP-YyOXy? zSPqJT>Cj{7%?1w~1;~+&vMbTii|1c`G=hC3%uP6xZ&qyB7$CP$8K-oHE=NGl`jn17+x)xiy05-^)_1+S=*7GAz6j1++o z%+-zEapC+qF)@!1z+LDM59X<_@4ai|!bSug5(vU%b>(R4`$%AgK8F}5OlMnn_AYFk zJ#*R=JA3~8-R2K$7I5!Rr~Mnf(coZrzPEPe>Iow=v*S_iSOk1HedglU88AqZ=wK~H z0EqzVE|DYzG?ggDIV6KW7uL^Q+=w>MZ=E@{5rIh|)GV4^9Zn{L9YA6qrmf+8fIJ@` zM7N-~qWdp6Duq*0mj9kQzX?ORw2}S>><%Dt>rJVVDY5C#=Lh}$0h+<{N?N4eqzY@+ zLFwu6^5HzqSjjp34h0YxlkR4UlH?+`dbpn~iGugm+S`b-fK~+MhqImO7=g(#4$B@e z3@MTY2jWSXgc+4KIRx9@Qe!}8xdrSS^#V&@v;-#Ud1p z+jyyvyv*PasGu@JXc)EGBef$@4rbn?Xwbn;P0BYZ$s2FpICTkpV+r5dQ6<@HH`k7x zxaACJjb#sr!?6@WBrZ@@WyIXzmAG^f7DZ^mY1!L&Ggy#|%Ck#$SM|gIW3U|XUFsgGDp&R837KDBW-dhF5BQ~iU3@jSx0OARBs~U<{X4q!t4?>wp@ycOZP=t=Ad7aS6+rUA) zckLKqUl`S;8Gw!C5e6)0oKl#ecxsmAMb({kB(wQ=`>G6!iuqVZk z1Z6QGv=c6OIP32YvdKa46*pG03vN#b5XTgOoB42m06QT2lj!{df$8eO z`1;y0+1AgXJQkmjk1!KCWc-e_7&jWg%2mnhl zA}Zp#7dP&?1iKO{EdFjNuB1qR!3vV1BJ}Fw*7^moX}IqpS56R|xv+6=V{Z``rzYQR39rCBd^B|QP0N9 zzuRmRA;@|!FbFEvFxZ{HcXQ)3Kp=JW5`49Ob_;fSOaZw`a)M(~qzSMHoESoMB{ABC zOp+XY-5LCo7{ThaVYA>#Fc#QQnvP-l?HQv1)Fh3Ou8?7b6k&*fipUE1TF$n?TC;l0 zF(@G<(H@pvj{Jg{(v;Sh0gPh|*e@kn_GQk@ekRnADr zg1TGA90(<;X=pj-0C zRpeGjDDvh0fxjgAB1Vx#;j$FDKN^BnAXMJ<560KwWCK-DPwf5e?eQV3ujH`@x}=$W z)9RXg@|V>LuQ66=MFcZ^jj-Ha*iHCNEi<0=Z?%OL5lt^-MUpiiNh^|U*T<}|6={%G zIlzl2&0mSZZsHE zVrwj9PaJjL8O*k)!--e{;7+Dgy9q?G6H$-_HQ#BfunBPL=kO?m>wxD{eb>Rl=1Z~V z(88L3j%73~98tP$`@H)943v{op^yW;0Jc>l`R%FmXD^-Gj9|^Te)@ED=f>v71=tTd z4d33oe&0nrCIjax;fTf7eM&^k)QkIAKG_wmIs`WmTVW-ZIMF8~6?NCwehY_ry-}5z z!*1^oj@Ys~w!`U|TQNh`Fn$(^f1JqTV7d+QON{c(_uydX-WU#oUGDEhunHf*sWL~x zCsgsl!T4}@kEx-Z=iIZg)8ZqglXS2UcjHt1S&3@i$BIe4D}seyH+T(wH%K$HhUciP zV6tX|gR+f5NH+=l%NxM(r;=Jsd7MF1*TXewmbo7`*u$#5HlITdHa&8}RYet%~N zOqPuMIc8(Bhg%8$3IrmCPRYT3e>fsa62Z*kYmv+SQ6G+Rgg03Vo5)Y*f`yK48nN*8 z*vj9+uFLOhDRvhv^>#zVHcj)qT@1uF-P$X#hiq)qZQI9hAv+j*uE_V3b`WEmZWluK zkB@D-RD8RqO-*U`F0Q+anKyA0X$=JB7>soXbRDjqXm_iJ`vlj9^BcW+e|EJ8XT68J z2e_Z>*~5!KnX-qu31-+!(Y~>Ro{5@_#y19u^^Q|O3!oE$*KQ=Nn^d{!%D6s=m5NQ?XQ}^%5^db=Yn!AVs`6!gxBdkjg#qQrB6zTUFyJ z+d!7F3=_MhjhGDHY_SY&bSt)i9-tIL{@V3 z{L!f9$P&HOt2#@)3oO6t)e2sPbw@39##8Sfhmv{JTh`8)*Oyi6#dT2_)mOq;vZq%$ zl9kM*_pm&PyA@nZ&^+OF=MzvThwdomtWsxW6OZC4F`+BR-R4p1szmR zRXze#@~DcaoS`YMAaFBSwnimNz$JfRP%64GoWX@wxcU!wa`QRNGq@)^z$?yh*;PeF zMC8qbDwm2E&CG&4swz?AFD;gO+~JT6_~{EtI-snF&o86E);Nrktw9S(x1060}AW z=N)MiL==Q$559>Y%FTukxqA)i?cTBshwvBZtpJVX@`{c^&5oS~xK3`lx zec@-RQ2d=d7aQVagx3vT?SCCQLqrZd3Z(|xJnAblEM9h{K{@guVyf7TA0W%XWGae6$})qUTqG+e#E}gXeQy%&(?- zUeSIktZ3zW%x-@kwXQOAJ)uei9(Nm^Sk6-q!tGi(>w19MzNkyauj3TVt(c)`167`z zrXa8-R-;!&(2JfoX~`uh^Rezc2u&&&=U-G#tLg-9Fo!)_HwdRa+}z47EGIL^xd^UA zFCuDlCCbE z{^Pn5a(VgFe;hT>_4Nvt=mNW^VPa0OnzgpbE2=vK{^e-+P#=%jd((F!sc7oRH&r*M zfzcB9Xf+0ypV^OtQv>$p7OKL}Qa*2uXIo2T)24dDn}qj5e=llR=6J9!pJ541ezPL?`euid2_$l#TC?H$^*S!^AN%zW^GK1gm(AeP^yEp8xsV=l^Ze$knQz~wh`se~$BrEnz8>x*vgUm?QOo1Jo^`~V z2L>883Cx4g>#!MLp+oly4H;wGbkj?jYs5%}AF4Gcn)kz`xyIKbT;u{K`V~5?d7G!!R}9ivGPak z4aQBK(xOqXi$@2pY)>IWmwau*o6GuuJQbiyRmzyqu&i0yI>zz(V7R+Ccdme>m$_J7 z-EdE*?MfHhA5S5>7aUg)5-Yp{UiEY$tnOz5gM6!-!q}G!%hZMMj{J#|3@_bDqaKSz zP^t^c`z4ZaeIC$Z>@P)O1^Q*)I&h6J)xM9V&>8WML?n>V3|G`K+)x!3+0nKCKw4sa zO@!)3Xf=Hf>d@wxqU71+RH4#*amYmfjK*&*fn?a_CJz}jSfI^ z*rB=lHYm$zc47d&YU7vX&CNAew;zwA7C(+|x8b!hOu8xdsyyYsiB)dgUt37K9iRV^ zQbVb|xkEa-Tj{wOA8aJ%X58jm&FuU*o^5KqCfeo|`5N??o1c?aSD?u^Yp{GxM!ZT^ z<(3-=`B74p@nJnF>Ke1aEd4qt%;|Ds2!t}@x8-Sb%MF2i96Ba{9Br=0Rk0$|M~&P> zRA0g9vo61oMiU-45fM!<(1~OgTIE(fAunqQWPR>_%9@`yW4k};Z}YK;K&Vx-#h>pi z_ZRjN@Y07RlkoiOwXO)Y>ejWU;5LWWWts0W5NdTQufV>rp;o79AG`w2f>_|grAV~H z?w-((kroC+txgX@{sJFrb%=OYMt@(Por&EQ{fn9Dr(1)&Z}@EEsE@B;_QdWFJj4Uf zZXQCGEYFR2BC`p`RL`(NZY^$2AUlKC%tEJe9Df8#a}Nke#mB8G;)2Pe%xYymN8k=y zy-QHE-gNN5Av{h14;AQ<~UD4;;R{4dD@At zOvn4td~Y~|$FESaqF3YlWzijH?l>P^x+vain8Kr>sn@R;%grb3T*tL=KS2V8i9Ira z($8E30Q=A)fO&M&z^SjJ`B-E4Nr&(wB!tJaAxvqOFK=RneqIP*etApLOhaeB!gtj0 ziS20T5Fc2Nq+9kHxre?oNqZ4L(= zci^YK`YE8JL@qeDsp=)UIg;A{Ki!ukxQr zRSozmsxv1HH2&Q2WzeZpdFKv~o|reb@b#={1kZ9lpuHBz_#k{|@LOR16<&+18=1$r zlK#uZt@EeeSiTG620s5Yi616#U|g8=c=6#cAHc6#96<4sSjyY$kKkb34(atFzPPXu z=H`#j^K+Y=pMQ!JT55hycc`5CIZZQjepbax zoS)sy#Lwk~uL}MRX4Y_A1~2~bZTTkVtPKql<|y#S1}@>8xtWVs$dovVSOr@U58Gn6 z#hjuwvzCofBp>)S%HBw zFN9_+g>)B|w`}b$Y1w|7v}{BuQTHTd(k=qDxt1K7Se7j-anGjhZY}>e=&bxfe=rel zkGmalFIexHc-HF-XT8+1^DMKK`;OE^At1~RU$BpN-H=H^qyya=m8-xW6C`;u>& zws64lcNFLS;~3qxoWL|Ic{>s~vTQo9YOlb6ut%2Nwtf7H`b2Suv>ZP}S`IP$yWn9- zW9ZVCk2c^d#$-5Dd|ROll;`Qh?rP;2H}BND@g&{ZC z@CsZE8y_4DcI~|+JFM5<7Ohp`Mv*$De+(`tosQO$!*ENeOGoBGl$6&O=!tJ}O0d{# zydpL$c=j~VA>M|604g%RU^cxKSoJRShm1>S_r^!PsGWzY4?%YKoPL(H^En|8SpFfj zMO*&t!FqE+ph|hOL)TpgA7zEIU9CI=#%H^w)zm!zgpb%!A!@&7?zw=Q1N0nWqJOOa z=o_{4VWFI}Nuf!il-Wc$l;#`?W3V(Ue2oE)!Ov3R;a9LC(dt(4)UnT4-aNer%4eD< zJ)G6~nz+B5VU?Ut_|O(GM#2cmgZ^FD82ub+jKa7EY|yyHMr9=z%Y_qEw3GOw|_eTG$ap?r0+o2$FeE({v|=%z6wtBJVX%uAZg?qu8N z4cJ3D4494ut-zhaUAeH~(v2@=u*6v%NDnIvmoRkriTwF_(qQ3-K7Ij1bthEFFjRUd zyn!k)n%0e-asGuGuNP5iv>L({GO2TPG7Qf^{WBmzxk7$TFaK#ub0rFNdvHN zG$e19av=YZ=dTP+n&x@48wgFhwO3$u+0dlhwvXRpUSi!{m;D#g=wfKn?LufbgAYx* zR6LVw3Y6yx)?HV)%T3Vjs=RYD>E9q91pHkl4yns$EHqUy`DB2X(uTNuk|SkOt*Sjf zv8Ai>7V%d{rP5CNQ+)q0@0+W#IA@sju!O6LZ;io)lKn|^eF$$H;GfY$_$+JM2i0}< z2lKu04v~(@zk71Ie{j_rM>&q2;S}CVfE0EF1<$QM81D}c;HXb1+lF;}oepa=m-}$M zQ@^mI>(pQTx?hsx{T#2^iiw)em!##~H$Nqq?~|-nLNXjQOS*NTKTI~9fZOa=yaQ#rut>Tor}7G*GqESL-KB0EI884F9Ih_vHF~}A3*^*? zcc7degl4`|P@GJnkYZm zt(P?q?)4$JF*lvi&a!6?XWP@^M3{d2L=N@P<)?t*78dtrz+bGmQ?XYUSzr308vHEf zIKPpX3wcDy?FH{Ye+>E}u)ei%8os@E{l1IQ`o-wX zX}FejW(yJmN|Up3&!vscQybCd`ne5#2ZHHc*)~dW39%N2ck#PPb;v=5fV*Dl8(1#1 znzA)i8*goj$5~mOTx?w@XJo{`t;*ahSR5VbLyvh@@Ui@p)+E0qh5Ej2>hBt*^8k; zY9J9qQyUCA3n?+To#OOJ9X?Qk3^g-mH+udeR(IRuodL0Ksa3uGfJa+wJrwydeq4G2 z@K9XIOor2s{7LCs+==1QGTiI(<$%|{z#bw?rfkK56x!?_BHIZc+M;USN-&r5T>pk;h-3H7$qmnc z`h^UR*qRx3KZ{j{5X&q7@Q4q&W=}8?gy2iq5S!jC?*WO4*@;H{HP4u|A-4Tk$`E^G zJ{gAC>x3`!iy7jEdjkKXnTFWwODJlQZHPT`tRc1~%QM6c_i*uRX}=ooAJiJ+xeTR@ z5E(W(py8PDygwXy2H;1-jvqxLpCw4uOc~)V)pk6A6c`MBMv3)m+qE1D)15C-a=j+4cLFFa6h4%r+Ob#!rcXuj?gP*jssFW@mqW#*4`E|6b^lT9cWDs@{7d zBwN|&71&hvu`E(5&uAEbnk$ zz2*kx|5A!8{Obgi8l9RDCB+)gR=y!@D;Knn{*zXh;Q~E%e!?#~yLw32PJ4bNboNY* zQio4(0K4h?v$|mR#kJXBG{Vb&L`m&^KA86L>lioOI~5IX zB5uNKOlqLQdAoeLLuDDmeNZMEnyi+++|H3{L?0|V%LMpX$spx5)XJ4oC9B2TPG}#GW(ccAyc^8VHu>yU6wZ@UqccKSAqD392 zDt){ES4p)Gz!~tF?tw9}J`*u0$_ONjmAAsIx*@rzGQ!m9v}EQ%Elk^RG5zWgVp?=K z?plUTaH{9D4Z2pA-(sSdyqVBR;!;64XVI!Hfm!G=-{-aztM*icC8E{&{iWlz+n7iphb zrtxMFYu-uIjP*4Ys=?1vHt~79SdtTKAky_P^d+A$)I?Tb1=$2{TJANP$`JdY7BGJp z;ff(tgBZH&vtJ|4-*cfP)02?h;}f_s#Ig)tiR-qGdyePzoJe(AtxA*tAZ>ymc?IO` z--o>Z?y;7i1%e^V$I;2~-fV+ZBq*?Tx_ynRAOs6vcj%3;kU5y7h1wtV4|<0aih}rR zXMCO9g4K*p50>*d)!6*UCXCtaWfak02k%|wG`f3mAq?k*zIes`vTz3*APBt8OOMiW z%2!t)13ydIz;#|=n-#n@Nn>w>!0<|-2A~)34d5W)vPI0@dVm+79C7%feY$jr@ z-eiy6D8+RP#xKmulko<0vagRjEAW5%#qo66NI&6}*p?xSr_W<<8rf%`|vXh5oO zo==IdN#wSy#XL*SGG%$3dU2b3@#^@Y>Ck;-{6(kO_GkaOsWGa+@HK z;kD^{%8v)32=5hU$K1>V5NV&xF&MHGzDnRbRo2QF_X;j@*qejerhRIyN=?W+VEYn$ z^SuIVw-!za`~qGM+m#3!(BdHHxkPt(pX~imEtduKsja{WwxKsUoH4&IKHdv?VR5G} z>fu^e{`@9s2P6106YPK~96Y0vKmV?y~=5jW}?EhxN3}cQkM$ z%`xuMdN6nEh#Y#L2pASk9!9g+3da%8g=Q>oSK}}~ffv+j<=LK~@@Dd#P&SX5)K1F^ zY$Lw@mu(w$2bMC3B9sTIKoC7Ql=oYtLFBOvSi$o^K8F?5LkgR|G95tI-hCz`MqRvR zEbJY;vnewz%m@3Pz58nJfLf|nmaAt9XZU!W518%yDMjuPC8a9|8Gr_mK7L1TYjF98 zWLe0ObZszVlcE!am_(bhWM-0e|1R(B@G{|@rq7XZ?yfeQ_7z&yVSK@?C!XUMwrI*C7Nh{0_V&G9OI$^|@tJLKr$j*m$}l-}9M_;AI~o)Oa^z+^D)4cFtDee9M66X!K4RxXh!!g99-zzZ~ab z$w%(>C0yQlOA7wQ=f=(b^ie?Y%Q2%m#fMD@{tW!`FZkEXZf@ zf+25XQ9a>}$$Ozo%%V0xXa#nKy^R&BQUq&gSFotDa?i00>ZvPPg78Xd9X1#nS%h zGsvDpCckE_haA24*58_KEM+$A$DZX-#wOX3RMq4cUS(x=ZU&+BcOYEMeN8FB@H~T; zm}Ui!z`DKQP0zznI@4iOz{@-9D=cS-XTLKG{Q<2C66ar~iT zW>^6eBsox`5ppksemW;oolaXkux0w}nV-YQ8xcyKWUw_d3=bJn#w{B9Gju z0xq!SEE6FMlMdV@-5^}5iS?Bq*q6n&5BK4=n=UlP+ZQ@c-#%VfK{hM|zF`0AAl(M; z8ei*=4yiiQ4Kv4@4b@uHlKm9P9V(Ghxvr>z;8!BE74_wn0qp-M^fa^y5JDNO@H^ho zl(bucCBHFu1WS<$-&t!)e2R> zn{3ofMF?Sv7V&+Elmgo$p+1MI)nnQtw<4S>Za?N4a^*W!-TjgyqGc*oS$`O+B&E%( zCO_sXCM7XdHTeZs*~n?Cruj5fjav02MBA&XToAnM7jQvVv&Z%{ex{IVRIT0T-HyOZ zA71qpI2{|cR~gFz=KDIcQ49Z-MEqGk3t9K zk8{8}uC~R%W{?kSzu(&*?!w$kg`KydI<>s3zZ{c&8ZAPiOtxiXznm3P-GEe7Rvmm)LovOM5Wikt)V8cFaMSXTr z-X5d8orA0scf}&e}yLUe@_fkn>y)m;)>8KHyDH zdf`EOGaysEv~_mfOG5uDPWT$NuAB}r#MkSO^qFFBxxLNogNP78vSL0(;+;~_j(jHIf-(et8--YEL>L*~_ObAImV`l+omZ`z=;Y&zudpbK#Y zCHLNrL$2^9k5hj-0EZ^y#+{sXa-4_>xkIja`i7$f6a7kFOFEV32@~yT32&U=9Wu@| zl@*xf*FZ1Gb1t6Rs*(7(7BI--Kp(suh%mY_&fg;qauC#jDZT_PaHg0BmoUOk!P3kx zWR!QD#{UW3l|RmEqzSnbZh~(lJ4R!F|M~L(OxjPx?qiJ%g9}A=LnhBp`e|x#HqSk6 zm;ca==bSnAM1P;#o=M{}Jiv-G8}1ZwJY!zn&NKLQqan((k2Du!*Jw?Tn$hgW?C#)l1ec#ZoI7O*TU#~<06-ixm(#Kb+ z2M^BW=d-=xWP)cFx5u-2ZwS{4(ko!9ln6Xi#TK-9g&j9FB=FbfIT9q$mxLW&S|RfF zw*(U?+z48xJCz8&V7tRx#K0xtENUw-knAPltiJeoFK92_gD`(U+RI#E2h1mbNjPhu zj5Q_9P7JB%EY2|E$2G3qmPpn?j{*{&Q+Md9L5I#AnIclm$STF;Gp>6b#z&4o zYv|zh19`HSCiW^l>SC@@Bk5^s&Vv;CJEe4>L|f&qH2)6_L;kq7sv$E-dE!LAZx-kE0!8g&)`>GS zdKJli|ETknl%F?-`#R?7G%BvCT`zDJ^%&WU$@cVMYDdH_WhJrXNx@)2dEmhhQkpv` zlAbIUx(zJL@z-?4Qkl-HMT?woBdpvmY@h##v=8|1n0hvz?#qe5)tkzviQ4DR!K3oh zCH=7dAUeMpDavXqzjy<9Jm=^|Aw`jrkI2XeJZ2xgI+_k((FXUUhlwxxv;HVryW`9q z=OY=SK6X?=Di?!KfL_Wz6}G*u)%{}twmNM8JBjVlOl*yz@Ui@s1+X;a1a|A&^Fxp>qg*_Ff5 z=thLc4G*F#)A4>J)g~d7NqLN5%EYAoyT^nC!wMc~SejRcgVD~)MD|ZeSdV4GnlP+B zu~*aSfV?LFF=zTO{#eC|e&9XAqk7WJ=z7v8@-96_?&lxABbSBBT0l6Yl{p_yNN zv_z8B^TKZCHj{>&z-kL~R79ik+)9nY2;M(}5HT76t>V^VN*Oe&`v`#dXC!zS{tVkk zJL~%B{XYxHnx&T(wf+dA<2yH^ZMauyz{-`Oo00+Lh^sZsO696zRJ9q^z_Ov ztO9v4gQQllqGI!620P-rNyVpgaw+xaq|jnIfna|^rvmxBm_d@+izl(lC~?)~UPJgh zEp;L zz0wlk9WVI~Aj)rZ>aJ()b?%R^4KjoK&XyxOGSsTgDK;PHD3ng+CznjPF$u>)?e>8l zjLd5y60tuD@A2k6ukms%bg4@?4!3=Alj4F#e&My=-}53Di)7hOC-_O?T|g<9;_3u$ zD^Omu;}VXy=^prq7M>R9qyc|HP79cOoPiU|n?9$nqTK^u<%y~3L1jz?C2pWL-7Eor z4qcT$j^oj2$lObMgVM8Hzruye4kwe*5DtZBM8}vNOaE9#e>Ch<2g#b=>(|3=B8R3= zaDHP#ZK?VNGct3gt=C?E4oB;!;BCVD5dCe)TohgFPjA3d2UAI@@YUYC0f|9|Cv@r*Bfgz80G-D=_A`Zl*mk zZ4^q|C~3r`cmF>~Gmhz{+~J2DdOp3BOT{`z0e$*I@bhpx;1AQ{ya>7>($@itg zKA+%ary+b<+dgkm0w?3W>MJlS?8$hqaUa5>wxv!l5dNC9E%>3=htLY)PN+Z|7>|r+ zWo(J^OikTgBYZ2qY9M}4>f%&iIc9g5Fq!um%P6xZ& zqyB7m?ADtqr<@LOwIjD}G105BEGP&oV0SnhY}+_+9QFTrG~DtW^X*QKJxk zbQ>k(C-pf7V<_Y=cDF`?a&5xH5#hFnfdTtE1s;PGcFepFV{mxO%l|j2Zjs z)n2jvga{_$jDD{CzbmgVq?I6uKa#2i)Xzm@YEN=isNZ(7QxfhDuk!0wPN|KRi#{3yYl}v_eSN1c|2>v9=Jz@ zX-jby7K$gJl?U$H>7(ElkI5ukj!O8nuhl{ zvvkshC5bG8da@TZ@OL=fP|iZG9+YZ3>Q!ZftI8%A1108!R7S z8l(6y7pWBOU%+GD6zRGRV6^BTI0GfI_AO{ACShBovHdKkNf%qc zwQ}uCSH*?9v6ADu<14;`$FwEvwSJMDhBRcaZOtN5YiiMeGWOckTCt4Ql|6v>Tcz*q z#>~Es$FnWgF_Zoc+-#@e46S2K#Ug0xN;H_vI!0GoEtd6~l*xKc#eQq9Y(AC8QoSob z9b?+}p1Pm`>HNqbMaae~+m<9z2w$%UG}4TX&Czm{X4cEu}Mi%pCRrq)cbMRtKh*-a?K!P9g z@j|o~-~!brnjMUfSEN6s>Ox(`_3A3F2Y)+r<2UepsfoQOqVKc?3hh0SM3&?Pv)Ezw zo^th|WbD>>A*HjJM_X!MZMiY1t2`f?BHripn-=&`yw4-B1oDinX5xLBEtiXRNA25ml;7BK z_ies~$GTN{IjF7fu-F6ZsV$P^>q(UG|6@nrDPzMDT!a@KFvGRG~T}okJIuo5y#aS(;^&-%v~xPQ!D}H0mLH;Z$D?Z)}-H znz0udt(Gg_ZRLNol|RyU{gBH%#tpGXvfT|_3kw=6hWJzK?^k#Yhh@t)>gwysxs!K_KoV^e$f~6d}xdNFhYM*52R8Y z2;GPADHOxrdFPNu{gk6)%8|})yIi@4*CU00*HwOA*Y)#0i^p3%SuWlu5f39$K!Z-CYN0`$7SuqU*u9Hv?k{%6-S)t z56u8(O`g9mCG$i1Bu(X`G+p219LtY(wGx4%(mmI2MNwk^6^?o*r_eC9<@usteuCsn zkb2n3YjED_Sjz6W9M&Ur`&2T2lm=tD6S zngp#TOdOFWvD$ezZYrO$seH<&>r>v&^B^WtcGHe!XiqOaY(IvSA9ZG0VD|K~+AUeu z)f%*^)}&2$E&3WB=emfim`sr5To8(@*qVhTS|^sIg_wz}@NJcgC#2()bFK2*8*ZZE z&*1TFh112Kb$xcMmZ4&0SQc0 zErO)2MyoU9JKJh2o`)cNmcX*E^ttZp^VeYf9&*yjf2tz6RNU>tnNIw|OARHm+sPfn z)9YL7g19(!CH|?P>wN0`-GS6nYr~!6`aP@Eiv-6b^LUQ_6@(57o2Q+i-r!&dZ%V*> zaK|H?n-a=Q@EwAkUVnZpy5qw6bMPV&6oj->Pk;_dmn_js_^cvqKV(Vjjpjp*Gn5d_p;k#|zSUety_w4nbfPwk-GQcEj^!Ep4Ap^Od1eQnMrjhBA$i%Q` z_jp7G(wF>(& zH^BgHHl++Sh7KCGqSOb;eRwEqdpev*X`jfQMoZhU1Y) zee7$MdKV!NNVZIgc1DZW17E1b>t7Op)vGnwqBsU1?7*WEH*CtX9!><;;MKj}yg$3z zgN%#A-2-t+B?C}*GRjR?0xnuZtOxs((fGyy_Ww~UF_Rij+&C>+w9*EvG{sa4lq3J* z0}8osrICZ+A*Oo`jxcMM`cg;!!o(y-#-IsotnHG&8O8~T9q#K{U(M*Wfh!L?nS|xY zk7?}w1&tjfDtHcooftwEW^c~GOcDuWWnn07=yiGa_sH+)q5o@o=-sBcG~;p_B|mHA z-<2_PK|>DQC2!OS6B1gbl7Gzy`{JYFthYS|#T$U*3vS-_csl4oUeZI*IVC<|^WfPp zH~=-if_`9semGmbJ}O6{@`hPNi+Rh>zH4zuTUAkK23Uy3fb7D@E5_3taPaCJ4AzE~sr zAp+63+k~s4xDe6(&Vuq`7fLl=g`p;cuLrB8Na`bDu*4KA@sXH9ZeWnK;X?_#pGK%O z1y+guj-1q}d>w%bIHYj70>3abl+N*zF*Cm1LC32vCCz;7i5&=vT#zQ+^wLJJ(hMB= zo`0yc>}%=KLwZS+3Ej;snfPpG)Ji5KkHPMUX)D3wwB)bE0oNj~H>>ZNPz1 z!S4qlRsaVQ&As9bet*avxm#cHTYZe|P8}>Y2jg8q&>avaTf{9U)8V$Y(bjmBCLAgU zCg~(xW#OT78y@-mFH?AI(0IW8RV)A<&JsG; ziN-2&^wK0-Bf`sqE>q^JRXNem`3g1A8adDylAB|6Oyv5@VEEfyhKtOFiRc&-5k3j> z-25RI`+A7!>8Lcms{6%9zFQCXv&rGMAs0D@o#nCrjPbt38?Q(im>4iUsHEUp;VzN4 zEv3}2`%1;z+sMJffiht|bGlWZ?+jO0OA2$^XsKvUF%%N3o&Kc14F;ezA*ic0^G80c zaG6uMbZfA1d@c;u$qm+1nffxM;+Gtl7%CDq_e)d^*$giI>%o}DSO4!g) z#3n6q#HLK*qv_y*!vSnO?BGsKY0#_ud`EuppD5_Bk*O|<-K$r92x!;Krc z5=?Gq>mDR6a%o!KRa*VlZ%}ghPMQU9KJytV7N}lj38cI4?*Xec+kTEfQ$s!;322f$ z@KGmtP##ojcl(z#RPUgng4GP}bea)mL4;T0lS>&}QOe@Xt_byhm{3~y90G3;xW!+@ z{@<)W8q9jI*9VbEGi@g#yZKg!FhaIbg<_TIs*6`U^HYb|7r#M_7{a6S)qY^ryHNs-n~QyRbGg zai3BmO?7`T-y4I_;4utQY-b3Ur1ZI({oz4>WcE{1X55@O;^t4VZ=xUR6C7npA!OZ? zr*Pi>jfxfjlwbt}z+sOa9ktYcBP80(U>Ktq{|QnL0|alAvlQ2X;kEOu3Y67DfLw=+1X$; z!Xwst{o8*Kj(0sc8BRe$2h$~%I zlFhFs;q5@GwJ0H*X(S;2b7ef;2QkSIQa9#D18d1iX|;dj#xc&u+t=VfTjdgCi* zZrvaB4|<1_N~D_kN794HBN;BWzPuUcMtA(ZGHV1#G9?s8#S1 zUrN}rgzTF`Xe!|{dmMQD17%BZrHDVYrn&C0U6os}MgV8l@KTWF5*IqHJaprA0`;== zHCAc&j{L??D1Cj9g0LHdPa_~qrLW5n$fv;#ztmbjRar@)`3&GwV-@J7r*HjD`csIR z4Tjf1S*2-%nB29&$n1Zl{#4;?caLOtcv%WEDdTGKtk38w@02`UmXfW+rgv$KQn~w% zeDEWRQ4i6Kf;Wh8Q_rNt%iz)1yJD{RVa%t2GjVtfewB#pScKr%&f!3P?6?BC6<|8p zflaXaV7gx#Y$`?Hkw5-P1=}74Tc-<8v*0q(q^e5f=gUCwA2-=edp zD!j83rJ@5#6wYm8n8nXK-$U5C-OZf8^GLfnx3p9L+AZa??>iJH3xKCPbBC>{<_+qQ5#|iR%^J9sBIg9$4S=1JzPV;$vTjPN50@>h zqW7j^m4lxR;iBVJsEPBsy4@m0$T;hE>dtH3RLJdkgujRY8_*xX?3F~Zsg_x2MZT(Q>^zS9c4ON=HBaitx1?784C>xO8PsoVK8P^|a zr#DMRe$WHa$+4G$&q>oDtY+d`ko_A~9*ms5F{J*$&xP^VSFXb645x6XA9m$%;g{T7 zElmMcPQD|G3i|)4fAtD@r(lggm+|cc#ss2#@m9Xy@7r zm=!j}W_W<#%+Rh56q;l*X`M^gNMsFrDYQb9`&o>yEb9?GIRQ5LWZ;^sX(2)pA zM9tR-T*~j&Yl;b^+(%TVuq1)o03w5gzP)C=X!C)*z&6_(P9_-n-X7zfOSokT#8aP% zc_+;QdXKX_E-26DB_C5cmIsndZ+GOoHAruwL2Af2m)Gk&v(8^+p(8WfQLrEyTCMSSt;`{3C9u#y zT?hHp6w|A4+QZa~X2<^)`#Pyha=@SRxT3t#Kha#-qPbG7iyfLYSF$Yaw+GpwRx=x!vMdeYoV!>RJoKwb?%L!7Cy+p2s0Ik=~IciPFXq0??0Os+%dJbeTT z4_l4;2Y3n61}gHXg)UQ`qZRA)5l{bh1#FW9Hg49%T`kNamtjcVyILagoG=XPpiCmr zO-~=zMwB>Ey&G`q<7%LL!9*>|b=reLJ z{5=KE-yk^R2iE$Ps>ladtvnBY_Y%Ot7%&U2NS}6b3P(Z%X8A69@GUFOP=Xi4R40 zpMqTsC2^B>6I0;HpT8wXirc&(qpg#`a5<&B4=87Ko|zeyCc^S#|Kx2U|}%V6sE<|u)#U~e-7VGHVK+NfL&tN??$n>GG1c#2&7B%K6|JGfnx zIlaQ7a2Dzn9(#)LD#k41Emqf$kYj!kF=mm?D?DU;#-Z?#lXO=wVs)3vj`*?Ui184i z!{rRw&Sk|Jk}DWz-L>M6BgctWT*n172AIo=yR>j*CC^V)S5F^LjtnPaod!xDipUsY zE@z&jxdI->u690w94Ycp_bOwAuZ5zS8tKxKBP-FBuX~>Fr;!5_EOhfyg-uRA7BVz( zp!IT{sjBNWK9L;h3z3DYSFq+VIizDTu|Y0=2|C(DZ{YKo!8=y8+&|u?m?^kNh=t1 zn(_(;Eps2(rP4FVK@+K`G8A+pkI`{htVzN!OE#!&d7ah{^O@u@ac zftBr*x+|sUkpn}X>Fh8fAxvONaXXaF^yNHa$TQf5EOU9U&c#}b`h0S1;-a$5QIo;5 z+X{_LJzH9~MXA?ZS$qLGEOGx>2FSvA%XMe#HFwI4$lU4Qu;AueX2I~1xjqy{6xNctS9MkOW#lL^ZlL?5Q)}c>({nl1RE(CU z45KdjJXYLt7nd(5M@{4`(E`V)XCdaQ;cAjF&T>75xZ_5Z=t#t+dXQ-F&^x<`x{pU%Zl ziXf_Nx${!oaV_*Qa*P;ZFo_Mq@|w@B(8&($jN2`B6&H71oBT?0VCWs18_xDpXa)=4 zkcWiNu}+&#IGjDiP16Bsf-cc4rMdft{$;fjLp@{6Xe)1x?)nK2cmwtb~{sq z%GbSm&Gma;LyifZdwqhyxxXG(!d%XMm@=oV~4-fHnD4VH|9x)hOtK@mI;l|5ia%_gjP7eHLcHruAbePW<^7Gjpe473W0C1(cK>~oQHIF;u{&RTb zxmIgI!YdfEJFzy&A>)Tm&-goVw=ck9poXekuE*PSXQ383I1Hqv2bW>0a=CysMHpns z``S&{&25u|#Py1~-5i$FTzkDPEfmd?SDW4?lnyyEJgA3vjKhPu0w% zE<{`FuRpsHo!>lrU&LJ(zA1vA_0y*#OND7Gy8FVJbL$uGi{7|#AKuS~YsHldMVg9E zUpRkvc01~}vV2iQTvUce;I0^6l#)Lv)xnF(?nKmP@b0uBtt7h8=Du=de&g}QI5CT_ zzN}2*s=pMpDLh}FhqRAlitf_*#VvO;?2j-zqWDS6D`X$5iZpU(nyoS3oUVwq>q)Mw>(bj8FhOW<@Z^f)WbpIj0SYI1`+5f&>7XVM~Rb3UH8hKOWJqYeTjiW|hGr43DIaLEo#pXX zPXbx({u@ra>+pUn&LZZ@dSrg%@eR!)raFnMxy;Vt8Ja~5X&=WF-KFu1J$d~LPF`!B zdIeS3xxva`jE{1czpk=DS^fD-j{k7_ zu`*Z@|MALqPv1K2ajwa~DFgB4gu&}*bZPSpqy&oA#5b7j&J063-(Ortr94K#9N9snaK#--tv6#j$-~k%ZGZSf(P3jbK$XICf=pc z2`KDKblfcW!Tk8-F<*h^&D$&7X&=TE-Q^{>=FY{xN#h5xmu5{w4UCtmFMM0S zQ-8wa8HyTsgehFTLM*Eoy8k+Xk*ks_he>pmNF%BsoJQ# zjO~Zy)es8dx|qBg(kzZ;>AM$G{WcoExZCJ-rM#*nH0E9&uZ9Nx{iRar-QRXu?~y2&MEAWweS(Ys|EYRGAvJLmNz?9cbfbzo{f#li9&2-9h1jK# zxtj2#*r&fVu5rWlw*NP`XVi$B;>|(m#SGyLpm;VsL~^NaC{pXZP6j~=s*;pF_M%b0 z!66ja>*8#I$PgCEJYnoqv>6)GsJ}~tgkP|d6*^lW`{XLE?c?&2RCCu6RmLcG+JMtD zVEE#)AIOr-V#XpBGDos5sf?B?B~f@$fYm}#)t4ztae1^_u08v6PO9p0TTH%sDXZVj z4(MNK^3`N0%=CAxgxwxMO;`4C(_fV1|HpOLVjVDY*2U;>1A6Qv&(fLKBIa|^PjW3g z4GfV4*2;wE;Jdq4P_~T zY-M?*+wPs2`*@^Vm8Nfz2&B#J5BYo{JVn?qYxwCVKhUZe)JrCxuXe(^MyAUbn&;{?M@~L)u3*MR$4e_5#FT z%ZYcR+7ydNlM2tzp9qibPOlE;_|m4IThV?8SNg>x>w);&*90Wb67m|MI1wn`v)`rr zWR!v`fSU2pV*T^jY3X}oVLP`x)4T5M@pU}Y>lO5-1N{s#ot@bi1h6QBv3+WW4+Yvo zV|8cwK-rSKFOOIMh~rhg-LAYc#VaM$vjmIuN4&|~%O%>HPM9IO6l(-Siy4cpCKFH} zU8F43U7z=hS=1Y`I3sR)n?O6@Q-xy3mQ)a3-j52M5x2y7G|Tdxy6%$@U&^D{sJuKW zyD1dyoqle=W;>72l0K5f1D1}7aIKjF%%^Dg@pe8JS+JBs$Qp9x_&*-Rk7pq6G~hjN zG!JHH$=q{<7s?Oholf%+Zyw$mK)|O%39w&^Fb}4Xcyao0G8qlwxo>&WG-NbPJu?)K z8GezHj0WtW~aRNfv?3|-lSPJdC31lDV=!TnttIXGSe zmmKgQm^j<1uQot>2YBqcKN}=I;E@c41zaj5qEt(ivla>qxDELTQ>`rRLf6$hyY@Q4 z@32U>Wk6i?RN))Y?##iGvzR2#%4R4auKMMAwh4+Qpyh^je}}V(kRnvfQp%sxr$~il z`iX_vlLA~3FYa-I2dP}9Z*6>r`@q7qHIs%Oa z7{azs8K+RBBhWm@BDU?Nc~$rJn16!h6}Zi@)sSSLV1kwAH1%Qcpuaz0!|b6$I25&< zteKfs%~}$>fy>CyB^|28JlwF&mB0%3{N7LUtf-6gdt|Sv4|zoio!_G<Six|zQj3S6&^<)yRgPOGnHFz$3( z9dRT;W9p=%H&{EL^O%Q@1Tc++u&3IkrqR%m0H#@%7gU=|^CWJ$8`W=Tc+#!a<+)X- z-@sEt<$-cJg^Hv)W;G}M zG0(NaU~%&?(HWLQ8D1wrDj#3U(yNxcExpHZqg#VnrQ_*U;wc12p%UNlJh_%zfD`1h zRVY!I}?Q+b)oA4f|9v&-A_aw1zR_?abi zj$O};0&~)1waQYO^t`>u|6b_vAF3AhxPQ0f`Om$(iaQIC(h@?a3L^EQn=EXV>+4 z?qcxkbYUCx1mI^L7)ZRd5m6rs4wFZ+LlFUr5(O|-j+NR_M1UgX!A=z_NrKz1eYl(9 zK(|`&3j1I?gKvK%uoq8(EMy0Qes6!cJ5}eOy-#?F4c$NY$WS8d z6(SIZx7cS8w#NW@XfMwr<)Q7B5REM_y54KgKgWn}w*#5P8XzkAR;u#^OvHT7GawZ9 zVu+D!nD-BM`qLe!KLj)6zFsKo#Wd@4Bhcp3JgIw|+~2_Qq!G&u9m1OR5)ae>H^$pn zlb`T>2yLf%WM~dhZBfQMr~__~3Dl`}mc|>B!aKR9p2OhXX+oCi7YH4|p5QY%3TW|S z4(on|%(p}s>+{eW>J+9S^KFSUU>eHHqu6whW*p;CY_(;mL-x+xp;6Zb@LUZ04KKx^ zPzOtnhn(mYYf(a>4z_8AAzZs9@W*urU(NBSU5zWZ8!2~tq~7&mc%)zbVRa#N*p>!M zk<8DlWFIJvxgW;HZRrKOkdtT0_DkTKYn^W4_|}5_Vi?f%#vqt7gmauxYR&SH3 zl-1eq!v`Q+?QnFXXI=^E$!|#8`gwsN(_~0gxi*bqwVlDkzM@0w0?=Cu_rQ}@cF0S! zrC#r-?7Z$FncwEvQ*Fx27&@<|&Y=v_{6ONh09?8s?9NE0%ma{kWEp;VQ< z@{NN^t3+0}VUg9Jl!dIOw*q8;ne%3v-Btx(wHFU%M&`Lp{DnQi zekmYDf&yt>^b4Arw80S)bKqx)BBt2omj8mXFPkpe<-x1D$I(B=ffvWI$kHdfCjRAJ z=*_IL@u+djkds*?8Mu(%3UK;M4ySrkW-Tyt!pu$&a$!I!ZYVZuNl~!bZ`sITC^l=W zdjQ(6N?N#iS38?2BUp=@-OA&E&vBv5 zbz5-L?q$$L^Fl~Z|JFwvGZ~L=?2o6Dz2Ua2s<_3JGIY8kywjr|6>&)<0M$ht#nm5+ zn$qH>K9V-CMPjN8^6f7TaJ^O)0K@;C70Tap;@POzWLja&xg5~tZ3}10@`=pKiFyeP zMK0%}ntU^fo!iRNNI~LdC5mp4~HdDH4f4R1`S2=xJZvJ zxPx&A-D(eU#JM#DozqyE&`E*wAUwg?KDfKnO=PkTDd#SWADnzhsdWlG{4yvchS$+wcA=4^gxr0cQk&Af#}xYn$S-~e__->>U!gI9_k z?ByG-Vq>5@Wd@mg2?PNmakoKJPXlP1LPdTR3!AQ;=-bjT#ooQUxAHK>K1|lpF9wsX zmm4NqB_~X_!U|yeY#ydsOzUq>T74EF_BIQ)h4Vad?)crY7&f}3q2BEBG zOPte%GVqjk&=bG6vADwZCk)|{xDIz*NIyRpq{E%1(L5Gj^X5=z4eoGfg}8nWk88K` zd>2;}{{eAvk$ra%*<{Tk*rxY=a0&_I`fBKfKV_$cBiWS|BKz$evQ1dIL<(8;*iX+S z{4W?ZxAwP(TN@zZt1yD>>T>*^W&B6#r6>t%X>Y+gZ|Hi!SXT*{~5;wtFBsn4k zn8~8FP$s}ZT=5i0SUQs=lS3#0T6c5gTRCVOZMn0QfEI!wCj99dPdtClp_#1b1L&-3 zY!hK`NU=Lfy99XL)yx0K!_zdgi|L9ML3}yd1&H*n$(orEiKB-7?Kx(x!ln=?e%c`0 zSOHZ3l7p%PTOfF1EhS0%(E>s-w}PUSsF(> zUhG1Yc60sd6m$0RD(BYD1Z!>0t#a(-$@$>H`7y+z>RWx#$<>k2Cy`=knq%j_I@7PmblFxg4XS^uh$P3f!HTk6l$LW%J1t7 zL+;aA0rXdS=sRM)AA%kXl(!mlr|I_=0iPmR6n=^ZGT?M!22hk%z=9Fag0^r&LM)h# zAw32p*OaGI34{E>BKUjMih{2Q0CF}(OQLhg2RzeM%=UP_S^*QrEECk}V2TO)RXJ;1 zUFpwd-F-3v*!7EdsjWrZ0Yo^zb@y8?0b2L+-h_v?%362bGv5fh5I>wpzF6$m5SQLJ$Q^l-%s5dKM!IWn~jkWH&kMjQs1M1?R z%eu4Fih{4YQ;v0Kd$j^4+`}=UR%=#X3I>F*?z6=1nF~8!tIKlQll6I^onTFRhY#;$ z=9(&#Z7mO<_p0Ls9=;Y2pPiHzgU{B_4WF%oHlYA~w&DulyU4>=7jF%a(syM#-p5`0 zgd^oIZ63@eh$N}pppvx_-`D0BPm;z8px)x4ZdBHwza-T2aVF4YaNF?(W<7#XZkUO> z2=Zs;2brL-0*Ej15X;NTG{n>X!R!iLqL7d7x)$LdS~*S~u**X$Kf>ugWb7OAy9z$r z(}c?aU2b}NmiQ|<@Y}GvfX|j7VCE@2jM@w#uhR{E`p3=_{kh8myjbeZY57PcI1s3i z4oK;l4i0{w-ne)wY)=AI%`DxYFOR$T+~}F~2grGG^|8d__^v6SNkA+~TO$E_kqp5dYUf$7pZQ zk}&*Jv}V0T@5*3=v^$<2d^N|A2IRMEK*zHfVuxT+a4itIn6VZ8vD@z~(Xv>C%5m31 z9HDwu25X$4`9J1Z(uSN>c$g@QC6mFy4n7wCz}!2z__a{IkYP&DT7&_K>Sk6>d=|Yn zTC2k58Tbx^>2MoavzW3ZXqLedMp?dw=S*81zs};!;jF(qp!k9p+6(c;P+Ej5rn2FQ z`GnxeZ3`)jp}0bRd>O}&Iy~O6ZGXmppCfP5}>v1w2Tr0K8OJ@>-Q(A4-s77w-47 zaJOP}V#dge_3-`k=$N#Kw;suw6qLb86@OZW>{$sgxjD4Hh=)n$F-{p>7%be67{hcz zY?-||3nyDO4^Xx=ir9&4O-^-g1yFr}hpItI6gXb$j;^b&SrTPy<^akTMx1gqtUnsSJsU`)49Cd)8RP94Qsq?70hm(*QNx^y8Ec4H&#AWp=sv_l zR~OIaxzLTpxjR?9FuPI6V9jK;JW$D!2oXj}vLTY?R{+fqanQ8kjiYBl-w+z}Fgaf0 zCS>U>1&muM0gL`B?PMt82nvQqX;5_4o+`aGB>pq$eGZaZr){nDLq#oH5YrjY|5AbCv-IOJZH;rAXAMBR=ojV(M9B-bR~V?mdB zj2j|Pv=BQ2Bj1p{KQFxGof-cjrPN6VA6c{bu*N=_DuSO+DWJ=6h*^xcd5pVe!k~IZ zCy+=4s=SyuPo)fD&VZbsWcY5aP_%b=*juz{!*u}q#wK|gBsC~gwK&c?QP|wrX+UY~B38**Yr#?g|fF(_HCHicM(A`=Kmznyg$L zJaVZUzA?vN^W?@<2FA?9^%Mr{P7`j(>lx5EedN_y7VDnK zunm9XOP4duO}4i@oDDaz$5R;?yREohc@2o5nEy!-5(g0YW2WyRr8qymVi1#ELgR1> zy21Fd`TMd0_@BnZ-!Km%g&kNK zNK3l`cfKDBsKlP&e>#Ia*pJu*e~Atz$L-E`WbikGMgjPfO`}lr!QYHF%YfJM8U7Ok zXSWmE!|JMAc%9BXQ?xUXZyT5ot`7!RU0{PQuN3TTqCV189dAL05}zt4q#rtoEa11v zbB}dtX4F+4BKW_f*w1x_jG4GyBC$&jzhija&aVM@{nBk#vb*;r=?K{PFpRP$wX}Po1u%aP2XnKXh@vS(FCXsg4(5;?YkVNz-|*dJ@{F?{ z2T?x_socv%AE)lEzT~ zt%>9X(yfPR@`o06X|B~3K>At+(oP3*sJ_A~zSMEZVtI84_wSXTV5bMyl#+L<^zdwG zSh^dQ3<0ZCI4!5PCIZXfsmgA7X=vk?`>@vQcxbEUVh`6d_&mLaC3YCdi#&Fc2=C3YS+f%%t?|egB?!X z<)b_s>gEVGEq$tII1j4UcvF!j=i=1lE1{ zF}SnL52p-_Y55VsFq1432>@#dhw+5cqTf4 z=mvsnDPq(BmWQ_E2Gy={(860|c9TBcGbK8N)P9b&6ba+Y)y@f|Z4U7Z?6`C7tN^y_ zENtDXd5}Us`>7;Sk0ZI&?TX~I0Dwt41))q(gEq?K$wYhDx{LsmxsuE5cy#qqBa4M zJ=RbVB3m&VB3l-PDGx-p#tI;MHxH3~iZQ8#;DOUOuQtOXq-te@qY5H2^t)qZZ&m=y zdw5vvoky!&iB!_A=)5TmO`>KtJc+^x5r5~A?AZ!nx`TnK(}e}IeynIZ*d9+s>WH1e zWYjZ%x=1D7i?trGDGfXQ$;t_^(nSV%D$g-XwuB5h@F&)7$#iqeW7l?9X6G2}y49w= zhhqCP*xfY?;DX2{tCF~3f%FQDa=20C_-iP1PfNhf4I^&yxY^qjPH)VgE^<9eJ5A}H zK+gP`6FXBLJZQu$kF=#5S^;Wj7}Ppl$OPtVYWfvG7qwv+wRB|?HCZH|%;mu^-DC;) zxoH*Nz~k36m-Q^?Re#I}E`r;+5H!_uV`yq9+2dV(Thi_83eY>npx3QdYbI1;dIVXj z;2n^wVxw_B-1nYxA(x?SZ3;Wz+WG6n89;bEl4MO+P@nlQ3fqSgAXV>sw%w;Vewu@} z(`cIjpE*UpI~`x2?=5<2E}KjOY^HEp=hF)YKETHiOB~8s8sd8Fz61R;JhUwnqUL&L zKBvw)e9)@Dw=G~QI=nKUJBJ(0=jrlkoYj2pM9%Gr20n1jkheEAN;|y@Ct?r zofd=(^*(_xVLkCK6DAFX6%*#M972ALgv;#OU~-=z7p0VxqopERPUD~G2AYr%+zh?s zNS2oMxaoRTM_HI57?TaN0l+_y^$siR)~)AZPEc9^=6MP|>3=N(cA^(60Q;33*zH=~ zqzR=Vx}hTCO2pw$FNR3o5YA046Li%BN50d*CwhnC&R>Vh-K`S1;KmR)GFX8T7l|sy)Bpbx)qs-x^|B51Z#7*b1QkOd9&Q(`wZ#w*YV$!en}=qb|NpKPZ5>rBV=RTM_Z?LX&j#KC|~L zfc0eztY9weeJiG8`jsiqV2}y5vxpP~RX5HU4gQ2)RzFq%^2-^JL3Zt75vE@WkI6L` z`5bL&E)ryl0-bh2r0*A6QKno)eIX^${t5to9t&{2$ruL+hTzK{$z4bn-yFt)pu)oI z1V_B)ix>x@-4bARL+Q`wVYN4~({d{w-w1(9UTDvtkrz^Uc_Z000?J=#&uDW6aK3(U0LDLk8jM=}JP!V?#qbRCvhF-bo#VUnO>|Ra(TMoTO zO>XJyW0eb`M^;XtXTC29JwvZt^y)2nF1hYz^Y~%L_;uT{Obl%@oKE^vxQ;U!k8bRb zrxUo3?#|2J<|1D;0iZ6F#!ogjzcCpckL2&t!8t*SoTdSpVVyoMt+kMkRbH8?xwG(Z zadHCf!yA>-@}!zvHEIi_kRZcsEs zxsO6?{%lQjbA_nI9BSQ8vl2wWK{RIZW*N_r6H9O z+9^#fq6jG(MzRvJQlViJima?OY{`yfMkw(==iGbFJ?nk$$?fy~{XdU~Pj%n#d)}|} zdcIz-_kO?8iE*4Skn0@Kh9l^;7@tGbCoU^7X7VTIS=giwNV&L23#R#TZzH~@p+Pnz zcIuI9rwo9mc{)&2pf@BfzkA^3FKn*)2L<_s2a`1%2<0W5G7tJ9PNkGAM6DvpZtnhG zF8m5!_&2|jr(Y#Xd8pt9Gx3$kEmTgb>-T|G%|kljROv#Xi#MQMT%Im&VRRa%bizeGfxTjJ zMT0xxBFp;^)VLq4MgwHs8&xQ1#Y0#)y!wY?qq9ZZsPN0|mB;@MtT+0lZcHNn2P(@1 zt4tqhWm4tgU(5B+@e==a_G*K^B@CH@Klt&IzER06c3UVC5H))M6l$Ogt*z(-*%V(g z{~&0EFS;O#-i0ZP$${Ubs1bfTvDOh51)CoIVshZ~#aEXuxp$)gR+9lpP44gl2#TMw zQ4{~0txyyH9Dden;y?dCP?I7+O*}m=7rB@P#|(l#4|5Wzc_8@9*ON|b;q=iBF-z>L za07+>AQnm_e1D`0_9x$8$1qgCqKj_}9rHbxVv@Z8x`sTLKe2$*HC@|+PR*2w1s2a> zp<%k>VZWxISYT0RTd0-PrfCOKrw-I?hsSl%Wpa$&YtXnZ?U(VI3l9$V_l54;B8EX0 zof-OZUE1=**P0H8Z;Uz|lJ;H!M}PG6B;+COg$F^K_i?=`auXZ3=$tShPW~QFXK+%h z)ul%3&g4?W*Az#fDawg+_kfy$&b#UXYbq2n0~fCVc<~nfc}nx3Ty&EM)NkUo1wWRk zE~E?*0}=2s4Ag-LYDwa244MEyddJ%2QSXOfjq&HI_|iZX2mA{_XP|K}y6-Y;O;C;q zz0r?*(UtZeYOVpKIj%nc?mBFym(e$a0QN@zZJhRm8Hk3*BCKi9+MV!$_*f#o2Dt)n z<&xvtN;q~Q|H1R596f0G8<{8N@$~ug25!*Fo1rRUA@f{9gS^Aw)!oqjk}lqUE=Yoe zH1sV8`^l3gncGXiPmoA~{t0|n@Hz0yDih2n+S;3|INF(+IGC&0PE_GP9qvQpl+2d^ z%Bf;*VrrpcZ#!MZe1^HHBlMMtjy9$alWlEOg2UbYeLY6wMf7nC^n?a=y#3S(HL*yc zgik{Xrs^K48Ib?M?<*nE1^VX}9PHvB=8HHeqCjsq|4=W!zjFkBFZjvTB@gVZ05jkq zeLna)KUxMz!oM=$Kb0K=e%}891Aak52FTxSz(7a9g8!4sARf$si|ph<@1UjSfF)8{ zq(Rmp+8?k7u=ELGeleL~tCJc0LRgrP3-+1#Y@k)KNWCcdNoA7`W}|}~_bynZN{a?f zqmtRmiVXUFmSC{#VsgRuO)zl5$`WEhKN6b?L;<0~h4u)-bfBN47I6ZYiY~Iu+1n!o z+o(i~hLC~EXAdhrgom(%MEs7Jq$WXPA`M6eQp5sWV*GJ|XkqJI} z{X-LDel!GdmoPECF7j=*4EUMFq<}487cCaf9gw2MCc%Mj8$<{Qeo||cRbUbZNR5K7 zxua{lqG?orU{|A{-)G4K%PuAnHZ=-XmXHbhk=Rtw8Uhb4v1C2(q>nZ?t4~y(NG!;?`{c!W$JF%v+G74S4I5c$2p| zr-FHFOTcSZNP1%fnx;<7`WKI8+d`p;D5eivx88^GEf=yl;!}h!#36XCK6$hv7EF=9 zPYJ5T-T3D~(To~liB!*I*HgwGz&7?`(uhflO;1@U9M0cR0mNq`Ty>5zB1P0RFdP2# zWPn?UA9P4pAT%9K14o!jrIXIetAtNri3j%uG5KKo+!R}Nae%Md$0$v#3RfvCLK23BP>h^2m4HHHp2ENX!pSmFdJQ@ zSAeeUr$d9LQOU6D6`PP$5vk3Hf_GBv1QXSlfS02PM8+SOL|b3c z<)c!~Zw*yqne3YTbYh6x)^~)27B#9!TP}qy;f=(nE11RRqCY9KSxI2J{IL~**i1xe zTJ=O%W9cQ+%)GKvydx(8hHagR*azi->>EUY?~56aUD(y0fWoQBDK99t2V=bv=wcAQ4Ul_^8^G+j$X*2k zPDcMl#HaDzeXXb5ikelZU9mlPa%*|w$H<6!5}O=HxXTSN3bPGNP8aFpV<*JZb|lg3 z8K_>#u8%K#I?FJN7AS6=$)=Al6b|QQr~u-#f$n`Fj9yZ1rPv5&qlYZu7ak)_2S=Dn zrNeH7O!x$rK@#@`F{2`z5i(q9LO{X~#U}(UD2T8Lq*6cD3XjGqgv2H1#bVw2|R3QnC6{G|3$lIclvwV?ly z`_72`Xq&2}EL66c|G<`QW0h36xa}aTR@1~cS4qOO0Y5#J{#eFGXR_AU^X0N3q7()j3#En zpHqpfXXQo6GugpFvWgk~*sRz@$`K-f{}fyMSRDr3%eD-xeJ-*y1KICH=M_i-RsZaE z$MB!Z5&*L8O-usVK2?T!B)(8M1aQG(GvEm45fBf6{w77II4}bpq{k-QGerkNm`bI= zuE!>P0?Vd9?h9gaVbf#7l_rEE{7`H{n3n+ekc}b{f)0h|pS7vaGmub)A`Y}WDkLZZ z+7YFK{pRB40k08qfp+S9`*}r5bcM1_v{Es(wVCE%Z!+1&fxnv!9$z+@;$UvC;$Sji z3baSeX39)-vlsR(85QV16Eid1w}A!0b%y!mNj550<}+2)F(HPl*qcu@w>P&jHJ^q) zA2E1!Xp=m=Qe3b{&D3O?sfigZhl-i`6mw_`+B9w%of6)aE=~E4!%HF8Tu}N?;Y}g>~stf$lz@oKxFV{LRwsK z0=WbtEA+$+=qQM`fMqU9Oe!ck8O5iBZV$xKB1OR8KuXZ{aWEzRra9#QWF3MWi-{8Q zAUaC4CH$XJ;=eW7ooHngpAwD^znq}CSon7ujFN}6oy05v+8d<~O`;%it6Syu7sks~Eno4n7b z3xJ-1j*hkjd@v|sTcir%FBkYP5kAM%Xe2z0jv^rgzBD35z8G}A2c?9HY&8nnkq|$4 z2_MMw2(nNZ5>oM!ip7v0FNJ#;`L!KoC#fuuA=I9Mk5z-!0HdR{>Og*%!K?_{W+UD6 ziJaQP+i*>MQxQVaL853tEI#}QX@EsWK(9Ffz=y|!w%-{*Z{g-SKRh%Hj*@mLj4q)U+w3Jr8VNy*n54|Q+Nn^6sb)oY1u z5?ttTCBEyT6EZsF?Z=DhO#<5451oW)485c$bRtuvi-(Vwho8SMbjcgsJC5*$j@b`$ z3x(byG}L!qAiq+DO$GZCI?5^7KPb}63uGFn85GF6OpwV?#UPppDjdKv&}7UY1l@XJ zfS%tL($ks+RrEG61ODa)E;O(Vn}d#)9{JpcC15kqK~`V0MuJ2P;Y+|)0y2RsZJ|Wb zCIE{-#E+OReNbS0Pq00Bx5cZ7um@Ob@ofP*27+$`==ws+sdfuMwx9(~?GJ!n6blAO z^&tGqCD;c#>=;BF9_0qLX@t)J;1p)y7eeHTlnd}>B#jIXS9x2o5{nKESthaA2>ep8 zTNXX_hH?*&7Cjq|fq}jba$OlzsUb`3$Sby?;4EzQfDpl3T|kHc4+imqhQ-35bFbYa ziKK)w(ACK723K+Y9C-OlD+Q({iAgIKL!=c(?&(0Qu}S^Z5PF7Cm8;D|&d4FfAjHea zp9yPZ5KP7{E3s0iZ$|sMfD0v2#aajSK~?A}Xdg&Wq2f@^U_eQhgN`9Nk5EBkGvx}d zCW5bO<@+gReOf4xDbxhhgkFs-9$Zb5KV#vc`&ywTfNL_8N}`H+g-BnePJ6(MS3sUg zmVg~jOqw>~33C+Aflx;N!hk%rbtFcz~W7TcIPiFa<>tNeVs$)R5?5E+pR+ zHg@5|yW5ndphU4jqNm2d4$240Ax{)91cBs0TVC|2Ac_4Oe&~hvkdl2Bg!CkPRgp>p z0j4RO2p0>k0K8;x17ddlXW3~Tm_~51fuo>xwDbk%$WX?h7J-0kLre5&JePz4iOaC) zjBVLn|Eww|%t(<$xUFv4{*%{|bwHjzgX=0xCv5-`?SkkDn!6N;kzj}h$bF(Dqb9!q zL0~KqSPHat?Gi>nP|A$;c5A1pzvkZG6scg{t@k03DW6CJ}L)h2`n37MY$ZMcy`` z${%qSMq|p|oe?0a+7Q*jBB>$>KyJIa)rw(YiIhYnhRqd3RuXKfvj+a5qXL7I4m6; zN$pxICB3WRL~H|e!cHeLt;5wJFEWV+qtlfbLE~if=v;imyaGV{jrGCb-;y- zunjm1AevLiAzReYwiOGraQ)Z!qpiKkW&ukhmQnDHKp))_DLAA>mmD+#>5T%~FsI8O zLPlq$UC;qLc&vi*A@&K#8kiPBQJw~f50rdZsYElt^3hiTXfinVGYuo7-xlhF#>*+0 zFiFK?3QzTL1t)KEp~X!wW14C8ATU$@R1ZZwdZq&6Af)>l9Dq^+Yovlgi9`h-0$drj zOw@TP>n50Hc_@<7xhEwPN?&Z8@L)1Jh|F(jbI5(9IUt-IU51Mcuv{Wnbk3fCQjsth9c9|QNMMH6 zqy7hkh0?SEVZmiV(0DT9!92>nG%g^*y3pEiE_=@vO6|e{G2Yr5clZa!E*v6dp-gmK z1*s^>Ijs?nov#YZcj1V|k`R5dxjilNaEB|Hr7m)^0L78$qcG}cP+bN>xve$YVvQ14 zFp(rKfes~_jIx~F8tK@vM2moQA{I(yDGa)7LW>`GxdvQvo7asVC07eNIRxn_qbQ-t ze}CV_f>Z3^X0M>ez<-Wauna&iuuvKPnIf3tv*L>S zi9J3a{uRW=&xQ7lBF$t0ejP1s^4$-!=p~;vp0f!TCOVKP@m=7a3wA8kUqGjVu<8$33TMpWVEsq8w&bBaiQ=bK+~5<9Va-e1)@Z9 zCnFFPZ7qFdr7roF7Pyfj_)HjMKn)WnpCUgPK!{Qn5sy#?Iy!B-QkN_+oo`X36+0wS zPeP#FYBL7JP>ZLJtVy6qE@bE(5)=S$loB3z3SxPU!OO@6FtenVNn}b*hn#^9FG22B z#ZIQx3gnkz;7pZNEZ%&d}X$VGuq@5 ziLA+~ZBCkLYL%iVnvq#F4@qxgaYjP25U-%Z`Amzfs0A)}&sP!+z z;QB1yrtNG5x{eTu4LlCHZ(mX2TCd#Oe?yKEQ&g4<=g?u8S7l`d4kG6;Z{3G%vm-36?y= z_n`6v52fQU31~@NZQ9BSz0DKX7D4MRK@v@c0BVf3rg9QIouv(dWg%J**-`$7l@qE- zhSrK`&5Ca$2Iw>fN3dpq(lN0B+6Z)53SH%dz8Qm!&_zy=m9(aE`Zp_~lZI#~bU6T_ z?e*xIIKomX+Mst{L=lN-Ka^zf zPz_B*22C_Y{M(zf)>qgFyk%2s=0? z?_^`uY+wnCBGD2i(0L$sATMeCE6(w3ScxbKnNSR|m?4PJ({g!S!5j#eJO|3hhAw(Q)?{NrMX)1@ zAr?QDJ{L^S585tbpd+CHy%c}@vher-xVj{N#(c`f4hGebp9J7Y&c-j0$dI3hzWIZs zk*hpmKgppL64D{07f)<_&`uD+$rjL_0@xq96!+7n#|Itc1}%Mq4~&B9U8I*!bC4Td zl??L2TTB|FF9L*;X7Y*zk|qb8M^fa+0JB-0#k3^|q%@i~FGYy9ZP%bPMwD&6Jptf( zdfEmOCVcRuZXV*fWZgm$7{9m#H@^Zhg<57)AQj5i3czwujDvR63Sc1Aysa`4zgE2~ zZD?SxaIxYAQe9)#f{y%CW`%q}W@7-|Q)MbN8+eUmXqa1|r(1|8Gz5oirNr3}T{4y* zWH=R~l}YzpW(%VG1||u`bYSBod;?Hj@S)*zVrD~Gwh-`duf_GIO z#|O3Mq4Or;{b}S^fE)9I`X9xI{>|U*2kuZatv5xVA$TTBhUmA@l8GS6sUmB9EMx=y zSJ(q!+Rug-jG8=sE0BO4{R;XfVzKA<r59*D>;fR}u?5 z(q@NfBMuI2Hs;sKdgN(NV-R+2=q4R}os0@ci(TYI=8aaUh^=2^g;LNHyNOVz84%8i z`C1dw+0^8i#u#EcnnySbBNlXiG)LzxP+zCaTlj&X8$j#Q;T00}?h_v9>m3vl09~wt zHv*!I3G?9ME+`z_V__Ab$BVEM(ARKM5zVHJYXhd$5hAtAU@$D+ODqdQn(|P0oo`8K ztwCwZ`_o$x)E3m@Nz7_13!+sN$e8~A9~cv@fJD2Z%>oWdNOekp+?ucyD~1l3Y9@j} zE;t{jeY-X>K&1WxFY+>>H$bRC3XM&VqZnXtP;j`vaBDtMY+NFANT{!epKl<@$R4m( zFz%tOLKQAQDg?bD2_zI@LTJT;P6z_KDYzD_&%eB!XtSYtGqEj%gVt@($_suAhjOg% z1fYdLm7HJ@8|Q5}DXFy^E)iFOfQ*Dq=auhKxaYRsNT5>@{s-6Bh!~{?Xi#JghI@0I zM#Z-abp9W;3+M`Q(gsRXuwCd#82@|Zz?!2Vvv~S1?$v6uz{H?L4`OQ<96yNpL{~q! z2r~a)Ua@r%1baX6Z3VIsq?UnZHqeGM$yU@sw!(wHBN(d|dH`Y2195O<;tAF#elQ!- zJjg^Iveh5MC^b$Vbe$QgbXvwv=;`xM*b9a>;=;Gg36IJpeuAA0gpLn`%z<%YoKOfTOb{|r$|V|t;G7fuXg8e| zkh6(40o+Um(;;m{CDEDy^XrHWp98H1ha&-e3=n@!1m$2aD-e8+J{MV%M0yK&44f!6 z)b@lU0*E=&>3hT{v12YMj)+?DA#)ud8Z0jIGc8dfip>J7K)g2N$u&SHuXV_$drblI zglY#4WD6wWfjosHI3Tijn1VaOMp{KySTH?ftioibEqshUQ!G$is9L~xr1hx$_gk!Ar#4;QR$|JGIJdA!2 zT#zi9uyDF#8f(GX02T-!K2x4xibGI`b9wsY<;1oirW}0({&G}IS?C=H#1JM7OdzPI zSnGxzR4jNCsbU`kyckOt%@tRNGpM4Y!o5F1BIm!nW1F^Tn|ldmFF=X!zok&gVPCx z6(5`6xIkzqU4T!AJcOnLz-9m)Mvk007vkj+6yoU{I1gH380_x_4}Tz@NBkUV?=AEJ zLCj=;NPG~#002*7RzaCWq-b{ti&+4+lza$eXki!AiE_m91-hU?Nz5mZQ!JXq1~A$M z(kIi@1=G}#(1IWkB?HpLvVnjyc_sE`oW-yMihYyF6#E#!5=+M%m9qF2p2q}*Q|#+F zdFh%v#C$H;pW=h%K_L=p%tDJN`9H~E4FF(yTF~~X5d^Sa^X8Ep6#j%o{g}*3NASZ$ z(j=8g#L2RzgnW_d%s)*1F31ZrdLc<1v4A0b(5VTiTZvYSq~T^B5E!&a5gv>n$q&_u z#80R_Xz_fJJvT~dB;una1H6)w4~?));|xR{Fnb|Fl~RiMaB*`80&gU*v@!(4)k1n7 zq}QOr#WH}PGTpI*eHk%*cFu@>hX@n<7yuDV2ZvUst_JpXwoZzDK`e}jPYQ!7daM=s z%w#Xqzbtw0Zq(Uu^k z_#Cml`&g@oJn*Xz2r7n&_R&!u{qdY(0L(2 z3&VWSU5W5AEI<;Aj>#nkV*orAU>ShiI2nkx!P1o(Hy7Y$*qBbt0fuw-Cz~ z&>jwA6VRe7q!G;JXputne4s5r`_GL2%@)u%1BHd_|G6cg5)kb`&}zajmjMR|@**K{ z9qrZn*J+JCpzk8KH9+wBMj+T2O0$w?Fu)oN^q})W#v|4MwY*%gGs*?0140)v^J4=b zQRMLO4Mr6Lo(hZ(fc%&gM5{o@hXJO5CS&}83wLoL!uMa^hu$bey^Ppq0pFU&4-&%b zX-Gjr4QLk7%3U$c0<|MhpkUgLg7g9CEV0#Qfl5I#3%HLVd_Mh_C87|bO+?s&_#apZ z`d(sL2;VZGo-|UQpc|QV$s?Ns02ac7H@X?n1rF?x38D_swg&VwG|VMfyx5mSe zve?=Mk{K20*9cs5l&HyHV<3`VY;o4USq7n2jS(!G7P>w2)cl{-aRRF z45T*7)AOv5LMxd*J9w45QcsO`kN>#SPk| z2cmzFwW2`d%EZPDzceCixO_lc08SEJ5#U+`Q`yTankZk`Hoz#@!^P1P^eQPsPRc-x z(7~6Z*kKe7>49J}t>z_RV&%j5A$lTGp)7Q}a|kEN0;_i%Ri>p&B*EEG6-p;QTfx8) z7adOIQ2K%WfNY@)@U+>nMXvG0R(^r_USeOHJ zQBv`l>!RTpUF%CZm9{^SIW*u1A4Nre3rgQE)CIbD6w@AvcyXH($CpavjZbM)+DwO1 zk@Z9~pG(R@(3|+Ip>^HFDwsoF*F6lxS|2(*ygvkpzspudu-R4^d|bT!ydptl_c4&= zKZ(|bp&Qfv@L?G#gUC@RK%2zoNg@TMhGbeP#b#M^UJlsx$0KbgZyMLNP>I zVs{{MO;1bcB|*N0$wJ|Ur4XbPpP10I1hY*XJtUm!7UF0!5s6Aed9*aTnR55|NuQu}wUUY0;Tb3*;6gmeTh18b%Mex2zj|T!f~A^t6zD-RS8X*hGD(3iTuW zX&8(^nMV7mkEJ6^%|$7k$Qb(q{a``w5EX&{6ZYi7iR^KybPkRt{~xnQ#XuD(7JC6|0djOtFok_* zdiES>wV^f=wNV=tx;v#;2Wk~DMQ!Y{OukJiiAeQ-7_~8RDHSf3()LlC+DanVxiUa{ zP_}_{Gz?A(YZ84`kX0j0wU345tXO zT#X-Qp}q5n&!J(KLq1T!3jm9!udO990MdA{TV#M&Aas)h`iqOZTcDpPR(lv&p%0>S zGBk})2oaT_csf>aQmbHDun?2NidIIkSRsg@b@T#ef`hBH$xEiZ!K`$V;TcpiAuC}t zVr=;^4bPwtWyD1)he%AANC&OeT}Gzi8B|y>G@&qvM#SRA=Ot{uBEH6@aBBi}Lf|D2 zy0DK4rum^E8LxRkA@Jf@>PO^I4hoZJnQWR;5G_?oE^^ubDPNaXy#ST8Na@>@!G z2()M-a$G813`dh&1$}O7UIOUmSkV;aiA9~?QxVK=>7XkUC_NP=dg>elUFbXb(2mVj z3jJL85!u~9p*@X-#)MC0@^oP?a@j=gsB4LKb?DdzHi27MFp#J)3t2X?i1I6IiXR)0 zMhtraiRx;j=LIA2NGK-e$ik0=bJM6lftY6e7fdlh(9(%i@yT@VjHM&~43Twfm5^-6 zVnv`5ip7@it%94caA%E?*vf<0>hO5{1~k9zfcYq;uMeWg*nk#1l))b<JW(BYK)ttVLF)ul^5ir*s0tuI1$^brv#1O@w>W$=^DyWzCD1T zp(7#T#Uhya=r8yo9T3}H;sJMPC=I z$%XVu5reiU9UCVZjww9+4?Ofi5ybDWc;su3^o1Y&uaH zG~Fkh8@u3y72i#v#ku_Oi-uh;c~D~{7$uKzMXX>kHc|O5=prCdJkPMLu-JrNa-l@1 zxKui_XmYFYGM&*x)QBi=MTGMN=HIyUqkK=IKB!x3Lfz{!Rcle^j_o z70M@Ax0y0L0}9P27?4vQPv^yyOkf#^761_;j>AF9rZWXfKEx)VK`o|6I7KQ7aY_!b z++xw^cgfJ5ujue4kG##IH-Nr@E_BV}5RATm5Ohc|Uv&6a^1L31@*$Cw;irM|(v?M| zDXib<7`A$d%a)bE0H}1*wgE$Og{wf}|bYM5?%cO=@R}gm{T?1sf z8#c$Kg8jzUbD5UAVGpB4Ex4b75+jIw+t_cJ7Aa%mQr#9Lt622V!DxQPi(aWfxktMh zxVi({Xw?UDHE3`yR0aMYTV@#y{s}*a4j0|?|5H+?B7~$sqLZ)<`Jl51lof96LEgoM zHmbs#qp-G4sEY7cLeC!*NSUs;5I%|ymoSx35Rq704lyuoiV_M#fhJ5M$SD>#Br2jC zN5Px^I{>-q86Yhhp=@|=!hpoM;*F_ABYdz3bfmI~^n{6Y^j=aMHBg2ELnzwOMB7rJFv@gas!%5)IFbt^g2biLkwlY4H#%`B z?O2%~6H2sLR6*j0RcLqs4X*;Wq+x<~65G2&a(9LPElN);fBE8tq=e*U3HXeA35j+R zu5e;bXE87-W_<>qPjA7+Q2t9BX0QRDNJ7f%C%P~S9!e;UN6*_t4TjN zS<^SgG{Uw_F@DJ~*{}Dj!{0U?^y~fPMPo_z>&J=B2b-lvyDo@a82%;r`{FM*zDxFR z8rbi5%+hxgMii{Nqt>|QXVC3;yPoC_EXrNla(;2tWeKT{y?UKIxln(+ZLpT^TZst| z7JPJZaydUTsAzn2Q(0Z)VXcv_o+X`>JFZwCk-y7n-NC%~2l`D)Jh-fS?wo}>TDDtf zNn5Xe)$YAQh>f<~sS>rY4G~)t$N!CLdYO0kl(hk@I7b0^VOJ!q$L9uEW6VF z)2EtU`s?1;*1nFvdh5p5+FJ*{e{J+_Is5DE(x~zwQRh39%~-K=;Gxgn4W}0Wu9~T> zXtT}K>e(yTtKTc%{J2{;^~aB^wV#THYbuT5l_uUcFRFRk()73~^7vHCS#SR~)!uzw za#er))7j@wuGG{x5MHIVWZcg;C%4?%GCe7`Lv+-#1uac4ng(*0zp#HYsp0*thNjoe zc{Sk+JC#(Ihy5O&IIDA4wL#Ob4s^f8(ZAxn*R9jq8yhRCCeLf9TNzWg(fgs>6III% zPEiU`32)zXEp4>*kCw!$8Qfem^6H)1v+w#Decm(e*-T#CAQyMT5o@hfBDYy`k`iy6 z{4~0zsBm0Qjq~Qd>gK&X07?NF3uq_5V(_PfIu?>{)*^n=3+NsoX@ zytiRSrP6&gyT(f04DpqI*-_V6-DXqA>y|4cCxjgiRu~*_*K7QXboc(j>n1O5mMa-4 z>ztB4Z%09GWbN}q`9pSTx~&Q*GLs+w6vBsw(BeXJ?oR;!6*k7zr5w|;L+xgcvL6KdC+g#D~SkCQQHp*mW zhuf?K3~mdCragdZg`+KV z{B;&rTWlPXGVNw`y;fRTrQ^gXo1=kyb`E_}=4Y(-%4){RX+^Q?qN5KVOnT*b;bzy1 z_p;Y3$!Xtpx_M*8_~nB}$xcu`&OLi$`-plwRewp+#3I4N~Y~` zi>ZVAnWn3c9w58*oqfHeqjU0l*PK&}uJ1n_K4Jb2z2}Ei)$R?uIiV(^@4^>l?|Qy^ z&v`cFz*g@^ZasGI&OckWAai%4LAQ@*uN22eb>=qyuv(O3_GD0F^q7kJt<@JLj{PwS zxSTr8#<}}?Keuh$v`?ike3TS3;rSSD$7@>*0wx;oTKvUvyY{mUO5R(iE>X!cN;Bqp z#q}}Y{^t4d&D%GGKJCJd*i!sCY1mEmUp=@t)#nar+Sey~LHW(%$lx#P)8dYBV^_>8 zy}dJjYLj#RmK}qXdNp@0dVA~R@m}#dOSi-wu}lw^DI6MR>0hUjJadm3_v60sJ>lzD zoxP|&})&y)}DGTmD!ME zdl6FK(?%<^@uOT7dfIGTK6pi^jnB8AdbaI(VQscTg-X=MelD^fo_aQ4KTxhbvFY2Z z(1dqS?o5szxX#-CUhw|>Q-x>Fo?g%+}%k8$rMmE7o({%==OU-Bmc889Y5u4C@>w7E{MjSFB|BPcqpMuy=HZT-TVB{N-{w}DvB0p! zQ{|b=vo2O~#(`PJ{#KHTemCxNu5=54!sur^z#- zyJ_p2#?F*q*L&I~4fDgwQN6Xh*mvU<^cv-CW8Tg2rJKp^gIPRZh50-DqU`k@H(HyN z@-k;lwbB2RXn#&ZrYYThVsfy)O4ryi_DKcuBQi@i4X%EDF?Xj`rz;hP+qMn7S9W8L zMsB>>JMYeQy0LZIv$Zxw7$){M&06!ya{CVRDQoI}s;M7d*iAbt<#pO>Uis=@Rna#k z3ckGZ`=D;dyW(@dGGgi4uI{}f2PtW+czP#l>?EVo#Y=NFvezGp%dZPOWz*nmre*JuTxyV*`Qg-vt<-5z5 z=qF|M$?TkQ+}xtN{pt<@g_90l*=uGRv_J6GOZWDw1%G1AXAF(Gr?lxoxXo~j-UWL! zZ#=(bU>M-C*tmIU+=damUcNn_lyE|-hu+lCBY$2^Hn=$=zc6~iOO<^aIy}=U(A@a8 z@U*o<*Oa;^shsYab2?{D5BU*mmXqMVqm%v6i^sAJ{WA|Plim}s zw(#Zs&b-x1pK=R#e=OYo{@oZEtN2k9e(cy3G2N!_kEKEFl$lwz{xvSM`@X#P;y_~I zkLhyWb3YkWXB}HGF1gR6En9DXZ;`uEGu^i6u#4V0mn~Wo?uAt?J>2WWj}dnt+}M7g z?ESD+@jYz*N~d3Lc>Z<9vd6!VFKzm?+-2N@DQk|5vVN)L&=_%MPi~0)<`u&oPqe?B zJItcu+%+c^m!ZX}>2FP*jP&wmOH#uA!Hb7xtJ?t!{xv3GKKbRX?6g>Og{IP5e?_PxSqi3VP&$NzF>F*j} zA0z9!yuRnr5JQRI<$pZ29(mrboblLhd*Rx3UQJ^se4pI;X#G(AGef_(yo?H}SrB3@ zbvpR-)1t-?IjWMQmupbYRSiy z-Ol@bF6Z@^UV8dIIqG*O40a7VcS>gatbp|D*nVF>^$I>|-N}8^bz`|5Q)m5M zJ8*tY{Re5AZnyV*8)R}&X5Nh)i{Pdizh?~P{n@6eGkA(ON`mhYHZ6tN`!NMu3!8yU-Y<9~)PxYEf&BfGb`%f7K`(qV2j zwkIxF6+QiRW_(lERe2jrbWPM%licHyziL+O)RY{brymhi_Uo6(0jnpsr8=naiNJ@16R{}54Ic6(ge z(WnaJ(szwVw@x&hw4%JARC(VyQ^kHx-G>-IQHrYMZ+vbvyj;J$M)oh4tFe%ujlH1J%=;%`aYoStc`@983a zYm#C|-@H3EQ$Kz7)HnK;I&$Z*)ArV(=2LhZ?As@es#fVb+t|1I?962s?|MA^IBQjp zPba$l<(+Att6X1wpHaQ5H9hsVw@B?MoTV)=iT$uifI1=taN-i^-=g%xYqh(T7 zw|=aWKu!}_6m_Uu1$ovctI$d<#Z~v~^6u=+D6c$nPBmcBzSJpkiVyNnXItkqIeIE* z{@v+!pwA13<)y0I`n`YKaQF1|6*m_~Tj|>^w0$Cs?spXuUgd(+FT)9qhB6`yxm)}z$xTUz4% zi*~v8m#6LDx?xsGb6rv0kDlForOa;Jws&ChLhJsfOZ#>z5QLn5KzdMfKbA{V9AgZSSN{tZx zk@H?Jjf|;^n{2M3keQmiQU2W;!<6|)uK7*6KOn(X;?4KK+6H$sUGAB82gj`R?VwY7 zXT{i$qa^dc@Mb7*cb|SSRX$?R+Wfe)qhgl6x4P+;f9R8C)`?pRRfF@=%AI}YOf~6j z8=T~CF;}8FT1!T2n49-D%hJ>&uYS^7ZL2o$R8Lx$-?5$>P%v)MXSuQG1_XCFJv-3p z#;s0&`uyGP=d;Un(4vfoYI@G`a}P~eCq4UfFX!va7H*%C>}r2SPsyZyjiI}9$oEZ8 z-*|oM-qP-4;gek|dHu#Lbk2AfEFJPAF?^-Tk{JV^jjddu(X;!}scNRBTN4HcOu2V5 z?B)ECHH&*oF8b4=mGVq+@fzI#qoQ3tN{LB4pJ7iDj;#xi`8MThwu4`0kNV*~&30rB z_M32iWUz}%@z`kT^A|0Ce)LMcf4VHcxOt1ojO6@VxnHgxO7ovIu(5r##<9-9yL`v( zkhyS7%ksL!k2QuNvL?ZHk@xk!r$)>zOqlRyz?>zuK2OVcjdfC7qH@Ikg@fAo*&m)d zJhL2=k^7}SH#^vLgxXS@clG&2sl%@hRx;F{c4lRcg3{!EZ&fqgZ;et|Q=r$!p~IF} zGJYGIi>A(KezT<{TQ^^Fs6tWa`>IZ=<<}hCpW80iT~_Yg-11HS>ZMsX?ha2L;IQ}0 zP^*0w7d31=4{wT`_}X-RufWxBPh34Lw_s;bL9wQ3NM-%!nbZ4?e(R_H+4{M`;5g59 zyKftDpH0zFtK9G`!thzjmpQkzQawt3m3RHRW6tc^zuTpsly(*P&0!}0KFE}q&6JqO zlz4zCF&Rz_Lh7L7RnAtkwdJKh*H+s;U$U)o3TNZwM(g^`GLDIJWLK?trE&0JVV2bR zrJ*)Ge^jU*{@KGOWc05uyGph9YR)?#y{oavrc7>L<=*b&%H|ztuF2e0Ha@*ydVlM6 z^Y)&dvhb6Y{mAQnS-nTfKbFzn6BjdJ$l|;eGSwRQyI%;8{VJb3JyA0Jyn0DpaD3{p zFFWjW0uF3Cks-hSN9rzh{qm}rn%7H>_Fp-6nXCD^x9m2DJ`VG1)Z)9PkLqdfJvb%I z>rmdag0$WrFB#Zo)_zg4Eh^qPYem^^uc4!lf4gIP&Bu6J&XtZY{YT|Je>ZOI%fk^5 z8tUKH|NQ*2^j-P$`Xx&i1$=cj-JkG9vfnWs>*4Zay07+bTrob+(R-hdt-Ss=B0H-|ZPUASlZf*NuAi~_WOuGC!BpE= zd$ZoYF6BdH&uqT8&@uJaG4tbpEqk8am{V=~_=noGVQY^E+kSU8ukDp!vx#%^e0%56 zjyaQ6=9@oDY}|ZU@Al8XBbtgUb=+!vm2`4zch9mdt2fK$J*YYoyJz+XUZ2^UCAM{W z(<40pa)9~pbyIVfj#hd-;KkRF$WZM|r%$ga+H0m}``GE>w5}Qo`;C;eO;b*lZ#-st ze9(G@M!T^2SBGrsFivXulb@51l}=f{%JUcX;I0zU#AIHP&{ocyM+8g~_V<8~pNP6WpUm9CV77wJ`GI{N5bAw=Uvp z+Hu{>Kb@jq>1Fv0N(q;r7Upw&opz4?(HF7#8&4$ee%iEGdEM0_iC2eCLk0)^*G^&= zlWp#R6X7ci7Nv7vb+InJ`EulQ9gY^KQdg@=S7*9bem+P0C`a!uSD(W@s#U3{rJ|zz z@@mh$S1(_dk_h}NHwrETDkL6$j$u3aIms1s=9g%7k^VgIwc?V2x+z{WPskN0C{B3C zacV3t_x`b~Uy6yfPlCc%*V=fsznwcZYJMLiKhbQS=AGXyzs~imk-NWpd~(V3FVD)a z_OG~oby$_^X%*hx*nT^^8$FipIApI+Q?%r~I>Vjo2DU8XMY+_(S^YIkim#lvTba`{ zRkhsL%Wd;yxR43fkkj>f`?1C#Pbt|n#z}dP4v`w)jwrldXD;Gb>|N! zbWgZ<$R06a$BmX7t4H-bAq}>T~vY+V(rFbLXnf`=qmu*=WBHlkSsp zYvdSvhrl5P!3I`+U(X&s;acMHp0kY}yB2t_Tz}+R?A@?Y8xI(V44YSVMRn@|o8Rf& z$NGT-qSyBRXcsWR+p@=N->(tHry?7be;vqQ>*bWx^KoJB`#%z0mQ)WCXmSm#$=%Su zMd{HuZ^*1P?jm(3{LP3bz2e7sPrNn#)Y~I={@d(L_W%6(*HA-ICO}U=_DM;xu~PR7 zvY8JX8ycF+8%iWmH4}_qXwk z{YPHpY?{1UVshPpfTF8@eLj_WC)|%%doHC}#XQcY``yHzUstJ>w9|HIS3PjT`OX#J zUG~i`D?K&C%~s?G(Ls6gSZ&v^46*WaFh7E|E7U!(NSsM6-5 ziGv*b?;5mbgjaA>%HQ$bxriq3P;XMXtjKA=7#+4PX+U<3(^Ei38=D`g&)r~THtL}Pi zk=Y&lR#)}S7>n1EuU3V6>Sdp@x%_0I@{e}MVrNU78(OS0>xAO9opsm4qzo#m{q>P zFHYW-THi&bF($vT{q6x~awE0ZNo=3rKDfPS`#S^V7q)lbKL1ej){7GP*`p;o47NCD z{6l8nl`J`j_<&%UlXt!3?%W(-B75O$mYme-k4JZyMXxlB`~K9UY%3?H=hniQjteg= zlNz-0t#ORhrK2k5OS)+;P0JiO;rfu10!>eXHNBGWWT_@0C0d=#?>7!V^2w~dh01f& zZyiqmfK;W#xW%z0g&Eq(o;4JG2Wn-!|_3{3^|T&T8BF?nS&E?ls5zZJs~3OVor) zCEG>w$FA;|8+*@Der4K&z+BCbLmIc=m@3~z@$dI8K95d(SF!P%)W}<$RM8X@WoMMX ze`!O)_@I}|QZ4P3awjfKO+K%C`02Ov!}3RE-#HxC?z_Yk4a)(~)*fHzFjo1|T-DKi zd%SP)IFvQ3gPlb}c+~#rnj(oWue3b`NX%g*DbPPC?yWu8?V_=w+4qmr4-Gqay6i*s z@eXP$RQ=NI5@Y@>bDd@qBXe^rm&a9%^Exx_Z19c$ zX?<8@`G}*wdtVN>OT9CDOUdWaKRWL}GCjoZyM2?~-DRmg6Hc$!wcK!j)2|J0YDV{6 z|Lx;hH;ta1Eh7|b_T}};dNJ|+?M~WPhRr<5J1US+8DpOh=RZ$(vy(h;?%M{e5G<5$-ge>Y|CJ>8|CqqfRqyNAqqrzR66BUL+$7P$*?m35QUH?qd z-E-ujQA=)8{`AKhaUJW|Ygnv4`{@2Y!>qIqnP(23)RnK7YyPpWr1HkP>)$&h4|`vp zYIeIvi&LJ{L^W51w0y~>UbkQBKUth@`Rd)h`u&TXHmbW-I$hR_({uW;I@i#;E@Y3f z?TkI$E$6OS=J8c0s>~qet!vAjn2bY1$7X)#Uh!SHs@b{Eg4pi?s|T-;{CIELwRFA1 z9T%_KKh<~RE#AVZ3d7vGN4)zpv3;ecjpo#zR~5pGbR|?>5t0~%zQe$2@#t1OXhA}MWN6C$2ZQE;iiK}g#9yZDBgvZs>Nin^9%lAkc z+0N1N-2(F#`DMG-kI0xU*`p$$?AMxxD_0)6o!-@by^-{oNi!v+@=emGe%@KgjrlvZ zS4?t@LC7w}+K7nipk4ZjHw^mSi|&^osqEiNB59u2_i1yi2L^52P?Yrk!1w(APT^g1 z+VvP#p`)au{Mz&En!Ey+`#oQE37xP@P<`!_F*v`B7T5PC?KH|Lgf*^??I z9l!9L>Ps*E=4{=0mFMMKJM+$=@6!UfPH{2!RAd~>=3Y4T!Sci9#{I*O&p)Kl@%}OB z3*9Y0Mc1DUD#)Gkql2gHhd^1w#a~{XODOy~K(12Twac?)j|<)h6$~}r?TfMq)vbS- z`&>Ma%uQ>gTlm)WKd_=gcgVj-TF9WYRU&+cfEu-Q+8G&6{i8`i|_N zvbUqmtscql+ns4u83IP?VJ#!x1UVCeN+OMcT z?Z$_8c_|<<7AEngBkZy}w%G>tyW0J7*+JV2h0jgy|E)VW^+@*z(pM}zcXjEYY^>!| z?x13&m14WAZrhHB&m)fe%Ba|y-VDkqY`VL|Iws`j)t0|c|NeOMp)g)mRsk`$O))Rb>N-fzO(vLoOK(cinwJ=k)Nf)V|{tRC}iRj<1>1&QU*O z;M1K;jmG|2+O%j{LsLxEZi|=ma^4(@N`B!n{OTTso<|dSGF!{kAMcKek=*`Z>JXC! zW0iJltl6_){o-Mb>fQb^30r1n$ENL(c!ZYJhW9QWl80wXqwGy*^Zxy?k|Ivq`d^^4m)q zC+{v)8?b(st;CE?sfsn-`%jh5@OdodX(XSS@$+quA!{a_wB@e0>X&Ul(q+t{x3ZHL zs`XwkV{@&C`}&PKN@e}We(H4Gd|1wGi@ZUTOq%^`U+mwwug{QvLzK7t{(SvW)TV{% z%Ijl>g(=*ysInZbJz)O%#m?5p+WX3AB)W#y2XsrgAp2I!ZszOI$)^Utww{|NqqW}S z-s1ADyKB{I3sTnSj8uED!N+(7XG1)%)NsYx^2^rCq}|Pe=bk;%e!unFqfgA_lxqvk z^s-k4JWuzWm-0#Rh2EUADkeKFN-v5?J7sOD7Jfrn?%|fu_eRT4SxZT_T+^96>V%K+ zq#oP4MCnvN+!FD5hw+m)ezl?Ivo0Myp7`)?hnSd<;L+=DzmfoVr?2IUW@>1&UaWTZ{vC8+7N@qef!m(nwnuhw9&fw;z_^Q(_?=4XFLCtJkn0Ron{Am1x+x2%P9#?NVTb5@zD+10A|yFFjAZ{jFZ^UsngiaNVY?VgVd zEz9t>^mx}X&ilp_$zJku#%>o?;})GT?|Z~xgL$U`3*Md$d3|wryJ3$ejxbVtscl>} zd-?J%x#lOVV(sqE{qg2@*O{HQ<&S)x678a2zWdtw=Xri#kKG z`0wc*n(xcD=+$0Pdh>U3cGlslHx0wnqtsT;(Qo|Yt?ZmP{`aFer`Wg(xlsX{gQd=$ z@judV+1h{C0<&)>ooal-%r~|>yeet)C&NkKRr}5@Uu5j(TR61wgJyBq$_qV{I_6iX zu5hS3^HTGUd+Om+A*)Ihhc#@Pm3`agNKk(DxIS|XzqY(<2nxtwRI@=(b4YOb$0aVF zr(M?`-5~pA|A?-6!CJ+~r^v6=-|nGVoSnP!W~GTz=cCn*E9cI6K5Au`+N~ovXAYlE z@|vlwS8Ue7{mrwec5zPg^3J-4#jF{hbn{?Z^6Pop??-K^NZ7EvboH}oj*3NDX*~xo zeIVVj)W72@y;Tw8`j1NNGI4FU*D^uR?q$19vE$ue;Wuhb){w96owD8b{M4MmnW3#y zoMD^0NsTwI%)`mW`@5yGYev@qr85d)%HMiCZ{JBj^zZw3U$*=>u+F;I+jo~@IY#$4 z59(jIvajvw>ZRs!ic21?4`}b49^id-!#QJp?F;!Azt%`Sj_xo!`cl)URlyzi+0W;U z9(>s}YgM9Oje4*B@<#P9Ju8y;x40ba@wxJ6Q3m(6vORi+P!r`6tJb%`21LR@x*_HG>~qmwgVG{@%wOF+C(~wARae8C@89=&-D8yW6O9y| zh?J39vwQCF&b;}tXT0jbb>q71@~e9K$0=jf^4TLkDHu%k>yi@{bz^mL?V#j8BQE!@ z*{SOa?;wGP7b0Nk4@3W;vG5I6-5}}B?guA)JUC;(vf|*?J5_#+pR_Ua-6W~-mBzMX zxGRV5OFO$yZKRjmNc+)G3}?-=a}2E-(GX)8$#tv`J2yltea*=dk6W8P)t$n+t=O*g zJ3)W`lsMJ$o$inQ2hP1YV0Pj1*{P3xVbp)_Rg@`dl&Xh$f-BXDhN}WZ+yJ5X;)s~>DPHR?FMvrUIWVx2_GmFKH!#c zitJ-WaLte{R$sfQcI!V!M&p4}d|k6g`1&-NlP$kKN>_ex_PN<^n`#F|Go9i^gX7kC zSo!eMJjRa?YN-kfUAAntWFtY2N1CVOFcNY&5W11p!m-jwdXqHNFb z!7bO%Y*5ZUzeLe8@Zo_B=fd29kCz1uU1YOIg6kN;DbX2yUv6E4&fNH>ql#sL4-@`` z9&B1*dwW*1 zc4I6~=E=-izE3avL*3Bh(?X>m%giX0*kdHeecu$i^xlW;mbAXhFJ}!_PC8;_#jU?< zWtf>cATegvowenChwVA4v-i-`T~=)%-C~E+ zold;ZQ&!FJFRSpsTDsK0KWBY>C2!q|B8$J#?e`Xq`yj2;>-^uevqL4_l|IZ&3+VUZ zM^*ayjn`k_j_7IjVQ*)XBfWf2Brmq9{kHMU;`tpO*ic{Xa%`kJNrR!e#kk@ug zWPJW!iM{O9%lwOM-hpo)1Uim^b?guQgF2D7w7ade>(c%Xk7vHn%(Geg=kEgd4-b-? zceS6D;aT@VqGadGnFytx7hMiP4gmdVgzph1JE!w^V(+x_Qmu z-N#i6Ow^lQwl^Pjw5)FSRfqS}!ER=WSSnI-!1cne#VTE-iu-0-40Id&_rg)nzdTj@ zeJ|=>UU}G|NBc1kG!IQk9kR{vW?)8HW>c>Wi+GO)P ztBA;~AAcm5p6;tDD8m#k!wT_+fRr1S?X?x-?r%G=^_)(+v(0EDsRg@NXDDB=HZJgZ ze=JPn#pCUFW<9+G5afw(pzj>ngn^EBZX^5-po^BmUS6NjMl79y7Oqka9J0gtogJh3)R=f6knUDI@w0yQdFIkYSs3Z zd&@iCU$j1UTDHNnlEK+c&IWST=WLsT$9Apu+I*2W?e?r~`)B$MC|O##A!$#irIi)S zjwuGkEV|P4%Q;VR!DjcyP5y7rUz;@~c9CRp)ix#Eda(%H}7fVz(%pT<;X|j#gS36qSLPhrxWzJI(~M{hqoyA}N`hxxo|^u9NRMURBUP%B9rPCrn8;ak zS8nRJvtM>Ci|$!c`E*nGov!Ol?Rs6Evmk2gGOY`1_NrJ0IV|0~N2@H>;;WU)QyGg2 z)w{3HkMU2KwjlkP_P~ZL!>$cFDcRmrhdCKLDt*r$(|Gs%+nn$@I}!rJr-wy54(|O& z_moUR&bb=>WgV_&j#XUJ&3Tgkgt!e(mRY^1Xcel{b!`X>IDK*C>AJ6|-5lRi;diW>UiHH;AX^bvBSKV&L_!Z>fWr!vyuA!fj*@@NaE`Rp=yU(RTjWkpoY zf|?Jyr~t~S5?TBHK2u>RfLWOvtkPW+uR{xT!kqW6{D}s z$-mrGgn5ZxAgtCIJl)dVn<tN6G54uqrpgQRbD;4W#Z_W1@%&4F- z?80rhY6H8rcd8{iZK3LR9vULDu#6YZVl*FRqlD##ZzK(c*lW(d<=f_jhnT~pE|F$x zB}6(O6s-r$`%in(`jUH21<0`q1g4=9?n1b#5c}pmdh1s01ylyE=#Xt{D7M5#IBY@167nIKgZk3=^WP60`# z!>^il0cL{hKZJM>s+OdU<_LQ7is1Q9P#n`4|V}bit#3d|RRYZ_Q;#Ic_kK_rb&|k@t{? zw>wg)oKczrcAJT?BRl&Zi|4AZm0D&c4hVgp4eVl`jvo9_Y}CHlpjoSlFj#mrhMtyj zKo7vHNe(K0*f}r4h0-Mj~iGng*em-x#ae2=(GE{(oGl|GR$+xPD_wBe@6#g zQ|cDVFX8srq{#XZK^?>)#;{b!g=WI0xweqo<)jaHMN0;>wFK7rqo=_}hG-LMK7H_O z>M)qQ1U1RZR>O&U!m#sp6i~ZQLS?^5of_)cU0Fr6;pakVV2UmeBm4!)WH5y;TPys8 zlrmVuSHecW@lFRT{JSe{tdipa3t$qRJQlf|oB|fP+aL?z!tZ0b_t5iT;Yih2s#qoW z-~T0b7+U4trc}W?-q$tyq0*^h9Y4#;VVSmut9JSIXZZuhqo{2v0-u=rb(+@Ab_IN0 zU0)f76x{9ry7YiO*I8@81OTT<5n&TTK@`_tq+-NHVXkB{K@FF$m8~R2PZxv1kk+?A zHKlsz$|McZ9TY8dVSz&+DjbRlapd5{wW45{=^0`d09X6)(Lw6`8al;=f)JzM#`o8} zyvemrhZVArq*zXpMaeKgj2!P(Dpc>U9_@PqN??XDN-lZ3VwR;DCkn;&xd%!3>$R^Wjdq7SPe;D?fLd028tno&v8ZqS2=R4 zYAw937$$MivfQ?Blb?@KKGRw>9?DG-6di@k5ljdB+CX+~LM!P{^$n*^H!^pi(g72u zGIJo^pI#*y8fXT;)3>nxvct*jc*0yqE^FvCSECd#&`2Gilun!5)r<@GRd(Y%FZ*PK z%&8Iw_*1Fd(Oi1(bp3caWRbC(8rc*q1-0ql$uh=Z3aW%Hkw)Ur*$bt}p5*J*lOC*Y zTpp$>{_fPNTReK1SH#1gY|PLck&Ti97xY}km8ya5eCitxCfB#e{Nf$q!=4rnSBD}x ztOUk|RFc~eJ?n~!c&K9NPxzicQO!U?=%|)jW?&JIOYJl*5Ip`?KUhIGX~;7+Vk{!$ z;_m?}?)0V<_bym)MhE5hlvv_>mo8;A-cKQ@$4<;DkKJp2RB`U1V(1tCmZQwu9af~U zpZQVYUA%9o`)$S|cI~o=9HPdDcICew3>D2*LtrQ2_3Z`T_tusFRM%7EKOw(?54f)6 zL8l6{!r+1-=Ef?4qrauO4xOWyQOdN_KU0qb55IUjh4|Z)7w!$Nxh-(b&f3Eh%;3bg zJChl)(2W?&jxB{?wC;CZAqIf*cx4NA1qiu5?Yqoj(H=6_=Dx(D-Rwo({E*tH)t+xR z6StirSWb^7-&R9|M5W`}=c3YVrMzj7cu^iOA+HURmk+HUbo9qHSA~cLfPgfG+B3TSb9A$@A$hEc(V{$hO zd=tDyIjCuDrqD6<(ckZnNU{pz^+gpe^xVuZX)>faf)d42+Z{h;+)*jB9#FTb@a~ep zE|#G(8x7AhH#>Lr6zdRuO={E-0D!Mh`lN7Pz-*MAEaX+F#8T-gE9p5KSW8_eKsnRVORZD2!$@8? zkpe;yO0)rO*@g9g4R3%m?TBw_2de$pxq-oTER%W*7H7?LeXY zH#Q$q1K0M~oFFH-&sOgbOGdTzfSgi%>!fbZ7-|UC~ z&Py**XF`cNDXKj&A5jW!g%iuM^76@*)e}M{j%!-_ zHP4G5mmDqbBH;X%7YHqlW(9m?SzPp6l8O=oCP9kNNHBxQ zK#U9r6y5b|q+24xUuoBTY+Hx5(YXUMeFCHVw59Zl~sNTp$S_rjHO7R z7qxG_zJYc?6OvtEC)e#AqM8a%Es3!Cl`*DtMhN^>SOTsX=@!QOJA8#SyxWsCv2~PK z^jlAZB9GL5GMMR}aQG$0dll@j1xhK%SNQ5}S@7V1z_?CGjW~J}us0I|Ck|^S5N-64 zwqJm|u<6m+h7i_%d(f47o%ujON%Zc>;|vsH1v!e+xJH`^IVAS3$kZkoO|2v%N3Ks5 z{ltqG0M5@Efs^;|aadqOR3n_o6NuBdkj5(^qBLCPPcffy#CTBvLKfPM*X9#p_eLC( zo15{HuL)ia)vFGfxE-iq@e~8>5cZKWs}hWyj8-cLPUcy*aU`*R?IMz-hjcL`a*XIM z2#j37aaGV!vx-f>ULLtDJ}RN~pu)Y>u494XUX3PW;|C-1N`t8SzNzf`K|RB{05Cba zZs~&$a3lL1p>{V=`RkiDLMFDH=VI#U5%#Rys?oOW_De|ZOH9y7KMkOzv*xum=ne9g zs2niEhe+Tfh37y;f)t}x+R#!y)r8anZsGmf>Hpl@ zx>Ugf6BBv-EgoA6Gj*pDtMJP*A1-#6I#Rf*H9FRY1UF0@Bt5TPUv$%yp@a_nR>;a~ zt-yprt|b9eE4vimi%MRMCTO6QbuZlb)ynKJDm$vuGwUo7vad5KmMP->LeORp6jqjx z^bf5PGah!TW*FZk9RCR=$tl#1-_xoLFYEh{S60WCyA)c@h+&flC=P+n; za3^D1YSdq~=(?Bn?sW+mkf+zZp&S(4O2B`ztWWa3OXR~3B(P1N&- zoD<=wJ$dHmW?$$KD#^W<$enSqk^-M5HGARO(#1-MGz{i74cAVwpd%^-^SGhAvx0SJ z|2;9`-S=7ZK$B5tseiv($ZSZdscSL(GSfRKoP+P*J7X$pVeW{Nj}+9X?bTEMMrM$rG{v_7X)Rn^!{LKMihJe0z?2Rw!3poR^T;2 z;YZ$6qB57tPM<}(mfsk}@+s-?_+E5J@q)B^JHHubO9I2TPByqSs8M}fLM2ULz4g%o zyJf7&V*+U9;Ln|Q&y`9~pO3XRa3W_Sh9HifMbxS6@~yI1+I0fQ{|m4VYATgY;&#_xHih;g5dtw?ln$G zW_-kit4Ko6ZK9F!juvf06d-6Yi5#*p*Cohs^|v|y3DxDtlHRKWip6`1Dh8z zu{%ZKtw<7EwzP4ho|z=dE}_$$zRl72b3N}AXDXM#$@ATQMyav z0~6RIhZIVPd=Q_K0Fk@mNxeG|0w^!L6K&G`3ZUX3`<>!pohg9w7eB!0d+{tVG5Ef_ zg&ye;6QV5*5d0mcZ9su!J;6=ocSO};Yqs;a=p44;71Wxo8+i{#(Sz`hIG#b&z`Q^= zP=p8?sgF){7CR;g3mTPoKm&|t6r}kfI|&^k#xKB0KnySbwOTsXHU-()G04OZ7%wcA zflNF*Nv)pdz>`U51URnxRBif>4bl z&u_%4^gVrRljN_wc%w^0)ayNnI!P2^F2ArObW=V0$>l7~ipd>X<5pt_9WKDIX6n+> zmcpkBA$&j}*W0bGH(33#)(y=~K{m>my&X*(ikqzfkUiC$JNCH+@jNvu0a#@+FtsxMI;V6>FTd8@vp_`q1D}u&Z62KZZ0Q5gsm&h*+5Lr#@ZCy0Bq3 zX%0A4>w(#t5g`*fZLAY!r92{R89Ksf0ajzOj6-AB?8kjF74=zDjf8oNBy!87jM;D2 zB`W0LkNV)@hAU%6)+@E72f^G>msP@&_Qka@!+^v04)nGSrR?39rzhcVVU)tV4w3HO zU9Cyr$YY?9GSCvz(FD_(n@0R4ElWz-R2{6cH6gJJ?Rb!m{Oz@GRRf$YZTtkO1(J;I z7!!_Klh|?@Uk&WGvme2xH(sJHUb$I_Yn8v|*`2w5EnmPmvhFSE>yzM?#HO$|q%_+l zbz}}y6cj;z5>e08YzN<;QIQY;rlOQOQ2jY_L?7%0Dfb&O)id(3mWmK$wWpQLMgxr> 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 W@e$$T^>lG@@egcJ1Z)5R0002_rQA*c From 3d92c4eb5424660667d068f8f01088a7d65a1774 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 07:42:22 +0700 Subject: [PATCH 03/17] FEAT[BE] :refactor egg production data retrieval to use date parameter in GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds method --- .../repository/common.hpp.repository.go | 27 +++---------------- .../services/closingKeuangan.service.go | 2 +- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/internal/common/repository/common.hpp.repository.go b/internal/common/repository/common.hpp.repository.go index e0f2bcc5..2c8187fb 100644 --- a/internal/common/repository/common.hpp.repository.go +++ b/internal/common/repository/common.hpp.repository.go @@ -20,7 +20,6 @@ type HppCostRepository interface { GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) - GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) @@ -197,10 +196,10 @@ func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKanda } func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) { - // if date == nil { - // now := time.Now() - // date = &now - // } + if date == nil { + now := time.Now() + date = &now + } var totals struct { TotalPieces float64 @@ -220,24 +219,6 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang return totals.TotalPieces, totals.TotalWeightKg, nil } -func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { - var totals struct { - TotalPieces float64 - TotalWeightKg float64 - } - err := r.db.WithContext(ctx). - Table("recordings AS r"). - Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg"). - Joins("JOIN recording_eggs AS re ON re.recording_id = r.id"). - Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). - Scan(&totals).Error - if err != nil { - return 0, 0, err - } - - return totals.TotalPieces, totals.TotalWeightKg, nil -} - func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds( ctx context.Context, projectFlockKandangIDs []uint, diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index 44137fad..ca76c67e 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -262,7 +262,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo } if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIdsAll(c.Context(), projectFlockKandangIDs) + _, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil) if err != nil { data.TotalEggWeightKg = 0 } From 9bf33d2bae0502528f909580649d6047242d15cc Mon Sep 17 00:00:00 2001 From: giovanni Date: Mon, 2 Feb 2026 12:15:41 +0700 Subject: [PATCH 04/17] add response stock to informasi stock product --- .../modules/inventory/product-stocks/dto/product-stock.dto.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/modules/inventory/product-stocks/dto/product-stock.dto.go b/internal/modules/inventory/product-stocks/dto/product-stock.dto.go index e571d2b6..be8a5b04 100644 --- a/internal/modules/inventory/product-stocks/dto/product-stock.dto.go +++ b/internal/modules/inventory/product-stocks/dto/product-stock.dto.go @@ -62,6 +62,7 @@ type StockLogDetailDTO struct { Id uint `json:"id"` Increase float64 `json:"increase"` Decrease float64 `json:"decrease"` + Stock float64 `json:"stock"` LoggableType string `json:"loggable_type"` LoggableId uint `json:"loggable_id"` Notes *string `json:"notes"` @@ -195,6 +196,7 @@ func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO { Id: log.Id, Increase: log.Increase, Decrease: log.Decrease, + Stock: log.Stock, LoggableType: log.LoggableType, LoggableId: log.LoggableId, Notes: notes, From 5b80081d05d6058dc3f070c617da0ee39b793e25 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 13:02:30 +0700 Subject: [PATCH 05/17] FIX[BE] :add handling for empty input in ToSummaryDto function to return zeroed summary --- internal/modules/closings/dto/closingMarketing.dto.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index 189ef7cb..421a8d3d 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -122,16 +122,23 @@ func ToSalesAgeDTO(e entity.MarketingDeliveryProduct) SalesDTO { } func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO { - var totalSalesPrice, totalActualPrice, sumSales, sumActual float64 count := len(e) + if count == 0 { + return SummaryDTO{ + TotalSalesPrice: 0, + TotalActualPrice: 0, + AvgSalesPrice: 0, + AvgActualPrice: 0, + } + } + for _, item := range e { totalSalesPrice += item.MarketingProduct.TotalPrice totalActualPrice += item.TotalPrice sumSales += item.MarketingProduct.UnitPrice sumActual += item.UnitPrice - } return SummaryDTO{ From 760b37449e556383c3204b634f52142596facd89 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 2 Feb 2026 14:08:29 +0700 Subject: [PATCH 06/17] [FEAT/BE] fix bug recording and closing counting sapronak --- .../common/service/common.fifo.service.go | 20 +- .../closings/dto/closingSapronak.dto.go | 4 +- .../repositories/closing.repository.go | 121 ++- .../closings/services/sapronak.service.go | 13 +- .../dashboard_stats.repository.go | 144 ++-- .../recordings/services/recording.service.go | 718 +----------------- .../services/recording_fifo.service.go | 703 +++++++++++++++++ 7 files changed, 917 insertions(+), 806 deletions(-) create mode 100644 internal/modules/production/recordings/services/recording_fifo.service.go diff --git a/internal/common/service/common.fifo.service.go b/internal/common/service/common.fifo.service.go index 14cbb5c1..190bf819 100644 --- a/internal/common/service/common.fifo.service.go +++ b/internal/common/service/common.fifo.service.go @@ -147,6 +147,7 @@ type StockReleaseRequest struct { Reason *string Tx *gorm.DB } + func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error { if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" { return errors.New("stockable key and id are required") @@ -308,7 +309,7 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St } if reductionTarget > 0 { - released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget) + released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget, productWarehouseID) if err != nil { return err } @@ -355,7 +356,7 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) } var usageDelta, pendingDelta float64 if ctxRow.UsageQty > 0 { - if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil { + if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty, ctxRow.ProductWarehouseID); err != nil { return err } usageDelta -= ctxRow.UsageQty @@ -721,6 +722,7 @@ func (s *fifoService) releaseUsagePortion( usableKey fifo.UsableKey, usableID uint, target float64, + expectedWarehouseID uint, ) (float64, error) { if target <= 0 { return 0, nil @@ -736,6 +738,20 @@ func (s *fifoService) releaseUsagePortion( if len(allocations) == 0 { return 0, nil } + for i := range allocations { + alloc := &allocations[i] + if expectedWarehouseID == 0 || alloc.ProductWarehouseId == expectedWarehouseID { + continue + } + fmt.Printf("WARN[FIFO] ALLOC WAREHOUSE MISMATCH usable_key=%s usable_id=%d alloc_id=%d expected_pw=%d actual_pw=%d\n", + usableKey.String(), usableID, alloc.Id, expectedWarehouseID, alloc.ProductWarehouseId) + if err := tx.Model(&entities.StockAllocation{}). + Where("id = ?", alloc.Id). + Update("product_warehouse_id", expectedWarehouseID).Error; err != nil { + return 0, err + } + alloc.ProductWarehouseId = expectedWarehouseID + } var ( remaining = target diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 92d3b2ee..30b4d945 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -234,14 +234,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin row.Notes = "TRANSFER STOCK" } } - case "pemakaian", "adjustment keluar": + case "pemakaian": price := row.UnitPrice if price == 0 { price = item.Harga } row.QtyUsed += item.QtyKeluar row.TotalAmount += item.QtyKeluar * price - case "mutasi keluar", "penjualan": + case "adjustment keluar", "mutasi keluar", "penjualan": price := row.UnitPrice if price == 0 { price = item.Harga diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 04391332..d37ae6c1 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -36,6 +36,7 @@ type ClosingRepository interface { FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) + FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) } @@ -939,6 +940,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). Where("f.name IN ?", sapronakFlagsAll). Where(` (sa.usable_type = ? AND r.project_flock_kandangs_id = ?) @@ -1085,12 +1087,72 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) } func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { - rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false) + poByWarehouse := r.DB(). + Table("purchase_items pi"). + Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date"). + Joins("JOIN purchases po ON po.id = pi.purchase_id"). + Where("pi.received_date IS NOT NULL"). + Order("pi.product_warehouse_id, pi.received_date ASC") + + incomingQuery := r.withCtx(ctx). + Table("adjustment_stocks AS ast"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + ast.created_at AS date, + CONCAT('ADJ-', ast.id) AS reference, + COALESCE(ast.total_qty, 0) AS qty_in, + 0 AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Where("w.kandang_id = ?", kandangID). + Where("f.name IN ?", sapronakFlagsAll). + Where("COALESCE(ast.total_qty, 0) > 0") + incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p") + incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err } - in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) }) - return in, out, nil + + outgoingQuery := r.withCtx(ctx). + Table("stock_allocations AS sa"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + COALESCE(pi.received_date, st.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date, + COALESCE(po.po_number, st.movement_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference, + 0 AS qty_in, + COALESCE(SUM(sa.qty), 0) AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()). + Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). + Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). + Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). + Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). + Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). + Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). + Joins("LEFT JOIN (?) pfp_po ON pfp_po.product_warehouse_id = pfp.product_warehouse_id", poByWarehouse). + Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("w.kandang_id = ?", kandangID). + Where("f.name IN ?", sapronakFlagsAll). + Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") + outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") + outgoing, err := scanAndGroupDetails(outgoingQuery) + if err != nil { + return nil, nil, err + } + + return incoming, outgoing, nil } func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { @@ -1286,6 +1348,59 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF return sales, nil } +func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) { + if projectFlockKandangID == 0 { + return map[uint][]SapronakDetailRow{}, nil + } + + query := r.withCtx(ctx). + Table("stock_allocations AS sa"). + Select(` + pw.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + COALESCE( + pi.received_date, + st.transfer_date, + lt.transfer_date, + ast.created_at + ) AS date, + COALESCE( + po.po_number, + st.movement_number, + lt.transfer_number, + CONCAT('ADJ-', ast.id), + '' + ) AS reference, + 0 AS qty_in, + COALESCE(SUM(sa.qty), 0) AS qty_out, + COALESCE(pi.price, p.product_price, 0) AS price + `). + Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). + Joins("JOIN products p ON p.id = pw.product_id"). + Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()). + Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). + Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). + Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). + Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). + Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). + Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). + Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()). + Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). + Where("f.name IN ?", sapronakFlagsAll). + Group(` + pw.product_id, p.name, f.name, + pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, + po.po_number, st.movement_number, lt.transfer_number, ast.id, + pi.price, p.product_price + `) + + query = r.joinSapronakProductFlag(query, "p") + return scanAndGroupDetails(query) +} + func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { if len(productIDs) == 0 { return []entity.Product{}, nil diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index 9501cfbc..4dff148b 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -363,7 +363,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj if err != nil { return nil, nil, 0, 0, err } - salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.Id) + salesOutRows, err := s.Repository.FetchSapronakSalesAllocatedDetails(ctx, pfk.Id) if err != nil { return nil, nil, 0, 0, err } @@ -570,13 +570,12 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj if existing.ProductName == "" { existing.ProductName = d.ProductName } - existing.UsageQty += d.QtyKeluar - existing.UsageValue += d.Nilai - if existing.IncomingQty >= existing.UsageQty { - existing.RemainingQty = existing.IncomingQty - existing.UsageQty - } else { - existing.RemainingQty = 0 + // Adjustment keluar should reduce stock without inflating usage-based HPP. + remaining := existing.IncomingQty - existing.UsageQty - d.QtyKeluar + if remaining < 0 { + remaining = 0 } + existing.RemainingQty = remaining itemMap[productID] = existing } } diff --git a/internal/modules/dashboards/repositories/dashboard_stats.repository.go b/internal/modules/dashboards/repositories/dashboard_stats.repository.go index 0662a0de..6645bd9f 100644 --- a/internal/modules/dashboards/repositories/dashboard_stats.repository.go +++ b/internal/modules/dashboards/repositories/dashboard_stats.repository.go @@ -107,16 +107,23 @@ func applyDashboardFilters(db *gorm.DB, filters *validation.DashboardFilter) *go func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) { var rows []RecordingWeeklyMetric + weekExpr := `CASE + WHEN r.day IS NULL OR r.day <= 0 THEN 1 + WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17 + ELSE ((r.day - 1) / 7 + 1) + END` + db := r.DB().WithContext(ctx). Table("recordings AS r"). - Select(`((r.day - 1) / 7 + 1) AS week, + Select(fmt.Sprintf(`%s AS week, COALESCE(AVG(r.hen_day), 0) AS hen_day, COALESCE(AVG(r.egg_weight), 0) AS egg_weight, COALESCE(AVG(r.feed_intake), 0) AS feed_intake, COALESCE(AVG(r.fcr_value), 0) AS fcr_value, - COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`). + COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`, weekExpr)). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). + Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.deleted_at IS NULL"). Where("r.day IS NOT NULL AND r.day > 0") @@ -188,92 +195,19 @@ func (r *DashboardRepositoryImpl) GetStandardFcrWeekly(ctx context.Context, week return nil, nil } - filterClause := "" - filterArgs := make([]interface{}, 0) - if filters != nil { - if len(filters.FlockIds) > 0 { - filterClause += " AND pf.id IN ?" - filterArgs = append(filterArgs, filters.FlockIds) - } - if len(filters.KandangIds) > 0 { - filterClause += " AND k.id IN ?" - filterArgs = append(filterArgs, filters.KandangIds) - } - if len(filters.LokasiIds) > 0 { - filterClause += " AND k.location_id IN ?" - filterArgs = append(filterArgs, filters.LokasiIds) - } + standardIDs := r.standardIDSubquery(filters) + if standardIDs == nil { + return nil, nil } - query := fmt.Sprintf(` -WITH src AS ( - SELECT DISTINCT pf.production_standard_id, pf.fcr_id - FROM project_flocks pf - JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id - JOIN kandangs k ON k.id = pfk.kandang_id - WHERE pf.production_standard_id > 0 AND pf.fcr_id > 0 - %s -), -actual AS ( - SELECT u.week AS week, - pf.fcr_id AS fcr_id, - AVG((u.chart_data->'statistics'->>'average_weight')::numeric) AS avg_weight - FROM project_flock_kandang_uniformity u - JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id - JOIN project_flocks pf ON pf.id = pfk.project_flock_id - JOIN kandangs k ON k.id = pfk.kandang_id - WHERE u.week IN ? AND u.uniform_date IS NOT NULL AND pf.fcr_id > 0 - %s - GROUP BY u.week, pf.fcr_id -), -target AS ( - SELECT sgd.week AS week, - src.fcr_id AS fcr_id, - AVG(sgd.target_mean_bw) AS target_mean_bw - FROM standard_growth_details sgd - JOIN src ON src.production_standard_id = sgd.production_standard_id - WHERE sgd.week IN ? - GROUP BY sgd.week, src.fcr_id -), -weights AS ( - SELECT COALESCE(a.week, t.week) AS week, - COALESCE(a.fcr_id, t.fcr_id) AS fcr_id, - COALESCE( - CASE WHEN a.avg_weight > 10 THEN a.avg_weight / 1000 ELSE a.avg_weight END, - CASE WHEN t.target_mean_bw > 10 THEN t.target_mean_bw / 1000 ELSE t.target_mean_bw END - ) AS weight - FROM actual a - FULL OUTER JOIN target t ON t.week = a.week AND t.fcr_id = a.fcr_id -) -SELECT w.week AS week, - COALESCE(AVG( - COALESCE( - (SELECT fs.fcr_number - FROM fcr_standards fs - WHERE fs.fcr_id = w.fcr_id - AND fs.weight >= w.weight - ORDER BY fs.weight ASC - LIMIT 1), - (SELECT fs.fcr_number - FROM fcr_standards fs - WHERE fs.fcr_id = w.fcr_id - ORDER BY fs.weight DESC - LIMIT 1) - ) - ), 0) AS std_fcr -FROM weights w -GROUP BY w.week -ORDER BY w.week ASC -`, filterClause, filterClause) - - args := make([]interface{}, 0, len(filterArgs)*2+2) - args = append(args, filterArgs...) - args = append(args, weeks) - args = append(args, filterArgs...) - args = append(args, weeks) - var rows []StandardWeeklyFcrMetric - if err := r.DB().WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil { + db := r.DB().WithContext(ctx). + Table("production_standard_details AS psd"). + Select("psd.week AS week, COALESCE(AVG(psd.standard_fcr), 0) AS std_fcr"). + Where("psd.week IN ?", weeks). + Where("psd.production_standard_id IN (?)", standardIDs) + + if err := db.Group("psd.week").Order("psd.week ASC").Scan(&rows).Error; err != nil { return nil, err } @@ -635,21 +569,29 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyUniformityMetrics(ctx conte func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error) { var rows []EggQualityWeeklyMetric + weekExpr := `CASE + WHEN r.day IS NULL OR r.day <= 0 THEN 1 + WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17 + ELSE ((r.day - 1) / 7 + 1) + END` + db := r.DB().WithContext(ctx). Table("recording_eggs AS re"). - Select(` - ((r.day - 1) / 7 + 1) AS week, + Select(fmt.Sprintf(` + %s AS week, COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty, COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty, COALESCE(SUM(re.qty), 0) AS total_qty`, + weekExpr, utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah, - ). + )). Joins("JOIN recordings AS r ON r.id = re.recording_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). + Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Joins("JOIN product_warehouses AS pw ON pw.id = re.product_warehouse_id"). Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). @@ -670,14 +612,21 @@ func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error) { var rows []WeeklyEggWeightMetric + weekExpr := `CASE + WHEN r.day IS NULL OR r.day <= 0 THEN 1 + WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17 + ELSE ((r.day - 1) / 7 + 1) + END` + db := r.DB().WithContext(ctx). Table("recording_eggs AS re"). - Select(` - ((r.day - 1) / 7 + 1) AS week, - COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`). + Select(fmt.Sprintf(` + %s AS week, + COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`, weekExpr)). Joins("JOIN recordings AS r ON r.id = re.recording_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). + Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.deleted_at IS NULL"). Where("r.day IS NOT NULL AND r.day > 0") @@ -694,15 +643,22 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error) { var rows []WeeklyFeedUsageMetric + weekExpr := `CASE + WHEN r.day IS NULL OR r.day <= 0 THEN 1 + WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17 + ELSE ((r.day - 1) / 7 + 1) + END` + db := r.DB().WithContext(ctx). Table("recording_stocks AS rs"). - Select(` - ((r.day - 1) / 7 + 1) AS week, + Select(fmt.Sprintf(` + %s AS week, COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty, - LOWER(uoms.name) AS uom_name`). + LOWER(uoms.name) AS uom_name`, weekExpr)). Joins("JOIN recordings AS r ON r.id = rs.recording_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). + Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN uoms ON uoms.id = p.uom_id"). diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 7a63d5da..c5537b53 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -21,7 +21,6 @@ import ( rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording" "github.com/go-playground/validator/v10" @@ -40,14 +39,6 @@ type RecordingService interface { Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) } -type RecordingFIFOIntegrationService interface { - ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error - ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error -} - -var recordingStockUsableKey = fifo.UsableKeyRecordingStock -var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion - type recordingService struct { Log *logrus.Logger Validate *validator.Validate @@ -89,21 +80,6 @@ func NewRecordingService( } } -func NewRecordingFIFOIntegrationService( - repo repository.RecordingRepository, - productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, - fifoSvc commonSvc.FifoService, - stockLogRepo rStockLogs.StockLogRepository, -) RecordingFIFOIntegrationService { - return &recordingService{ - Log: utils.Log, - Repository: repo, - ProductWarehouseRepo: productWarehouseRepo, - FifoSvc: fifoSvc, - StockLogRepo: stockLogRepo, - } -} - func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -347,7 +323,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } var warehouseDeltas map[uint]float64 - warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) + if s.FifoSvc != nil { + // FIFO replenish already adjusts egg warehouse quantities. + warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, nil) + } else { + warehouseDeltas = buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs) + } if err := s.adjustProductWarehouseQuantities(ctx, tx, warehouseDeltas); err != nil { s.Log.Errorf("Failed to adjust product warehouses: %+v", err) return err @@ -529,39 +510,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := ensureRecordingEggsUnused(existingEggs); err != nil { return err } - if s.StockLogRepo != nil { - note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) - logs := make([]*entity.StockLog, 0, len(existingEggs)) - for _, egg := range existingEggs { - if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { - continue - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1) - if err != nil { - s.Log.Errorf("Failed to get stock logs: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - latestStockLog := &entity.StockLog{} - if len(stockLogs) > 0 { - latestStockLog = stockLogs[0] - } else { - latestStockLog.Stock = 0 - } - logs = append(logs, &entity.StockLog{ - ProductWarehouseId: egg.ProductWarehouseId, - CreatedBy: actorID, - Decrease: float64(egg.Qty), - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: recordingEntity.Id, - Notes: note, - Stock: latestStockLog.Stock - float64(egg.Qty), - }) - } - if len(logs) > 0 { - if err := s.StockLogRepo.WithTx(tx).CreateMany(ctx, logs, nil); err != nil { - return err - } - } + note := fmt.Sprintf("Recording-Edit#%d", recordingEntity.Id) + if err := s.logRecordingEggUsage(ctx, tx, existingEggs, note, actorID); err != nil { + return err } if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, nil)); err != nil { s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) @@ -818,40 +769,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { }) } -func (s *recordingService) logRecordingEggRollback( - ctx context.Context, - tx *gorm.DB, - eggs []entity.RecordingEgg, - note string, - actorID uint, -) error { - if len(eggs) == 0 || s.StockLogRepo == nil { - return nil - } - if strings.TrimSpace(note) == "" || actorID == 0 { - return nil - } - - for _, egg := range eggs { - if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { - continue - } - log := &entity.StockLog{ - ProductWarehouseId: egg.ProductWarehouseId, - CreatedBy: actorID, - Decrease: float64(egg.Qty), - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: egg.RecordingId, - Notes: note, - } - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - - return nil -} - // === Persistence Helpers === func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error { @@ -891,381 +808,6 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v return nil } -func (s *recordingService) consumeRecordingStocks( - ctx context.Context, - tx *gorm.DB, - stocks []entity.RecordingStock, - note string, - actorID uint, -) error { - if len(stocks) == 0 || s.FifoSvc == nil { - return nil - } - if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { - return errors.New("stock log repository is not available") - } - - for _, stock := range stocks { - if stock.Id == 0 { - continue - } - - var desired float64 - if stock.UsageQty != nil { - desired = *stock.UsageQty - } - var pending float64 - if stock.PendingQty != nil { - pending = *stock.PendingQty - } - desiredTotal := desired + pending - - result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ - UsableKey: recordingStockUsableKey, - UsableID: stock.Id, - ProductWarehouseID: stock.ProductWarehouseId, - Quantity: desiredTotal, - AllowPending: true, - Tx: tx, - }) - if err != nil { - s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err) - return err - } - - if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil { - return err - } - - logDecrease := result.UsageQuantity - if result.PendingQuantity > 0 { - logDecrease += result.PendingQuantity - } - if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - log := &entity.StockLog{ - ProductWarehouseId: stock.ProductWarehouseId, - CreatedBy: actorID, - Decrease: logDecrease, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: stock.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock -= log.Decrease - } else { - log.Stock -= log.Decrease - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - } - - return nil -} - -func (s *recordingService) consumeRecordingDepletions( - ctx context.Context, - tx *gorm.DB, - depletions []entity.RecordingDepletion, - note string, - actorID uint, -) error { - if len(depletions) == 0 || s.FifoSvc == nil { - return nil - } - if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { - return errors.New("stock log repository is not available") - } - - for _, depletion := range depletions { - if depletion.Id == 0 { - continue - } - - sourceWarehouseID := uint(0) - if depletion.SourceProductWarehouseId != nil { - sourceWarehouseID = *depletion.SourceProductWarehouseId - } - if sourceWarehouseID == 0 { - return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") - } - - desired := depletion.Qty + depletion.PendingQty - result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ - UsableKey: recordingDepletionUsableKey, - UsableID: depletion.Id, - ProductWarehouseID: sourceWarehouseID, - Quantity: desired, - AllowPending: false, - Tx: tx, - }) - if err != nil { - s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err) - return err - } - - if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil { - return err - } - - logDecrease := result.UsageQuantity - if result.PendingQuantity > 0 { - logDecrease += result.PendingQuantity - } - if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - log := &entity.StockLog{ - ProductWarehouseId: sourceWarehouseID, - CreatedBy: actorID, - Decrease: logDecrease, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: depletion.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock -= log.Decrease - } else { - log.Stock -= log.Decrease - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - - destDelta := depletion.Qty + depletion.PendingQty - if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - if depletion.ProductWarehouseId == sourceWarehouseID { - continue - } - log := &entity.StockLog{ - ProductWarehouseId: depletion.ProductWarehouseId, - CreatedBy: actorID, - Increase: destDelta, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: depletion.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock += log.Increase - } else { - log.Stock += log.Increase - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - } - - return nil -} - -func (s *recordingService) ConsumeRecordingStocks( - ctx context.Context, - tx *gorm.DB, - stocks []entity.RecordingStock, - note string, - actorID uint, -) error { - return s.consumeRecordingStocks(ctx, tx, stocks, note, actorID) -} - -func (s *recordingService) releaseRecordingStocks( - ctx context.Context, - tx *gorm.DB, - stocks []entity.RecordingStock, - note string, - actorID uint, -) error { - if len(stocks) == 0 || s.FifoSvc == nil { - return nil - } - if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { - return errors.New("stock log repository is not available") - } - - for _, stock := range stocks { - if stock.Id == 0 { - continue - } - - if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ - UsableKey: recordingStockUsableKey, - UsableID: stock.Id, - Tx: tx, - }); err != nil { - s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err) - return err - } - - if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil { - return err - } - - if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - log := &entity.StockLog{ - ProductWarehouseId: stock.ProductWarehouseId, - CreatedBy: actorID, - Increase: *stock.UsageQty, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: stock.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock += log.Increase - } else { - log.Stock += log.Increase - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - } - - return nil -} - -func (s *recordingService) releaseRecordingDepletions( - ctx context.Context, - tx *gorm.DB, - depletions []entity.RecordingDepletion, - note string, - actorID uint, -) error { - if len(depletions) == 0 || s.FifoSvc == nil { - return nil - } - if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { - return errors.New("stock log repository is not available") - } - - for _, depletion := range depletions { - if depletion.Id == 0 { - continue - } - - sourceWarehouseID := uint(0) - if depletion.SourceProductWarehouseId != nil { - sourceWarehouseID = *depletion.SourceProductWarehouseId - } - if sourceWarehouseID == 0 { - return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") - } - - if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ - UsableKey: recordingDepletionUsableKey, - UsableID: depletion.Id, - Tx: tx, - }); err != nil { - s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err) - return err - } - - if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil { - return err - } - - logIncrease := depletion.Qty - if depletion.PendingQty > 0 { - logIncrease += depletion.PendingQty - } - if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - log := &entity.StockLog{ - ProductWarehouseId: sourceWarehouseID, - CreatedBy: actorID, - Increase: logIncrease, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: depletion.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock += log.Increase - } else { - log.Stock += log.Increase - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - - destDelta := depletion.Qty + depletion.PendingQty - if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { - if depletion.ProductWarehouseId == sourceWarehouseID { - continue - } - log := &entity.StockLog{ - ProductWarehouseId: depletion.ProductWarehouseId, - CreatedBy: actorID, - Decrease: destDelta, - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: depletion.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock -= log.Decrease - } else { - log.Stock -= log.Decrease - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - } - - return nil -} - -func (s *recordingService) ReleaseRecordingStocks( - ctx context.Context, - tx *gorm.DB, - stocks []entity.RecordingStock, - note string, - actorID uint, -) error { - return s.releaseRecordingStocks(ctx, tx, stocks, note, actorID) -} - func (s *recordingService) resolvePopulationWarehouseID(ctx context.Context, projectFlockKandangID uint) (uint, error) { if projectFlockKandangID == 0 { return 0, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") @@ -1356,212 +898,6 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context, return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx }) } -func (s *recordingService) replenishRecordingEggs( - ctx context.Context, - tx *gorm.DB, - eggs []entity.RecordingEgg, - note string, - actorID uint, -) error { - if len(eggs) == 0 || s.FifoSvc == nil { - return nil - } - if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { - return errors.New("stock log repository is not available") - } - - for _, egg := range eggs { - if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 { - continue - } - if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyRecordingEgg, - StockableID: egg.Id, - ProductWarehouseID: egg.ProductWarehouseId, - Quantity: float64(egg.Qty), - Tx: tx, - }); err != nil { - s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err) - return err - } - - if strings.TrimSpace(note) != "" && actorID != 0 { - log := &entity.StockLog{ - ProductWarehouseId: egg.ProductWarehouseId, - CreatedBy: actorID, - Increase: float64(egg.Qty), - LoggableType: string(utils.StockLogTypeRecording), - LoggableId: egg.RecordingId, - Notes: note, - } - stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") - } - if len(stockLogs) > 0 { - latestStockLog := stockLogs[0] - log.Stock = latestStockLog.Stock - log.Stock += log.Increase - } else { - log.Stock += log.Increase - } - - if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { - return err - } - } - } - - return nil -} - -type desiredStock struct { - Usage float64 - Pending float64 -} - -type desiredDepletion struct { - Qty float64 - Pending float64 -} - -func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock { - desired := make([]desiredStock, len(stocks)) - for i := range stocks { - if stocks[i].UsageQty != nil { - desired[i].Usage = *stocks[i].UsageQty - } - if stocks[i].PendingQty != nil { - desired[i].Pending = *stocks[i].PendingQty - } - if !enabled { - continue - } - zero := 0.0 - stocks[i].UsageQty = &zero - stocks[i].PendingQty = &zero - } - return desired -} - -func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) { - if !enabled { - return - } - for i := range stocks { - if i >= len(desired) { - break - } - usage := desired[i].Usage - pending := desired[i].Pending - stocks[i].UsageQty = &usage - stocks[i].PendingQty = &pending - } -} - -func resetDepletionQuantitiesForFIFO(depletions []entity.RecordingDepletion, enabled bool) []desiredDepletion { - desired := make([]desiredDepletion, len(depletions)) - for i := range depletions { - desired[i].Qty = depletions[i].Qty - desired[i].Pending = depletions[i].PendingQty - if !enabled { - continue - } - depletions[i].Qty = 0 - depletions[i].PendingQty = 0 - } - return desired -} - -func applyDepletionDesiredQuantities(depletions []entity.RecordingDepletion, desired []desiredDepletion, enabled bool) { - if !enabled { - return - } - for i := range depletions { - if i >= len(desired) { - break - } - depletions[i].Qty = desired[i].Qty - depletions[i].PendingQty = desired[i].Pending - } -} - -func (s *recordingService) syncRecordingStocks( - ctx context.Context, - tx *gorm.DB, - recordingID uint, - existing []entity.RecordingStock, - incoming []validation.Stock, - note string, - actorID uint, -) error { - if s.FifoSvc == nil { - if err := s.Repository.DeleteStocks(tx, recordingID); err != nil { - return err - } - mapped := recordingutil.MapStocks(recordingID, incoming) - return s.Repository.CreateStocks(tx, mapped) - } - - existingByWarehouse := make(map[uint][]entity.RecordingStock) - for _, stock := range existing { - existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock) - } - - stocksToConsume := make([]entity.RecordingStock, 0, len(incoming)) - for _, item := range incoming { - list := existingByWarehouse[item.ProductWarehouseId] - var stock entity.RecordingStock - if len(list) > 0 { - stock = list[0] - existingByWarehouse[item.ProductWarehouseId] = list[1:] - } else { - zero := 0.0 - stock = entity.RecordingStock{ - RecordingId: recordingID, - ProductWarehouseId: item.ProductWarehouseId, - UsageQty: &zero, - PendingQty: &zero, - } - if err := tx.Create(&stock).Error; err != nil { - return err - } - } - - desired := item.Qty - stock.UsageQty = &desired - zero := 0.0 - stock.PendingQty = &zero - stocksToConsume = append(stocksToConsume, stock) - } - - var leftovers []entity.RecordingStock - for _, list := range existingByWarehouse { - leftovers = append(leftovers, list...) - } - if len(leftovers) > 0 { - if err := s.releaseRecordingStocks(ctx, tx, leftovers, note, actorID); err != nil { - return err - } - ids := make([]uint, 0, len(leftovers)) - for _, stock := range leftovers { - if stock.Id != 0 { - ids = append(ids, stock.Id) - } - } - if len(ids) > 0 { - if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil { - return err - } - } - } - - if len(stocksToConsume) == 0 { - return nil - } - return s.consumeRecordingStocks(ctx, tx, stocksToConsume, note, actorID) -} - type eggTotals struct { Qty int Weight float64 @@ -1999,16 +1335,17 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e var standard productionStandardValues var standardFcr *float64 - if category == string(utils.ProjectFlockCategoryLaying) { - detail, err := standardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - if detail != nil { - standard.HenDay = detail.TargetHenDayProduction - standard.HenHouse = detail.TargetHenHouseProduction - standard.EggWeight = detail.TargetEggWeight - standard.EggMass = detail.TargetEggMass + detail, err := standardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if detail != nil { + standard.HenDay = detail.TargetHenDayProduction + standard.HenHouse = detail.TargetHenHouseProduction + standard.EggWeight = detail.TargetEggWeight + standard.EggMass = detail.TargetEggMass + if detail.StandardFCR != nil { + standardFcr = detail.StandardFCR } } @@ -2019,21 +1356,6 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e if growthDetail != nil { standard.FeedIntake = growthDetail.FeedIntake standard.MaxDepletion = growthDetail.MaxDepletion - if category == string(utils.ProjectFlockCategoryLaying) && growthDetail.TargetMeanBw != nil && item.ProjectFlockKandang.ProjectFlock.FcrId > 0 { - targetWeight := *growthDetail.TargetMeanBw - if targetWeight > 10 { - targetWeight = targetWeight / 1000 - } - if targetWeight > 0 { - fcrStd, ok, err := s.Repository.GetFcrStandardNumber(db, item.ProjectFlockKandang.ProjectFlock.FcrId, targetWeight) - if err != nil { - return err - } - if ok { - standardFcr = &fcrStd - } - } - } } item.StandardHenDay = standard.HenDay diff --git a/internal/modules/production/recordings/services/recording_fifo.service.go b/internal/modules/production/recordings/services/recording_fifo.service.go new file mode 100644 index 00000000..375b75ce --- /dev/null +++ b/internal/modules/production/recordings/services/recording_fifo.service.go @@ -0,0 +1,703 @@ +package service + +import ( + "context" + "errors" + "strings" + + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" + recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type RecordingFIFOIntegrationService interface { + ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error + ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock, note string, actorID uint) error +} + +var recordingStockUsableKey = fifo.UsableKeyRecordingStock +var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion + +func NewRecordingFIFOIntegrationService( + repo repository.RecordingRepository, + productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, + fifoSvc commonSvc.FifoService, + stockLogRepo rStockLogs.StockLogRepository, +) RecordingFIFOIntegrationService { + return &recordingService{ + Log: utils.Log, + Repository: repo, + ProductWarehouseRepo: productWarehouseRepo, + FifoSvc: fifoSvc, + StockLogRepo: stockLogRepo, + } +} + +func (s *recordingService) consumeRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + if len(stocks) == 0 || s.FifoSvc == nil { + return nil + } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } + + for _, stock := range stocks { + if stock.Id == 0 { + continue + } + + var desired float64 + if stock.UsageQty != nil { + desired = *stock.UsageQty + } + var pending float64 + if stock.PendingQty != nil { + pending = *stock.PendingQty + } + desiredTotal := desired + pending + + result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ + UsableKey: recordingStockUsableKey, + UsableID: stock.Id, + ProductWarehouseID: stock.ProductWarehouseId, + Quantity: desiredTotal, + AllowPending: true, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err) + return err + } + + if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil { + return err + } + + logDecrease := result.UsageQuantity + if result.PendingQuantity > 0 { + logDecrease += result.PendingQuantity + } + if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: stock.ProductWarehouseId, + CreatedBy: actorID, + Decrease: logDecrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: stock.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock -= log.Decrease + } else { + log.Stock -= log.Decrease + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + } + + return nil +} + +func (s *recordingService) consumeRecordingDepletions( + ctx context.Context, + tx *gorm.DB, + depletions []entity.RecordingDepletion, + note string, + actorID uint, +) error { + if len(depletions) == 0 || s.FifoSvc == nil { + return nil + } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } + + for _, depletion := range depletions { + if depletion.Id == 0 { + continue + } + + sourceWarehouseID := uint(0) + if depletion.SourceProductWarehouseId != nil { + sourceWarehouseID = *depletion.SourceProductWarehouseId + } + if sourceWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") + } + + desired := depletion.Qty + depletion.PendingQty + result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ + UsableKey: recordingDepletionUsableKey, + UsableID: depletion.Id, + ProductWarehouseID: sourceWarehouseID, + Quantity: desired, + AllowPending: false, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err) + return err + } + + if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil { + return err + } + + logDecrease := result.UsageQuantity + if result.PendingQuantity > 0 { + logDecrease += result.PendingQuantity + } + if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: sourceWarehouseID, + CreatedBy: actorID, + Decrease: logDecrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock -= log.Decrease + } else { + log.Stock -= log.Decrease + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + + destDelta := depletion.Qty + depletion.PendingQty + if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + if depletion.ProductWarehouseId == sourceWarehouseID { + continue + } + log := &entity.StockLog{ + ProductWarehouseId: depletion.ProductWarehouseId, + CreatedBy: actorID, + Increase: destDelta, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock += log.Increase + } else { + log.Stock += log.Increase + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + } + + return nil +} + +func (s *recordingService) ConsumeRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + return s.consumeRecordingStocks(ctx, tx, stocks, note, actorID) +} + +func (s *recordingService) releaseRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + if len(stocks) == 0 || s.FifoSvc == nil { + return nil + } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } + + for _, stock := range stocks { + if stock.Id == 0 { + continue + } + if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ + UsableKey: recordingStockUsableKey, + UsableID: stock.Id, + Tx: tx, + }); err != nil { + s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err) + return err + } + + if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil { + return err + } + + if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: stock.ProductWarehouseId, + CreatedBy: actorID, + Increase: *stock.UsageQty, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: stock.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock += log.Increase + } else { + log.Stock += log.Increase + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + } + + return nil +} + +func (s *recordingService) releaseRecordingDepletions( + ctx context.Context, + tx *gorm.DB, + depletions []entity.RecordingDepletion, + note string, + actorID uint, +) error { + if len(depletions) == 0 || s.FifoSvc == nil { + return nil + } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } + + for _, depletion := range depletions { + if depletion.Id == 0 { + continue + } + + sourceWarehouseID := uint(0) + if depletion.SourceProductWarehouseId != nil { + sourceWarehouseID = *depletion.SourceProductWarehouseId + } + if sourceWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") + } + if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ + UsableKey: recordingDepletionUsableKey, + UsableID: depletion.Id, + Tx: tx, + }); err != nil { + s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err) + return err + } + + if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil { + return err + } + + logIncrease := depletion.Qty + if depletion.PendingQty > 0 { + logIncrease += depletion.PendingQty + } + if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: sourceWarehouseID, + CreatedBy: actorID, + Increase: logIncrease, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock += log.Increase + } else { + log.Stock += log.Increase + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + + destDelta := depletion.Qty + depletion.PendingQty + if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { + if depletion.ProductWarehouseId == sourceWarehouseID { + continue + } + log := &entity.StockLog{ + ProductWarehouseId: depletion.ProductWarehouseId, + CreatedBy: actorID, + Decrease: destDelta, + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: depletion.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock -= log.Decrease + } else { + log.Stock -= log.Decrease + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + } + + return nil +} + +func (s *recordingService) ReleaseRecordingStocks( + ctx context.Context, + tx *gorm.DB, + stocks []entity.RecordingStock, + note string, + actorID uint, +) error { + return s.releaseRecordingStocks(ctx, tx, stocks, note, actorID) +} + +func (s *recordingService) logRecordingEggUsage( + ctx context.Context, + tx *gorm.DB, + eggs []entity.RecordingEgg, + note string, + actorID uint, +) error { + if len(eggs) == 0 || s.StockLogRepo == nil { + return nil + } + if strings.TrimSpace(note) == "" || actorID == 0 { + return nil + } + + logs := make([]*entity.StockLog, 0, len(eggs)) + for _, egg := range eggs { + if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { + continue + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1) + if err != nil { + s.Log.Errorf("Failed to get stock logs: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + latestStockLog := &entity.StockLog{} + if len(stockLogs) > 0 { + latestStockLog = stockLogs[0] + } else { + latestStockLog.Stock = 0 + } + logs = append(logs, &entity.StockLog{ + ProductWarehouseId: egg.ProductWarehouseId, + CreatedBy: actorID, + Decrease: float64(egg.Qty), + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: egg.RecordingId, + Notes: note, + Stock: latestStockLog.Stock - float64(egg.Qty), + }) + } + if len(logs) == 0 { + return nil + } + + return s.StockLogRepo.WithTx(tx).CreateMany(ctx, logs, nil) +} + +func (s *recordingService) logRecordingEggRollback( + ctx context.Context, + tx *gorm.DB, + eggs []entity.RecordingEgg, + note string, + actorID uint, +) error { + if len(eggs) == 0 || s.StockLogRepo == nil { + return nil + } + if strings.TrimSpace(note) == "" || actorID == 0 { + return nil + } + + for _, egg := range eggs { + if egg.ProductWarehouseId == 0 || egg.Qty <= 0 { + continue + } + log := &entity.StockLog{ + ProductWarehouseId: egg.ProductWarehouseId, + CreatedBy: actorID, + Decrease: float64(egg.Qty), + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: egg.RecordingId, + Notes: note, + } + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + + return nil +} + +func (s *recordingService) replenishRecordingEggs( + ctx context.Context, + tx *gorm.DB, + eggs []entity.RecordingEgg, + note string, + actorID uint, +) error { + if len(eggs) == 0 || s.FifoSvc == nil { + return nil + } + if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { + return errors.New("stock log repository is not available") + } + + for _, egg := range eggs { + if egg.Id == 0 || egg.ProductWarehouseId == 0 || egg.Qty <= 0 { + continue + } + if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyRecordingEgg, + StockableID: egg.Id, + ProductWarehouseID: egg.ProductWarehouseId, + Quantity: float64(egg.Qty), + Tx: tx, + }); err != nil { + s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err) + return err + } + + if strings.TrimSpace(note) != "" && actorID != 0 { + log := &entity.StockLog{ + ProductWarehouseId: egg.ProductWarehouseId, + CreatedBy: actorID, + Increase: float64(egg.Qty), + LoggableType: string(utils.StockLogTypeRecording), + LoggableId: egg.RecordingId, + Notes: note, + } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, egg.ProductWarehouseId, 1) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") + } + if len(stockLogs) > 0 { + latestStockLog := stockLogs[0] + log.Stock = latestStockLog.Stock + log.Stock += log.Increase + } else { + log.Stock += log.Increase + } + + if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, log, nil); err != nil { + return err + } + } + } + + return nil +} + +type desiredStock struct { + Usage float64 + Pending float64 +} + +type desiredDepletion struct { + Qty float64 + Pending float64 +} + +func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock { + desired := make([]desiredStock, len(stocks)) + for i := range stocks { + if stocks[i].UsageQty != nil { + desired[i].Usage = *stocks[i].UsageQty + } + if stocks[i].PendingQty != nil { + desired[i].Pending = *stocks[i].PendingQty + } + if !enabled { + continue + } + zero := 0.0 + stocks[i].UsageQty = &zero + stocks[i].PendingQty = &zero + } + return desired +} + +func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) { + if !enabled { + return + } + for i := range stocks { + if i >= len(desired) { + break + } + usage := desired[i].Usage + pending := desired[i].Pending + stocks[i].UsageQty = &usage + stocks[i].PendingQty = &pending + } +} + +func resetDepletionQuantitiesForFIFO(depletions []entity.RecordingDepletion, enabled bool) []desiredDepletion { + desired := make([]desiredDepletion, len(depletions)) + for i := range depletions { + desired[i].Qty = depletions[i].Qty + desired[i].Pending = depletions[i].PendingQty + if !enabled { + continue + } + depletions[i].Qty = 0 + depletions[i].PendingQty = 0 + } + return desired +} + +func applyDepletionDesiredQuantities(depletions []entity.RecordingDepletion, desired []desiredDepletion, enabled bool) { + if !enabled { + return + } + for i := range depletions { + if i >= len(desired) { + break + } + depletions[i].Qty = desired[i].Qty + depletions[i].PendingQty = desired[i].Pending + } +} + +func (s *recordingService) syncRecordingStocks( + ctx context.Context, + tx *gorm.DB, + recordingID uint, + existing []entity.RecordingStock, + incoming []validation.Stock, + note string, + actorID uint, +) error { + if s.FifoSvc == nil { + if err := s.Repository.DeleteStocks(tx, recordingID); err != nil { + return err + } + mapped := recordingutil.MapStocks(recordingID, incoming) + return s.Repository.CreateStocks(tx, mapped) + } + + existingByWarehouse := make(map[uint][]entity.RecordingStock) + for _, stock := range existing { + existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock) + } + + stocksToConsume := make([]entity.RecordingStock, 0, len(incoming)) + for _, item := range incoming { + list := existingByWarehouse[item.ProductWarehouseId] + var stock entity.RecordingStock + if len(list) > 0 { + stock = list[0] + existingByWarehouse[item.ProductWarehouseId] = list[1:] + } else { + zero := 0.0 + stock = entity.RecordingStock{ + RecordingId: recordingID, + ProductWarehouseId: item.ProductWarehouseId, + UsageQty: &zero, + PendingQty: &zero, + } + if err := tx.Create(&stock).Error; err != nil { + return err + } + } + + desired := item.Qty + stock.UsageQty = &desired + zero := 0.0 + stock.PendingQty = &zero + stocksToConsume = append(stocksToConsume, stock) + } + + var leftovers []entity.RecordingStock + for _, list := range existingByWarehouse { + leftovers = append(leftovers, list...) + } + if len(leftovers) > 0 { + if err := s.releaseRecordingStocks(ctx, tx, leftovers, note, actorID); err != nil { + return err + } + ids := make([]uint, 0, len(leftovers)) + for _, stock := range leftovers { + if stock.Id != 0 { + ids = append(ids, stock.Id) + } + } + if len(ids) > 0 { + if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil { + return err + } + } + } + + if len(stocksToConsume) == 0 { + return nil + } + return s.consumeRecordingStocks(ctx, tx, stocksToConsume, note, actorID) +} From f4790e56ea51c72d42c19bfb02bbcca23bdb40af Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 2 Feb 2026 14:25:37 +0700 Subject: [PATCH 07/17] [FEAT/BE] update herautics for closing counting sapronak --- .../modules/closings/repositories/closing.repository.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index d37ae6c1..827d0f9a 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -1124,8 +1124,8 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka pw.product_id AS product_id, p.name AS product_name, f.name AS flag, - COALESCE(pi.received_date, st.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date, - COALESCE(po.po_number, st.movement_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference, + COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date, + COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference, 0 AS qty_in, COALESCE(SUM(sa.qty), 0) AS qty_out, COALESCE(p.product_price, 0) AS price @@ -1135,6 +1135,8 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id"). Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). + Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id"). Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()). Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). @@ -1145,7 +1147,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka Where("sa.status = ?", entity.StockAllocationStatusActive). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). - Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") + Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoing, err := scanAndGroupDetails(outgoingQuery) if err != nil { From 6e0ff557a815ade38f0fcfc71a1d9fe361839683 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 2 Feb 2026 14:59:29 +0700 Subject: [PATCH 08/17] [FEAT/BE]delete adjustment use in doc closing counting sapronak --- internal/modules/closings/repositories/closing.repository.go | 1 + .../dashboards/repositories/dashboard_stats.repository.go | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 827d0f9a..cd5ce2da 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -1147,6 +1147,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka Where("sa.status = ?", entity.StockAllocationStatusActive). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). + Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)). Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price") outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoing, err := scanAndGroupDetails(outgoingQuery) diff --git a/internal/modules/dashboards/repositories/dashboard_stats.repository.go b/internal/modules/dashboards/repositories/dashboard_stats.repository.go index 6645bd9f..3c04f9a0 100644 --- a/internal/modules/dashboards/repositories/dashboard_stats.repository.go +++ b/internal/modules/dashboards/repositories/dashboard_stats.repository.go @@ -581,13 +581,12 @@ func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context %s AS week, COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty, COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty, - COALESCE(SUM(re.qty), 0) AS total_qty`, - weekExpr, + COALESCE(SUM(re.qty), 0) AS total_qty`, weekExpr), utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah, - )). + ). Joins("JOIN recordings AS r ON r.id = re.recording_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). From 96627e964f33d5efd4d4d32e4394bce2d6900981 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 2 Feb 2026 15:23:15 +0700 Subject: [PATCH 09/17] [FEAT/BE]Purchase rejected payload --- internal/modules/purchases/validations/purchase.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index 564cc96f..6f5d3013 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -51,7 +51,7 @@ type ReceivePurchaseItemRequest struct { type ReceivePurchaseRequest struct { Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"` - Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"` + Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"omitempty,dive"` TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"` Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"` } From ce108da847b163b7f6e6b2d29395c47146cc8914 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 15:36:06 +0700 Subject: [PATCH 10/17] FEAT[BE] :enhance production data calculations by adding TotalBirdSold and refining profit/loss metrics --- .../services/closingKeuangan.service.go | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/internal/modules/closings/services/closingKeuangan.service.go b/internal/modules/closings/services/closingKeuangan.service.go index ca76c67e..804ca023 100644 --- a/internal/modules/closings/services/closingKeuangan.service.go +++ b/internal/modules/closings/services/closingKeuangan.service.go @@ -41,6 +41,7 @@ type ProductionData struct { TotalWeightProduced float64 TotalEggWeightKg float64 TotalWeightSold float64 + TotalBirdSold float64 TotalSalesAmount float64 } @@ -283,6 +284,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo continue } data.TotalWeightSold += delivery.TotalWeight + data.TotalBirdSold += delivery.UsageQty data.TotalSalesAmount += delivery.TotalPrice } @@ -383,46 +385,77 @@ func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *enti func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection { - totalPopulationIn := production.TotalPopulationIn totalWeightProduced := production.TotalWeightProduced totalEggWeightKg := production.TotalEggWeightKg totalSalesAmount := production.TotalSalesAmount totalWeightSold := production.TotalWeightSold + totalBirdSold := production.TotalBirdSold + actualPopulation := production.TotalPopulationIn - production.TotalDepletion - weightForSales := totalWeightSold - weightForCalculation := totalWeightProduced - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - weightForSales = totalWeightSold - weightForCalculation = totalEggWeightKg - } + isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying) - calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { - if totalPopulationIn > 0 { - rpPerBird = amount / totalPopulationIn - } - if weightForSales > 0 { - rpPerKg = amount / weightForSales + // Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual + calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if isLaying { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + } else { + if totalBirdSold > 0 { + rpPerBird = amount / totalBirdSold + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } } return } - actualPopulation := production.TotalPopulationIn - production.TotalDepletion - - calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + // Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi + calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { if actualPopulation > 0 { rpPerBird = amount / actualPopulation } - if weightForCalculation > 0 { - rpPerKg = amount / weightForCalculation + if isLaying { + if totalEggWeightKg > 0 { + rpPerKg = amount / totalEggWeightKg + } + } else { + if totalWeightProduced > 0 { + rpPerKg = amount / totalWeightProduced + } + } + return + } + + // Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual + calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) { + if isLaying { + if actualPopulation > 0 { + rpPerBird = amount / actualPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + } else { + if totalBirdSold > 0 { + rpPerBird = amount / totalBirdSold + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } } return } plItems := []dto.ProfitLossItem{} - salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount) + salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount) salesLabel := "Penjualan Ayam" - if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + if isLaying { salesLabel = "Penjualan Telur" } plItems = append(plItems, dto.ToProfitLossItem( @@ -435,23 +468,23 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj )) totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost - _, sapronakRpPerKg := calculateMetrics(totalSapronakAmount) sapronakRpPerBird := 0.0 + sapronakRpPerKg := 0.0 for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} { - rpPerBird, _ := calculateMetrics(amount) + rpPerBird, rpPerKg := calculateCostMetrics(amount) sapronakRpPerBird += rpPerBird + sapronakRpPerKg += rpPerKg } - sapronakLabel := "Pengeluaran Sapronak" plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeSapronak), - sapronakLabel, + "Pengeluaran Sapronak", "purchase", sapronakRpPerBird, sapronakRpPerKg, totalSapronakAmount, )) - overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational) + overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeOverhead), "Overhead", @@ -461,7 +494,7 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj costs.RealizationOperational, )) - ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost) plItems = append(plItems, dto.ToProfitLossItem( string(dto.PLCodeEkspedisi), "Ekspedisi", From 1c1f2f03aaf12b4b300f9bf6acafc8a7b3c9eaef Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 16:46:33 +0700 Subject: [PATCH 11/17] FIX[BE] :remove unused product warehouse repository import and streamline stock consumption logic in consumeDeliveryStock method --- .../services/deliveryorder.service.go | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 51e37465..a5eaf856 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -10,7 +10,6 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" - productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" @@ -502,69 +501,24 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor Tx: tx, }) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err)) + } + deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) - totalConsumed := 0.0 - var fifoConsumed float64 - var directConsumed float64 - - if result != nil && result.UsageQuantity > 0 { - fifoConsumed = result.UsageQuantity - totalConsumed = result.UsageQuantity - } - - if err != nil || (totalConsumed < requestedQty) { - remainder := requestedQty - totalConsumed - - pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) - pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) - if err2 != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock") - } - - if pw == nil || pw.Quantity < remainder { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. FIFO: %.2f, Direct Available: %.2f, Total Needed: %.2f", func() float64 { - if pw != nil { - return pw.Quantity - } else { - return 0 - } - }(), remainder, requestedQty)) - } - - if err := pwRepo.AdjustQuantities(ctx, map[uint]float64{ - marketingProduct.ProductWarehouseId: -remainder, - }, func(db *gorm.DB) *gorm.DB { - return tx - }); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to adjust product warehouse quantity") - } - - directConsumed = remainder - totalConsumed += remainder - } - - if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, totalConsumed, 0); err != nil { + if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - if actorID > 0 && totalConsumed > 0 { - notes := "" - if fifoConsumed > 0 && directConsumed > 0 { - notes = fmt.Sprintf("Partial FIFO (%.2f) + Direct (%.2f)", fifoConsumed, directConsumed) - } else if fifoConsumed > 0 { - notes = fmt.Sprintf("FIFO stock only (%.2f)", fifoConsumed) - } else if directConsumed > 0 { - notes = fmt.Sprintf("Direct stock only (%.2f)", directConsumed) - } - + if actorID > 0 && result.UsageQuantity > 0 { decreaseLog := &entity.StockLog{ - Decrease: totalConsumed, + Decrease: result.UsageQuantity, LoggableType: string(utils.StockLogTypeMarketing), LoggableId: deliveryProduct.Id, ProductWarehouseId: marketingProduct.ProductWarehouseId, CreatedBy: actorID, - Notes: notes, + Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { @@ -572,8 +526,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] - decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Stock -= decreaseLog.Decrease + decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease } else { decreaseLog.Stock -= decreaseLog.Decrease } From e406b20ca7e41e70f1fceb8019dfcfeaa408441d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 21:11:27 +0700 Subject: [PATCH 12/17] FEAT[BE] :Fixing fifo stock when marketing deleted --- internal/modules/marketing/module.go | 2 +- .../services/deliveryorder.service.go | 13 +-- .../marketing/services/salesorder.service.go | 107 +++++++++++++----- 3 files changed, 83 insertions(+), 39 deletions(-) diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go index 2f8ea4fb..2dde163f 100644 --- a/internal/modules/marketing/module.go +++ b/internal/modules/marketing/module.go @@ -64,7 +64,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) - salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate) + salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index a5eaf856..1d0a9481 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -563,6 +563,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor return err } + if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { + return err + } + if actorID > 0 && currentUsage > 0 { increaseLog := &entity.StockLog{ Increase: currentUsage, @@ -570,7 +574,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor LoggableId: deliveryProduct.Id, ProductWarehouseId: marketingProduct.ProductWarehouseId, CreatedBy: actorID, - Notes: "", + Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { @@ -578,8 +582,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] - increaseLog.Stock = latestStockLog.Stock - increaseLog.Stock += increaseLog.Increase + increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase } else { increaseLog.Stock += increaseLog.Increase } @@ -587,9 +590,5 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil) } - if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { - return err - } - return nil } diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index 9d950307..df75fe82 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -19,6 +19,7 @@ import ( userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -41,11 +42,12 @@ type salesOrdersService struct { ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService + FifoSvc commonSvc.FifoService WarehouseRepo warehouseRepo.WarehouseRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } -func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository, +func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, @@ -55,6 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome ProductWarehouseRepo: productWarehouseRepo, UserRepo: userRepo, ApprovalSvc: approvalSvc, + FifoSvc: fifoSvc, WarehouseRepo: warehouseRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, } @@ -230,14 +233,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } if len(req.MarketingProducts) > 0 { - for _, item := range req.MarketingProducts { - if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { - return nil, err - } - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, - ); err != nil { - return nil, err + for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err } } } @@ -333,6 +336,32 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u totalPrice = totalWeight * rp.UnitPrice } + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + } + + if err == nil && deliveryProduct.Id != 0 { + oldQty := old.Qty + newQty := rp.Qty + qtyDiff := newQty - oldQty + + if qtyDiff < 0 { + return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.") + } else if qtyDiff > 0 { + _, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: deliveryProduct.Id, + ProductWarehouseID: rp.ProductWarehouseId, + Quantity: qtyDiff, + Tx: dbTransaction, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err)) + } + } + } + updateBody := map[string]any{ "product_warehouse_id": rp.ProductWarehouseId, "qty": rp.Qty, @@ -345,25 +374,20 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") } - if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - mdp := &entity.MarketingDeliveryProduct{ - MarketingProductId: old.Id, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: rp.VehicleNumber, - UsageQty: 0, - PendingQty: 0, - } - if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") - } - } else { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + if deliveryProduct.Id == 0 { + mdp := &entity.MarketingDeliveryProduct{ + MarketingProductId: old.Id, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + UsageQty: 0, + PendingQty: 0, + } + if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") } } } else { @@ -380,10 +404,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") } - if err == nil { + if err == nil && deliveryProduct.Id != 0 { - if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) + if deliveryProduct.DeliveryDate != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id)) + } + + if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: deliveryProduct.Id, + Tx: dbTransaction, + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err)) } if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { @@ -459,6 +491,19 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) if len(marketing.Products) > 0 { + deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id) + if err == nil && len(deliveryProducts) > 0 { + for _, dp := range deliveryProducts { + if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ + UsableKey: fifo.UsableKeyMarketingDelivery, + UsableID: dp.Id, + Tx: dbTransaction, + }); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err)) + } + } + } + for _, product := range marketing.Products { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("marketing_product_id = ?", product.Id).Unscoped() From 58ae03a090af42a3aa9b6013f4c8927d8fe94735 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 2 Feb 2026 21:45:18 +0700 Subject: [PATCH 13/17] FIX[BE] :remove unnecessary quantity calculations in Adjustment method to streamline stock adjustment logic --- .../inventory/adjustments/services/adjustment.service.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index ceefcb1e..862d6991 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -160,7 +160,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse") } - afterQuantity := productWarehouse.Quantity newLog := &entity.StockLog{ LoggableType: string(utils.StockLogTypeAdjustment), LoggableId: 0, @@ -183,14 +182,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } if transactionType == string(utils.StockLogTransactionTypeIncrease) { - afterQuantity += req.Quantity newLog.Increase = req.Quantity newLog.Stock += newLog.Increase } else { if productWarehouse.Quantity < req.Quantity { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity)) } - afterQuantity -= req.Quantity newLog.Decrease = req.Quantity newLog.Stock -= newLog.Decrease } @@ -243,12 +240,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } } - productWarehouse.Quantity = afterQuantity - if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return err - } - createdAdjustmentStockId = adjustmentStock.Id return nil }) From 9a328ae1e44d914ad0774f8c91278f80fa39dded Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 08:06:52 +0700 Subject: [PATCH 14/17] FEAT[BE] :implement proportional distribution with rounding for stock allocation in transfer laying approval process --- .../services/deliveryorder.service.go | 1 + .../services/transfer_laying.service.go | 85 ++++++++++++------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 1d0a9481..6d9392a6 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -520,6 +520,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor CreatedBy: actorID, Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity), } + stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index e6e9a862..15351e56 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "strings" "time" @@ -743,7 +744,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - + stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction) @@ -817,6 +818,27 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") } + targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare) + + for i, target := range targets { + roundedQty := math.Round(targetShares[i]) + if roundedQty <= 0 { + continue + } + mappingAllocation := &entity.StockAllocation{ + StockableType: fifo.UsableKeyTransferToLayingOut.String(), + StockableId: source.Id, + UsableType: fifo.StockableKeyTransferToLayingIn.String(), + UsableId: target.Id, + ProductWarehouseId: *source.ProductWarehouseId, + Qty: roundedQty, + Status: entity.StockAllocationStatusActive, + } + if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target") + } + } + stockLogDecrease := &entity.StockLog{ ProductWarehouseId: *source.ProductWarehouseId, CreatedBy: actorID, @@ -937,36 +959,6 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi return err } -func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { - - productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx) - - existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) - if err == nil && existing != nil { - - if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil { - return nil, err - } - return existing, nil - } - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - - newWarehouse := &entity.ProductWarehouse{ - ProductId: productID, - WarehouseId: warehouseID, - ProjectFlockKandangId: projectFlockKandangId, - Quantity: quantity, - } - - if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { - return nil, err - } - - return newWarehouse, nil -} - func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { @@ -1060,3 +1052,34 @@ func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFl return kandangMaxTargetQty, nil } + +func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 { + if len(targets) == 0 { + return []float64{} + } + + targetShares := make([]float64, len(targets)) + totalRounded := 0.0 + + for i, target := range targets { + targetShares[i] = (target.TotalQty / totalTargetQty) * sourceShare + totalRounded += math.Round(targetShares[i]) + } + + diff := sourceShare - totalRounded + + if diff != 0 { + maxIdx := 0 + maxDecimal := 0.0 + for i, share := range targetShares { + decimal := share - math.Round(share) + if decimal > maxDecimal { + maxDecimal = decimal + maxIdx = i + } + } + targetShares[maxIdx] += diff + } + + return targetShares +} From 82f0db107afae96b1c4dc39321a156315f0a0ee8 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 3 Feb 2026 11:17:11 +0700 Subject: [PATCH 15/17] [FEAT/BE]Fix create avaible qty --- .../recordings/services/recording.service.go | 40 ++++++++++++++----- internal/utils/recording/util.recording.go | 20 ++++++++-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index c5537b53..29f9cffc 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1073,19 +1073,37 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var remainingChick float64 if totalChick > 0 { totalChickFloat := float64(totalChick) - remainingChick = totalChickFloat - cumDepletionQty - if remainingChick < 0 { - remainingChick = 0 - } - updates["total_chick_qty"] = remainingChick - recording.TotalChickQty = &remainingChick + if s.FifoSvc != nil { + // totalChick already represents available qty (total_qty - total_used_qty). + remainingChick = totalChickFloat + updates["total_chick_qty"] = remainingChick + recording.TotalChickQty = &remainingChick - cumRate := 0.0 - if totalChickFloat > 0 { - cumRate = (cumDepletionQty / totalChickFloat) * 100 + baseChick := initialChickin + if baseChick <= 0 { + baseChick = totalChickFloat + cumDepletionQty + } + cumRate := 0.0 + if baseChick > 0 { + cumRate = (cumDepletionQty / baseChick) * 100 + } + updates["cum_depletion_rate"] = cumRate + recording.CumDepletionRate = &cumRate + } else { + remainingChick = totalChickFloat - cumDepletionQty + if remainingChick < 0 { + remainingChick = 0 + } + updates["total_chick_qty"] = remainingChick + recording.TotalChickQty = &remainingChick + + cumRate := 0.0 + if totalChickFloat > 0 { + cumRate = (cumDepletionQty / totalChickFloat) * 100 + } + updates["cum_depletion_rate"] = cumRate + recording.CumDepletionRate = &cumRate } - updates["cum_depletion_rate"] = cumRate - recording.CumDepletionRate = &cumRate } else { updates["total_chick_qty"] = gorm.Expr("NULL") updates["cum_depletion_rate"] = gorm.Expr("NULL") diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index f40818bf..2b146f5f 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -28,12 +28,26 @@ func MapDepletions(recordingID uint, items []validation.Depletion) []entity.Reco return nil } - result := make([]entity.RecordingDepletion, 0, len(items)) + aggregate := make(map[uint]float64, len(items)) for _, item := range items { + if item.ProductWarehouseId == 0 || item.Qty == 0 { + continue + } + aggregate[item.ProductWarehouseId] += item.Qty + } + if len(aggregate) == 0 { + return nil + } + + result := make([]entity.RecordingDepletion, 0, len(aggregate)) + for warehouseID, qty := range aggregate { + if qty == 0 { + continue + } result = append(result, entity.RecordingDepletion{ RecordingId: recordingID, - ProductWarehouseId: item.ProductWarehouseId, - Qty: item.Qty, + ProductWarehouseId: warehouseID, + Qty: qty, }) } return result From f75225b81b2d1404c30dd03879f994c1c8be607a Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 12:02:45 +0700 Subject: [PATCH 16/17] FEAT[BE] :add production standard detail creation for growing project category with zero target values --- .../services/production-standard.service.go | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/internal/modules/master/production-standards/services/production-standard.service.go b/internal/modules/master/production-standards/services/production-standard.service.go index 2ea95cf3..c2841708 100644 --- a/internal/modules/master/production-standards/services/production-standard.service.go +++ b/internal/modules/master/production-standards/services/production-standard.service.go @@ -152,6 +152,23 @@ func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Crea if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) } + } else if req.ProjectCategory == string(utils.ProjectFlockCategoryGrowing) { + if detailReq.ProductionStandardDetails != nil && detailReq.ProductionStandardDetails.StandardFCR != nil { + var zero float64 = 0 + productionStandardDetail := &entity.ProductionStandardDetail{ + ProductionStandardId: newStandard.Id, + Week: detailReq.Week, + TargetHenDayProduction: &zero, + TargetHenHouseProduction: &zero, + TargetEggWeight: &zero, + TargetEggMass: &zero, + StandardFCR: detailReq.ProductionStandardDetails.StandardFCR, + } + + if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { + return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) + } + } } standardGrowthDetail := &entity.StandardGrowthDetail{ @@ -265,6 +282,23 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) } + } else if projectCategory == "GROWING" { + if detailReq.ProductionStandardDetails != nil && detailReq.ProductionStandardDetails.StandardFCR != nil { + var zero float64 = 0 + productionStandardDetail := &entity.ProductionStandardDetail{ + ProductionStandardId: id, + Week: detailReq.Week, + TargetHenDayProduction: &zero, + TargetHenHouseProduction: &zero, + TargetEggWeight: &zero, + TargetEggMass: &zero, + StandardFCR: detailReq.ProductionStandardDetails.StandardFCR, + } + + if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { + return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) + } + } } standardGrowthDetail := &entity.StandardGrowthDetail{ From 4eacdd543ae20dc95397c43ba44b81bc747a00dc Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 3 Feb 2026 12:08:11 +0700 Subject: [PATCH 17/17] [FEAT/BE]Fix add new response depletions_rate --- internal/entities/recording.go | 2 + .../recordings/dto/recording.dto.go | 18 ++- .../repositories/recording.repository.go | 18 +++ .../recordings/services/recording.service.go | 104 ++++++++++++++++-- 4 files changed, 134 insertions(+), 8 deletions(-) diff --git a/internal/entities/recording.go b/internal/entities/recording.go index 0cc5dc03..1ca3deb2 100644 --- a/internal/entities/recording.go +++ b/internal/entities/recording.go @@ -12,7 +12,9 @@ type Recording struct { RecordDatetime time.Time `gorm:"column:record_datetime;not null"` Day *int `gorm:"column:day"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"` + TotalDepletionCumQty *float64 `gorm:"-"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` + DepletionRate *float64 `gorm:"-"` CumIntake *int `gorm:"column:cum_intake"` FcrValue *float64 `gorm:"column:fcr_value"` TotalChickQty *float64 `gorm:"column:total_chick_qty"` diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 0fa14e97..191b9676 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -1,6 +1,7 @@ package dto import ( + "math" "strings" "time" @@ -73,7 +74,9 @@ type RecordingRelationDTO struct { RecordDatetime time.Time `json:"record_datetime"` Day int `json:"day"` TotalDepletionQty float64 `json:"total_depletion_qty"` + TotalDepletionCumQty float64 `json:"total_depletion_cum_qty"` CumDepletionRate float64 `json:"cum_depletion_rate"` + DepletionRate float64 `json:"depletion_rate"` CumIntake int `json:"cum_intake"` FcrValue float64 `json:"fcr_value"` HenDay float64 `json:"hen_day"` @@ -230,7 +233,9 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { RecordDatetime: e.RecordDatetime, Day: intValue(e.Day), TotalDepletionQty: floatValue(e.TotalDepletionQty), - CumDepletionRate: floatValue(e.CumDepletionRate), + TotalDepletionCumQty: floatValue(e.TotalDepletionCumQty), + CumDepletionRate: roundFloatValue(e.CumDepletionRate, 2), + DepletionRate: roundFloatValue(e.DepletionRate, 2), CumIntake: intValue(e.CumIntake), FcrValue: floatValue(e.FcrValue), HenDay: floatValue(e.HenDay), @@ -426,6 +431,17 @@ func floatValue(value *float64) float64 { return *value } +func roundFloatValue(value *float64, places int) float64 { + if value == nil { + return 0 + } + if places <= 0 { + return math.Round(*value) + } + factor := math.Pow(10, float64(places)) + return math.Round(*value*factor) / factor +} + func intValue(value *int) int { if value == nil { return 0 diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 27c399f4..ce4dc0df 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -39,6 +39,7 @@ type RecordingRepository interface { ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) + GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) @@ -314,6 +315,23 @@ func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingI return result, nil } +func (r *RecordingRepositoryImpl) GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) { + if projectFlockKandangId == 0 || recordTime.IsZero() { + return 0, nil + } + + var total float64 + err := tx. + Table("recording_depletions rd"). + Select("COALESCE(SUM(rd.qty),0)"). + Joins("JOIN recordings r ON r.id = rd.recording_id"). + Where("r.project_flock_kandangs_id = ?", projectFlockKandangId). + Where("r.record_datetime <= ?", recordTime). + Where("r.deleted_at IS NULL"). + Scan(&total).Error + return total, err +} + func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) { if currentDay <= 1 { return nil, nil diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 29f9cffc..28329041 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -128,6 +128,12 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if err := s.attachProductionStandards(c.Context(), recordings); err != nil { return nil, 0, err } + if err := s.attachCumulativeDepletions(c.Context(), recordings); err != nil { + return nil, 0, err + } + if err := s.attachDepletionRates(c.Context(), recordings); err != nil { + return nil, 0, err + } return recordings, total, nil } @@ -152,6 +158,12 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro if err := s.attachProductionStandard(c.Context(), recording); err != nil { return nil, err } + if err := s.attachCumulativeDepletion(c.Context(), recording); err != nil { + return nil, err + } + if err := s.attachDepletionRate(c.Context(), recording); err != nil { + return nil, err + } return recording, nil } @@ -1026,12 +1038,8 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm return fmt.Errorf("getPreviousRecording: %w", err) } - var prevCumDepletionQty float64 var prevCumIntake float64 if prevRecording != nil { - if prevRecording.TotalDepletionQty != nil { - prevCumDepletionQty = *prevRecording.TotalDepletionQty - } if prevRecording.CumIntake != nil { prevCumIntake = float64(*prevRecording.CumIntake) } @@ -1063,12 +1071,16 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm } currentDepletion := float64(totalDepletionQty) - cumDepletionQty := prevCumDepletionQty + currentDepletion + cumDepletionQty, err := s.Repository.GetCumulativeDepletionByProjectFlockKandangUntil(tx, recording.ProjectFlockKandangId, recording.RecordDatetime) + if err != nil { + return fmt.Errorf("getCumulativeDepletionByProjectFlockKandangUntil: %w", err) + } updates := map[string]any{ - "total_depletion_qty": cumDepletionQty, + "total_depletion_qty": currentDepletion, } - recording.TotalDepletionQty = &cumDepletionQty + recording.TotalDepletionQty = ¤tDepletion + recording.TotalDepletionCumQty = &cumDepletionQty var remainingChick float64 if totalChick > 0 { @@ -1111,6 +1123,9 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm recording.CumDepletionRate = nil } + depletionRate := computeDepletionRate(prevRecording, currentDepletion, totalChick) + recording.DepletionRate = &depletionRate + var feedIntake float64 if remainingChick > 0 && usageInGrams > 0 { feedIntake = (usageInGrams / remainingChick) * 1000 @@ -1201,6 +1216,81 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm return nil } +func computeDepletionRate(prevRecording *entity.Recording, currentDepletion float64, totalChick int64) float64 { + base := 0.0 + if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 { + base = *prevRecording.TotalChickQty + } else if totalChick > 0 { + // totalChick is already remaining after today's depletion; add back current to approximate previous population. + base = float64(totalChick) + currentDepletion + } + if base <= 0 { + return 0 + } + return (currentDepletion / base) * 100 +} + +func (s *recordingService) attachCumulativeDepletion(ctx context.Context, recording *entity.Recording) error { + if recording == nil || recording.ProjectFlockKandangId == 0 || recording.RecordDatetime.IsZero() { + return nil + } + total, err := s.Repository.GetCumulativeDepletionByProjectFlockKandangUntil(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId, recording.RecordDatetime) + if err != nil { + return err + } + recording.TotalDepletionCumQty = &total + return nil +} + +func (s *recordingService) attachCumulativeDepletions(ctx context.Context, recordings []entity.Recording) error { + if len(recordings) == 0 { + return nil + } + for i := range recordings { + if err := s.attachCumulativeDepletion(ctx, &recordings[i]); err != nil { + return err + } + } + return nil +} + +func (s *recordingService) attachDepletionRate(ctx context.Context, recording *entity.Recording) error { + if recording == nil { + return nil + } + current := 0.0 + if recording.TotalDepletionQty != nil { + current = *recording.TotalDepletionQty + } + day := 0 + if recording.Day != nil { + day = *recording.Day + } + prev, err := s.Repository.FindPreviousRecording(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId, day) + if err != nil { + return err + } + totalChick, err := s.Repository.GetTotalChick(s.Repository.DB().WithContext(ctx), recording.ProjectFlockKandangId) + if err != nil { + return err + } + rate := computeDepletionRate(prev, current, totalChick) + recording.DepletionRate = &rate + return nil +} + +func (s *recordingService) attachDepletionRates(ctx context.Context, recordings []entity.Recording) error { + if len(recordings) == 0 { + return nil + } + for i := range recordings { + if err := s.attachDepletionRate(ctx, &recordings[i]); err != nil { + return err + } + } + return nil +} + func (s *recordingService) createRecordingApproval( ctx context.Context, db *gorm.DB,