From 74d9ab6d11393489987a3ca84ed29b422881acc2 Mon Sep 17 00:00:00 2001 From: Jacob Felknor Date: Fri, 22 May 2026 01:08:40 -0600 Subject: [PATCH] Transfer Order (#11281) * initial skel commit for transfer orders * initial transfer order backend model * add some serializers, rename PLACED to ISSUED for TransferOrders * adding from admin console works * simple table list almost working, but we need to add order line items.... * add other cols to table * add Transfer Order from table view * moving towards a detail view * wip: adding detail view * add take from and destination serializer details * add other detail grid items * edit/duplicate transfer order * more action buttons * first crack at adding line items * add to line item * add filters * starting work on row actions * more action buttons for line items * fix copy lines in duplicate * basic allocation works * allocations table actions * allocate serials * allocated serial row expansion * add transferred qty to serializers * move items on complete, show in tracking * change panel to transferred stock upon complete * allow incomplete line items * disable edit allocations when completed * add ref pattern and to settings * add admin to line item inline * add calendar and parametric view * basic transfer order report * add transfer order ruleset * starting allocation buisness logic throughout for TOs * disable accept incomplete logic, which was incorrect, until I fix * fix incomplete allocation option * add transferred col to default report * add transfer order to calendar ics view * chain condition for readability * add transfer order allocations table to stockitem view * don't account TO allocations in availability * add transfer orders table for a part * 'consume' option by doing take_stock * squash migrations * starting to test transfer order * more transfer order tests * add transfer order consume test * wip, more tests * more transfer order tests * had to refresh_from_db * switch "to" to "transfer-order" in url paths * only select non-virtual parts from transfer order * add transfer order docs * deconflict migrations * fix frontend build error * fix validation on transfer order reference pattern * add oath2 scope for transfer order * fix state test to include transfer order state * add barcode_model_type_code for transfer order * bump api version * check view role for transfer order, remove debug/commented out lines * add serialized allocation test * Fix migrations * Frontend fixes * Implement required 'company' attribute * transfer order report context * attempt to fix tests * delete transfer order allocations on cancel * add a few playwright tests, more incoming * more playwright * add source and destination locations to table * deconflict migrations * Fix build issue * attempt to fix flaky transfer order test * duplicate transfer order before running tests * Adjust playwright tests * Fix migration dependency order --------- Co-authored-by: Oliver Co-authored-by: Matthias Mair --- docs/docs/api/schema.md | 2 +- .../images/stock/transfer_order_calendar.png | Bin 0 -> 44399 bytes .../images/stock/transfer_order_display.png | Bin 0 -> 89501 bytes .../images/stock/transfer_order_list.png | Bin 0 -> 56809 bytes docs/docs/concepts/custom_states.md | 1 + docs/docs/settings/global.md | 4 + docs/docs/stock/transfer_order.md | 150 ++++ docs/mkdocs.yml | 1 + .../InvenTree/InvenTree/api_version.py | 5 +- .../InvenTree/common/setting/system.py | 20 + src/backend/InvenTree/generic/states/tests.py | 6 +- src/backend/InvenTree/order/admin.py | 38 + src/backend/InvenTree/order/api.py | 625 ++++++++++++- src/backend/InvenTree/order/events.py | 9 + .../order/fixtures/transfer_order.yaml | 68 ++ .../order/migrations/0118_transferorder.py | 482 ++++++++++ .../0119_transferorderlineitem_line_int.py | 18 + src/backend/InvenTree/order/models.py | 695 +++++++++++++- src/backend/InvenTree/order/serializers.py | 620 +++++++++++++ src/backend/InvenTree/order/status_codes.py | 27 + src/backend/InvenTree/order/test_api.py | 850 +++++++++++++++++- src/backend/InvenTree/order/validators.py | 21 + src/backend/InvenTree/part/filters.py | 40 +- src/backend/InvenTree/part/models.py | 47 +- src/backend/InvenTree/part/serializers.py | 7 + src/backend/InvenTree/report/apps.py | 7 + .../inventree_transfer_order_report.html | 52 ++ src/backend/InvenTree/stock/api.py | 16 +- src/backend/InvenTree/stock/models.py | 53 +- src/backend/InvenTree/stock/serializers.py | 6 + src/backend/InvenTree/users/oauth2_scopes.py | 1 + src/backend/InvenTree/users/ruleset.py | 7 + src/frontend/lib/enums/ApiEndpoints.tsx | 11 + src/frontend/lib/enums/ModelInformation.tsx | 16 + src/frontend/lib/enums/ModelType.tsx | 2 + src/frontend/lib/enums/Roles.tsx | 3 + .../src/components/render/Instance.tsx | 6 +- src/frontend/src/components/render/Order.tsx | 43 + src/frontend/src/defaults/backendMappings.tsx | 2 + src/frontend/src/forms/TransferOrderForms.tsx | 308 +++++++ src/frontend/src/functions/icons.tsx | 3 + .../pages/Index/Settings/SystemSettings.tsx | 15 + src/frontend/src/pages/part/PartDetail.tsx | 16 + .../src/pages/stock/LocationDetail.tsx | 48 +- src/frontend/src/pages/stock/StockDetail.tsx | 36 +- .../src/pages/stock/TransferOrderDetail.tsx | 552 ++++++++++++ src/frontend/src/router.tsx | 5 + .../src/tables/stock/StockTrackingTable.tsx | 14 +- .../stock/TransferOrderAllocationTable.tsx | 285 ++++++ .../stock/TransferOrderLineItemTable.tsx | 529 +++++++++++ .../stock/TransferOrderParametricTable.tsx | 48 + .../src/tables/stock/TransferOrderTable.tsx | 185 ++++ src/frontend/tests/pages/pui_stock.spec.ts | 208 +++++ 53 files changed, 6178 insertions(+), 35 deletions(-) create mode 100644 docs/docs/assets/images/stock/transfer_order_calendar.png create mode 100644 docs/docs/assets/images/stock/transfer_order_display.png create mode 100644 docs/docs/assets/images/stock/transfer_order_list.png create mode 100644 docs/docs/stock/transfer_order.md create mode 100644 src/backend/InvenTree/order/fixtures/transfer_order.yaml create mode 100644 src/backend/InvenTree/order/migrations/0118_transferorder.py create mode 100644 src/backend/InvenTree/order/migrations/0119_transferorderlineitem_line_int.py create mode 100644 src/backend/InvenTree/report/templates/report/inventree_transfer_order_report.html create mode 100644 src/frontend/src/forms/TransferOrderForms.tsx create mode 100644 src/frontend/src/pages/stock/TransferOrderDetail.tsx create mode 100644 src/frontend/src/tables/stock/TransferOrderAllocationTable.tsx create mode 100644 src/frontend/src/tables/stock/TransferOrderLineItemTable.tsx create mode 100644 src/frontend/src/tables/stock/TransferOrderParametricTable.tsx create mode 100644 src/frontend/src/tables/stock/TransferOrderTable.tsx diff --git a/docs/docs/api/schema.md b/docs/docs/api/schema.md index ba610a0773..49c88f3a46 100644 --- a/docs/docs/api/schema.md +++ b/docs/docs/api/schema.md @@ -7,7 +7,7 @@ The API schema as documented below is generated using the [drf-spectactular](htt ## API Version -This documentation is for API version: `449` +This documentation is for API version: `459` !!! tip "API Schema History" We track API schema changes, and provide a snapshot of each API schema version in the [API schema repository](https://github.com/inventree/schema/). diff --git a/docs/docs/assets/images/stock/transfer_order_calendar.png b/docs/docs/assets/images/stock/transfer_order_calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..b04ccb30a58f437b71335b934842ff9a542b5300 GIT binary patch literal 44399 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 literal 0 HcmV?d00001 diff --git a/docs/docs/assets/images/stock/transfer_order_display.png b/docs/docs/assets/images/stock/transfer_order_display.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e0bcb335f1de15ed50797d6d212a6855f7d4e3 GIT binary patch literal 89501 zcmb4qWmsHE(|@_ec7NR; z_dGBREoI$RU8kxm@Qb_z3L*g_3=9m4l%%K<49uG=7?@XiZ(qH%r0X*0y!=71mDF^A zfkEy1`}eOHI#fa!7*ZH1Q6UwV9h3%C#zMcKy|VvS zm2yG?D=Lb$gg19^cY0=el5z)p3!1?4Xqs z*B@TWD}`Q&TJL6^yo=(Ixx)v!rUb+#%^Pq7)a1#o8U{P_>a=x-6N1}Pc4Wmo;mq7F z&wtu7{FzPIxa=YE&KKYWUz*H2(bUuxh)szhWtGy9Up&wylajKOHMIXQfT<1 zu6w!oJ!%;WUCl!H(a&7Q_8ZDKt7<_Yx>&_+_1fo|T9*A(ek=_C4~p5D4!H8V5n-N2 zAq`_=B}oS@HwA5jLu#yX9xYh*_*cr3y947X&ja{udkJ2xw2qsE8o5n6Cii8l!7^$Ots9ZtX$Xdify>WRio0PU(b?}uCnsycHZlM z@YyO09(1QEXh2Md4d#=AjYJ)9eYZ{#iUCNvN=-e$KpelKLutQ;!=K$sdJ~}2ubd!1 z;Wfq}GIc#w+h%WS9{w`t;SfAgle#rZi)rF4*@9>q? z*vK6yTPd&-cw^pr95)!dCLkc%bx{NF)a`xoV}g#q(S3;$L`GJaV+~{+*!PNX#0(eY za{G~&7m%0NJ1U`Y=PIeIv4a>g^gnvP3I5d~yT~>S*wN~u(iI8A4eSl>Y7x{DenrVf zEo}^f=BL9-&@Zwv>^#-GAwXBUhfrf*-^y4wPH!t+PX+Lzp~lgBR6b3#J$YMB%3nOa z8k}vwQO7muak=+0Fs`4Rm=YV}USy%lyaYjp0cWWrOUg-7!8@PZsoa8<6}Y3(khB$)opm!>>EH zsokpVcc5TBuMOff!ZUe;ZI)vWf~_mUJQ~m3oMh`YTN*acXl0D62dhu3xk_P_aPIfM z)@+XL8IC96s-N_v8rbMSH!WbLTljgZjwkrKD1y!#k}{VkmghCv9RW}S8VT=p0_8Y> zs4Zo-M>XyuiyHaboxPW%q~<$~;cS3~)=H&eY1xwInPhW{URrKTg}$MOhWaz4g$OjW z^kiBR2aq7Huu6P<8_8QmERZO%-E+h-cPRC7FzggGTRc=H<&*O0hJk(tT|Qe=2vKrx zs>tmgkpK(g6#bM{k)h55$&V_vunOp(2#AsW%M!sKe4dbz=wEa26-7vnk+a1y6M ze>n}Ne;E;f#8>;hC>>3Ch^blg=4X-F`Au3?3P;X0*<;+ z!E?ns#kq}%K4A9p9Q0mS_v1Of+1!Q_8I*635jP`ZbEl*-RA`7hWtV?~k6Yfw8K}EC z-q@G*vx;}PqayR>QT9w|zUOnGbrVtWH{M{Y85+){1g)xvsgvGj_@&9=XBfftNhNaa_^b-p z_EZSAVNKy~23Z^iui{oT)ynV5ysTFmpp$92@j8PTwfMaDG?wbbq2osjZ?C9P*(_g~ zf+v$e2`iWN!TOH~D>v>>d|sW!%*zj#C?E8!?$){rUv;W|R84s(v3x||6sI`tF?3BZ z>t}Hw=JbkGXLj8wvrcl_BO=k1eOE_=c-z=UlTCXC_(~Kgmj0SV z)|SZeygQy|x=-`S8FAZ@t6NapMl-4M6Gt^BcCYs%MhoYs=Ps_p!U|g5(oXf5V$^k^ z2;h)m15gG=xjr1kj<3n}>IVw^m8tM;>j_Bd8aR>cG_lMSC04%lT+Bc4jW@*VoQTZc zJ`(K={G;jXC%uQ|tUPs2br4yjl2gM&W@4IJsV->0`V4dOUa!_yh;3uRCB8@UmwMg5MAAB{fV$^xw%hVt;%Oh>V7)7rDP~s z&MsB3aBV=o{$ z-cSJM5m5{?`*oPuJX)M3N01x;IxIKS65~bvVC$xfK6~BzwHWl}wlckzq)deU5nXhH zI$#7rg^!hH3xJQlu>-0l&8Y$?to*1VpXBn`O0`i7FGX_@7A7Gacy?+y{*g+!TAPj! z1Jk}Dj~&c=qXpSLaWFPlMY=ddzA=m)i7i>7@Hk5`o@&8R1-K7)J8W_yp=$!sy~q{0 z&YS#p&AqeeEUk*H#5_sgDK16EKI>U}7Rf!hNmFD7{>91H5vV04lyNslGZ3=>;`qbys@!|N~`=#UGJAZBTX zeU`)yp+V~DsW`atV7}W1T&nG_=kldq7^y8_I1N&+I0o=WhXRU&Ql*WbJGOA8nV#&9 z*GYB;nBt=!;pp6)8*TCeAy+7x4JHwZgYxoqVajV)yqTR;(EDpg6L)B65@w2XW=5W% z&CuOf{c^C4-_r8Ulazju(q&BOa9_jfix zWN7+3Y_nHy(64(>EPLS0qQZSwQ1q+PGxUT59Juf^<-QbbSP8%8$YMHjGBRcH`Zk4A zUb7@}>Vn5y%9LM$E|0Z28xKp^dR`q!25cy(L>8pV`S3+^v_9WA0wJ>V2r{{$O?jMj zpz9Y4-POZ)FvegFuATv^Ow~rmQ$Ki;XkP3ERqS`;`+MJhwYk}Z6}ONv(-u%^Ld)-ZzKjf8yQi zuE>Jdmavy|J4>6-VMjru8`f*HowAK_NbB%YUY!C3ClZkISQ&1Ap?%IXP(xQJ{Qnh}tIW@_rT&vKG3Lc0zy zb8r2u#|dkPFP*N!2iW&!LRF!|a%}8;nWSFHbqaIx z#zMbJCT#5RvI!V3>l}RssU4Sk8R<_v^(Z<0y$UVMe=kp|bpL$qmm_|fjPxuxC5{b_ z&m~Hy#^UnzXOxtB&m9ziXTKy+mw(v#xVVYK)I^R&dn5~RW8#ORKv1f{LPG^NH-VUXDKH9yA=qYy>T-;I*)6PY|0B zI08a;#Qk-YMGK|34M-{?imf_pSzK7UYE=82j}$B!OL#MA_M(KzEJ31E18nwnOd_Jd zQ0IJ+YDwizsb9kzPhOq2d3LL<1Q(qVD(Vb!;J`krZri&eiHh@;4}`py9Oy4j(-gI= z9gx4Lhp4h!e|ai{#EY17qX!VVVhMk&p*65lD;v{J=-`|^4dy%PvLq3grkH4L^}^o5 zr5#|pyds)#ELH<TMVpU({Pb;rbUk8J zp4YBQqVL)q%x)JfFpYy-Kle^xTx%|909S5B!smT4OA}5BN3hmlcpDiU3~DZ zQ?KQpo4$io3$WGrrv zp=5YKC(|TA%p_JPmE=nr%(;Njo&@3r#!k^?AQC{@Ix=fsPGQ)kJa0ET9CUBII}!a8v)3zx2+#t$D#jI@o_(hn!?@S2 zc_w(}x_(tcQlhzep?!!n+DqI#SIz!ZcZ8%T&l(1(Vn5G7mE%K!u0vJ)I8BZPO7S?w zvP^GqKFl+#4F+6&VuZ8fdp# zQDw*}9;&X_6v%lLs4x}|8T`oSX(W+P&u(t)As`{D^8|f^o)lDVW3*Sv={I(rf5RH` zs>1ofBwsZ~%ni2pT4^OaD=N8!reE@Qx7=;5=jxeFcziv_0-}QKzM?Ro{<^K1#dwlz z>!t$zxo`V*W@L5#oeXCj3QFio^m+dRa}J~R_(U`&U&Pgp9_!7oBu=kYr-6fh+M4jN zuV~%=3}K^uJvFDncRhJa(PpDTKN`+@><;YZQ^q|jCC`>8#*As;Qu8So_U9?hD);O( z{h&58ewRG&FY2}!WNYAHU~Dax{yH@dPzE_9+R=KtiA#7an75}%%0230 zb1lW6ZT+QvDfd;ivxeYAt#P}$6&6Ic_1HC($+5|so1A?U>9kcl+9j&^UhpXbzV~%> zY@&6wf?0u?>^JA1zDc7N24!`jQB_Ow42u{=U#>Fk!#3V1@zwJ@_KMlt#CTI?{(!05 zz?ZQa8*IC|B^MgTpnum zct^H1m7kwigGg3pmhpkaYIF+QPNB&9s-4qXM=`R&H9-G%P%{ZIT`r21PsR~Cz(p|S zf3ZFc`*TH?P{5ax?6|9DaY@{_t@fksI~go24mwiTn&aCyUZ3`qK?nS~rQr#AsukI8 zRt)OQ96wn|7F;JQ4v#FdC@V{Ts447a6L?j_ywd&Vpe1RSa|bl=i)D6I5o{3Wa6o3s;p8t7r^qo z{I)`rG=<6+(*~XC1?!;WPfH5thboI;h1Jnu8jiqFO{I;Ly2`7tj_CJfWP`95Wq|u+ z?lOzLzc2>-nD;mb6V^pCZ~7Mb8qL7cRAr%VuA(~SF4T8bP`LYS=<&LNW&zh zMC--`*iqY*wp&{CXklgpZaQ|a7%l&*Nk|*s28s4s$_{fIr3(pVFqMV6YGrYy zyk|Ahr;;$cxs-Rk>8ZPG!Nz{` z=FRtwo=0k4?SjKe%c60Yr-<})Tt?YF>`q)8!%OgOvurL10%Y`(eza742-NlhYNlM}}^*RYHhM=Sp>FQ2N(l z_Nb`pYmW-)VOWua8xwwPYkGj5-ehdQo%Iyg&HeXgq#$&1LLOPSH)2*ABrL5~%ZVC* z;kRY6fa^ojl3Xv$gUnOX3(C^S`~g#X@@ZJ|{neIYT4q~O3FnnHG%*Wzj#3^@26Ji> zj3(p9l1<|le{(|rsT#LEjjeZ1kMsQ+esqHu@2TG*B(pGao8-2Y1-t}})NYxEKiMt4 zNu^ffRw`=zQMq1_P||k7vvK99{Dx^Y(S5&Cz{0R=@~@!EWZyT2o-MsSTMnWn$@ZAI zj@%NiSGWBl1bXYza^Hf&c8J(%;Vf$OpxJHw#I>v}R&A-|Id0YBeo~$<`Gl5aBT-RK zqDu;WJTimB1e1@AlM~z%3I@ppJIqh#x_Ld|&>N2j|4Eq)B{9$fPTI~u;@v?1Ks*la%hnbX;jxnn>lzHAt8e8WvpD3T{o}ubaoz6!Zx;V@2Je#L8 z*_CI1TeC$cTnHqep7%Isu2V!$j@O=w$&RPZP^b%zNI`Vdeow=kYIyY_jr>g#6(Q$P za-AId)m?bwA!m&HDrC`Nx!rm4do_QubC1zcmV)WsVYL4|ZT(|~%<71;gz;XuQOWya znO2_kR=J}KD1Sc`enociChK@|PwZJIB7SvqP+8geXQ9zk#g!}dD3(W~WX!Yp6qOIh z^+C27?Nuay4}cTyhKfu+PCGkrIj%{+wvyCqtq{5U=O~gqc5d^pn2UYy`p5SuiEVcm zlA$M?*#H@~xcpGw*wJ8K;&Q5k%LH=m)U!ShCn253Pe|&E4Og}S`tjhC1@^D)-(Gb1 z%^Nt5gKlLlb3{aBKBxVuZ-2qQ1U8|$CG)?Qm6VmW?HURzA2P-2$$}U*hciEF`O?KP z#oEUWxVCOXiQea)H+M)94+x~P>ecC6@9tZQ&GjVzRBFw=zEr;;B*Rl{`6@G_P6z83 z68~nh8g|)}y0o5$JtSJAb`3N4FcG0ogVRsK@O-1EvI{`FkxhK0;E(~>CzR0)>*iWR^#h%*ik|`BcW7N32p8*PoRmNEFt2=giKOLT4i)n! zXu8{jx$GAlxosxNgSvLd*gGD;^M@_^wTP{ zZ}|Y-QAK$Y!8RMTo$4puU~oiSlUsQhUP=D31pGu5in0HYZtr~v+RUZpIac+P2fM3N zU2iC1*&3s2l<+v;tPNpo3lztqRjNmiy!Xx3% zj_`)x&uC+zx@y_@Xk)@xOG!FB34_xz6JxA#goHetdYoMLgJ{6|LakO?Lt7EXTo+rX zZLk2qv8>|iMwhxJRaCUteCeTH;d=AYP5gO!;4=r4{*g2P>H-a*B&}?_$Yr^)V!&VQ z06R-ii`;r3u)uZBxM@CEAAk~{3)lG(u zQogu3kRtT0Z8MF|NKPaNEq3tL17%(1w12VBFKeFg8U+%60=28k4;HfI*XK=F0UDsz zM~?u(gb4dD=zI+AgK|WCBArKa{Z;-5xy`afgw4#XM)6AQZeCNtC}5r|#31SAn>&jS z<~BKQ6Pc%9bA<9Z5k*BA`C)~DrdX`!N){^oF$?&U?1%;}>;#km$B8*htf&t__uO<8 z1eFTCg|W{AiwXL_0wIEDy0!Xa50D0}m^vPpvw``}(x;A@+-vMklK=(PD$(LHXHh2k zXZI+r{D+9R4-M(afu_yPD&LXxPzX)>hKdyi2o}vIFD9Dm)Q?*|Vd0QJ($W%e zI$Iicy{O#ZUlbL`UsTTi=ICJgemfX{d8WxvKi|8E8K3Uqr0KaqGs|;1m|wS$cCah~ zA-A%5XfkJDw#8*d8H~@>UbY(@*wi=A9^2}aD`J*FJ&(TeiP-xCwQ_G*aEK?^`izWQQy!G`x+g&iK$A92FC3LRhUghnQw?{eFb}rM`u3OI$c9-ZhAT40aSCt z`&I9oP~w7Xa~}0FM)J{YMC}UJOQD)LLd7jt5luo`_uaA!g(O4=L2aUaXHpYIM=f~z z1}fVeG5(t`pvQ{3`*qy;aZLfjD&PFe_M?zqHVNL!;qs&6R%_0*KXuS#9M_qC% z=qZWhZY6f7NG8XTMeqGc#%$D%JfZCD2xfL77Cwte_kolDc~Y|dBdi9l z+DPG!mF#`4faBoMLR3PGaY{h=)r`Nm%`mTS;ot%Rd+LN5BV;ky_a#QY(4R=|5eJS6 zjAFY^3N-!NHgYDlH)6~&Nxf%6M&SM|Cx8@A^x;FM0w~FfckPqm5ypq=tupIzS<#R! zj)PxUO_Qi})Cwd8q-3hU-k1DHi8Lf7OXVF!KveB)$MQ4(oCYncb~V&HpP=!QZeTlD z;5Euh-6%RBRQ?z{+}00Tu$oPF96*k&I3Q=L(RC~3|D=y zDAId`PJtC@@X$^)h~2=|eN7U7?gH4T9S^yMh7Rr&Ch;pbkAD%DzWN4z)Xb62sdrpx zN0_ah6rYvgJ9-P+t-T=14bYfgRMk_buCFi0^cH^|ddN`@@U!owMBj5n!=*2`= zXl4NerMFqaAR0Dur|UHbsk2kNPCZE;t?PGDt^tHNWi*_-$T5hb|@Y(X} zyljCrG)}Jm4tct=cd|Ao5h|C;cevnSK6}w!i5Pq9p8@Ggx^z>P32(I9D3&gHgzF%*Z{=OG_gz@CzKUEq z&#@>>glLTVug3oUI&*ggzmC#CbA<$7z2Ui2f6$*MiAuO3SY?;$PS;`jal21$kH%&z zI_dO_1zZ)MYnbWVKa1@C?3kJ9qUl#%|0BD17vNRF=IY4zFDkSbH8x4Yg*dxq3)SwuAC%oHgJYT6}` zcyIz7A^AWyZZT&?6hE2ckzh3tPPDT%G>|g?olRLDj}>GD&=uWp{OUaz4ydO{AZcT) zDvvIs9oAV&Mfh}!SwPbO_t;}5tqxQ>E>6ZMqAEl zTio4^cQ%oU`ReoYO%FTK!aZ--GBYzfeBgmQn@a}uB1eNDJn^5iTDOTjWD}?jMYZ+m zqH)UEf>dvX(G+fI!j~?_Y-jHS`c6p5z^3&F(GtgdYkQ1ug%#J%E_%;jPa+(Zw~BVn zPMy`n9_&O{lBDvc#wDdh{qLuNlE?99wJ+xWDQFe>n$iH9pk^XEDY;p~=3}3vMlD&D z`j4Qk5b;z9Z?mz~t}@cP^FA__k2TQakfY^cr3iDk+JoMp;QaI9YAn^!G$G;y#%vye zCv^Z{I)xvVqdvp{Y&ivw55*7}@%PIZoE(rCMI|aoPkl*g-8M&W4!!d6vN4#Fp~+UQ ziha&R1s{!7_MB`+#VFKC$0Wb7m?9D2*FP{;P8$AnMCJVrL{x@_uU(*J>h4yTmU zT%684L~KXf1+4;6Ayys;JRbSjs*)CKy#1U@$3k$O*VPhdeu$smaOMrFU1#%}&kU&F z-02l~c0ON8K0Ij~UAcEX49;+9qn(y@==KdKl+de3sbtLs$*d$ogjwat== zKJk0_OTQ#z%3}h}nkFMxG$PA)nhnY$V`#AG@zG4OkNYR%BJ6)?X{J`WN)W5}@7;9u zdPJ*x@y-fmpc0pH9J>m7<&8LM#hqUhE(U8h$+4_*)VW9nT6^qS&(#X&sJTZQS*}D_ zah1rp1BD#F(~c#*$j^0M?w-?kG#UR)NZfq|6xNh_m_Cn=^y$rsEV36Php~dQytVVl z@n$@CxWpmVQPgArofh{N)zVZ3QeIV49FwP@2*BXeCROhceohKe5->AyeW?CT_^3`o zvcpePo`@FLqkHZx+N0C3_){Vnm}AKn{)90pB{HoxU9JhGF6A#DTW<;yAi}S5K&}gC z!M3H|0SHg(D16llek$*`aJvouKmd4i=HL8E#3?C?v6PfSbq71~)KvtuXETAB zs+J-t!;r6Xd1g=)y2GT2rfoJwc*y)w?3XO%`;*)$*%XqC{#k7tI<#!y>ZvyG_sN4G zogSvJW1v2JKBC;IA~yt0c<7O4fU>3{1~o}rcfkBZ4);WdcZptAXc6J>NZ*(9zctL z4XA<~Veo^4%TgKY{RMmQz{ZjEyCiAZ8J#-nU~tMhiBPe*qomII{#Pb4gXs$n<7*jt zsQO`c-GywkFq_rZ_MBuWA-Wm)`YfS(kAJ9~S|}>2ZX=6{t=`aTtZ#Tik7sM~JbE)# zNr<*}h?^jf5$MyMe3%xxR1szY=B4P@048adRlEx4?)rP|th^Du%QYM;JCEDU-S)tx zw%JcfsnMzmDJ1dLpY%KEuY9U-G0?|tR&#xkW%XO87JA8)1$3}HideR+PRD$~(OVa0 z>%gXsxW}^ADQJ~@`(}>zj07AlWcOI=f&^-0HW8`0Dq(cw!SK;sI~HKSeodLB_Y@j+ zgO*rj?R1GniLMx)o^N&k5V^YAYSs_L#l8s*Un*y`_poWyalHC+{`7$^bLY9589J>1 z`^2O(X;)+_we%|YO@T8B8iKXJOf|!(I%ew!mcXJcH3^FroX8|9%i*Gn!PxBS(_y{u zC7)V%l4?F2R8IK!QdVnH@uDc(;Xs z8B^rqEcxQo_O-{H)t&LEn?3`rrUL!!93&|3%tiDxYN2luZ=7B8ATHKNkr?Rx(F}mb zN*t{krD3hQ@p3-%qfc_)JT5(%PW@44i;=EH)MiBjqn<$JiJTldy@c93Y;$PqhF4om zCJ~6=IH-NLbwrVx%1S2Gm9f@qDJ_ZTa+$~i*J4gh;chh}w4}UH^Wf%V9rY94hs`Hg zarK((@w=qeP~=N8f;P7c9)X8YzYapkQgou-q+x3ivioVz$=jFcZ<4D=GQ|n*;0dss zlM?}Ll?i`i^!84A|Mi(sMf^?h?P}TqdB9Q{17QS;(0-0fH2Pn5HrbS&amlL8QV$tCHJJH@DfTsvVs5Z3LcVp%8g*5+Jj zUf6>}Sz;VN&?Ad{cqlNw6pqw$z&cpp9VRFK9=9uC;qjFhp$C?il`%8~8SGZsct+mA zJaBKXwr{O_%W75bAMVB^pn+XbFeEt_sm_Q@NruZAJ8%h|)F~%StD<2ESD#WL;BIB$ zw;n*v%RLC-t75cFtUW8&^0U6bJX*XUHzY>czmPImn)w!QhWJ=}!=`}AKlY?-i4OR| zW#e49y*z4|TQJA!srP-{p#=3x?J9s8U_ zLB_KgSE)N0e}k}t0%R{sQ4bxnrmblJY_hi!_i0X1QwcmDVo)D2dN<#83nVQul+1}QGZJUNl{`#rVr ze6pl1t)xxX9=LE_+meYx=5|EB<|~AJ8s{q?`I2_;t9;L8fS+lw;x%r) zs)@i}`Ajsb!)J_tA-J8co@5>INEZ+B-C;{wp;>MckBs|xjYs-$BuHVg1L}I{-z@L1 zuymZ+w!pF3x>!x%Vq^F^$Z1i7J7GW;M}5S8swaewPO#kD*nYI9*xKm3Vs z&PK{*=-o{nc0yqdK%mH7ABEDRiDfQ0pMvt!E|Kwi>r2LM{Ypwfc8L#qkh}{$iILkw z>J=j(drBc*F*-JS>0_g&`Y71yIRu6K-i0xo zHacPY%-VO9m4qg;M~_PBG}5myds!S)XIzuKb&>da^>R1XwCd-i!L^V~g;ce(*4|F{ z<%I$9UGSzgiGNZ0tN?~A?jhdUPRX$JYjL3 z(leH4)+#dV|AhmzTaR}yb-_4U#?M5UQELcz-E&z)@5-t_venAu-)-Nq-AHAc65#5^%4Z|P^bnsgE1v~g!#e(!-+p>h$)kf!i#m}xVSKW z@X#i{%-)r^Y5Q0u&R6D1F-N|BMnXRi`07etAuw{pV2TGY{!g@6SHCGypcK`I_ojYYkPXyz>cwuD}s7a_wU&+_~FC03qhKUXuc;LUcp zy;krKQx-vjP+NFwKNRRVGxB6r-Ndk=f!SHXxkH;hF#PglT29;qL6ZVw+tSR>ebt@M z^W=D0oiYxz6ip5TGu%)hyt<4FEzKINH~i)Haw&H%x`jOTZZaDK0UJw;0)ya~+SR%6 za;9!U^yM<~+)Du1su*+io#Lc5ug^VOC{1`v==*_?_Y312GW1zb;v=OKHip^mtC72G z^iK+bEe~ByQxu5_mBQQM!u?DDW=g@R<|gi2qF@tw_&9jo=wZEH`-bM{^o-P&mcf>Q zRPNemv{Fej%trM_Xb7_X$==aS3+#^KOX~mS+4aU+-Ye;@R1Ng5ko7n)TqB979U{V7 zHrsnoGmgUhuBLLD%0w>Oa|B$zroI2*Q z2OEt`c@Kq21slFxLw@yO6Thvqs-M@{c*+&DyP_T+H}YWT&F%1wJBpe+ib>kY;JFPV zpfxTL%%VA_RtxTAM(%WWAS&>j+h+KubYba?Z+XvW3*=J$S7=w0KLmu55utA)q5euY zg@}6icc}!-H`H(c-(?T~Bl_i|v6iu6e}!8)Axj2E`@Fa@lawqosE{ts`lMg?91Sfg zdTQR|^ya))pvzw!8)hM+Or1$0?thcPWV>zu6<9d+Kk?owU80gZ9GSF?FV`du9`QE- zS}lUTd9pYFID~qt|6ttR90UKZvw-=3muNI@KJeP+MYh)D)(m4~M^>vS@~fy66}aDo z(H0gp_ILktfBqZq+jF8_9qUAC<~)VT)S)n%e>MV4x=i~m{cjGxm7+X?+Rx}WdN4{F z`{iR|Caw@Dg1VTuA(Hk^M?WTxU&q=R66yYy=aKU+_$7CD$4#?#t&qJ_3e$t z^b&EuSse@f>Ve~k5?R(-xl+z|=4(jzyINt_@`bTRnC);SAOOIcBt=`o?Z(38)&q*5}}RS6LKzy5j)`rVNj5u>ic{Ws1p9%p`6;?vQ+g=ax;{^_m9ArH#T zvVbvF)x^Kr6Y>6xfsn#2va%A!_~L5){iHi|+|ut}PE1I?gFvdae0l$AhM9dSe@R)1 z1I(UwkLO5h`xA+h#cINRJb&LK`E=T#j@|PwwNA3X#l;fZlO868uKm zizzm4$)RnSkc+NBXh5Kd^+xE2bPne}((A7>8r;7thkj`xcS`0c3B5~1DKgjQd@uCw z-SKCM{NikWcL)Rz?oTDpXL5i2*FvVwJ0;t^2C&-dZ^j~ZoAMhIA-k>jZ+)tMV@#|U z$T3^IrQhTo6JqB2g8{BI+;o9M0O=0Z(YKIxpfA5I^pDQ|(wg02(||+bNwaCqpi_rH z{86$pM2K`z{sH(I7~lcVla|7JV3U6FTVU-Be-}c@IHN6Dk_0{4!k<5>#`FSg@vE!i};Vw!E%}B6x97dWve@VfbM>H ztg^8|2@>0w1P0#jrz_$>HbvFIQ;`OvC4P*ZuzXm{*Z&HBA!qPo_|DAki|9SuRLNyy%gv+^v{wzzy$T+(&;ncLEo7YPwwAdt0-9=8%o* z=(YVJkPV&2X!K64T<>0wq7Z?fHq=Y=>D$zUmD@6EO1FxH-ea~hi9@P@ z?}yAnjI~LogI0FPln5*P++9MO4yMy|qVG-QK5flNnS5j;&o=V2(e-hTOdJH_;b}JY zQ9}KXS&4C+_vlU7JCR!5p4~#VPfa&~Ky)b3UQ0qC!kaRTGl#ot%}+E1MPd&haya&~ zHyhe24EefAo^PUp@Cl&wQlIbmthfq!VNQ4Y|)jcPvvuTog=KZD|!%q`|PfF@}7N91}5q|k6<>cHI zh7Xwt9hD>kSoXp6ow`GUIwrXzQ3dWSmp?96b%(`A{R5^n?UgMYn)>xps4gsL2<8Gw zzuNwDFv5Ipa+-hQ6&-0YVoQq~F0M%C(O;=ixHT#|4Gi9#e{-JjSaz)JuGwswo6!;v z4XBrH*4aH2Yuu!o8Ay04MMjVC#phDu_>q6j7oo|QsvM^}#CIQQnI23&vOh}0n=~*0 zfk3awOJ>HWe=DFPi>~VB%sraMDn=Va7u3CpB%leI`j_ z+q%OM_V5Ini^IxE1rUoIJdh@We9y@C4h7ZGkbD~e0b9?1!s6F;ej)(_z z)%R<8X?DEabt^Tm8kLRzC7u;17aq7_srgV<2Q{ zNi-_(-%8->?oc%jo5crSPAk-Oc3#lTYCNmo-pp`D&Tqs^+zaIbJnct62mbg#&SN8I znlM(*4_wO*Xy}S{)+$=krKSJ6Wxr6rKk<$!67ZhLriR%5_ITU<7r38M-5x({v!L9C zaT&s`UH!5D&YX$w8mu;|2;#(gp}S*LLpE(v0Vw2(6u7t<*}jb+(7%49t}NX_R$az( z82jYQA}AH8kae^(ng&lzK?QiDsQUT%|MMK;Ue;pXvbKj+jy#p4>r>`OL~qg<=?X5M zYR!#--GAMm^p+R-OXql#ej-$x_CM3a3HIB$vtDkT&)}=iO5u|Nt;%Ie+H=RmMm9D5 z>xk|CEY)fFS!p@hp<1^9mazPq_VZU*tpo7!S8*Wbe?DLCfpTJP=%{cj?9q5VD>MWj zwd6O;zvOK_MJH>`B41WJ2_M<LUtgr1 z=)HC>>%uIJ|9dL()Fo;){sd*d$-(n)tO!|ZunHG`CMr*g#Ne_029c!7+ME1O zzDPW(f4_iBs^4%a?6bSYE%lJ;y z?8o~+NVd|yZb&)ZChu1gfTx%V8~S)BT2x%zf22meK)7%JR|Ufi;;zP-bUDBgs&<>A zH3hQoq*SgWQHXx=-Ra+ae=I9Kj;VRJSFLe{W~Fj)uUCS?h#ve1`E|~$rlKWLeDf)z7W;?h># z{F5wFGE?DmXyr8y#{UkE=<>b9oT6Pf!7G*f|#b!C&8Uo^%QBk0ie_9tXT~7)WhA zA1Lya5{=bGe5oHwsud$cVttM*(|Z1$LL7ZyTMKEiT_3 z$Q_lDk2_U&(fzqqS1Y2rm(j)IBq!Q{D;1^D3>;=E66d$wF}OtAI`iPvcP0i&R`1Wn^T3Rzx{H z^Q-Wi_y(&8i()Z1Yj2?Xc+LqjCWIP0*#ikUAXi7^)S1Gt&*uwl)6Mh!fpFKr;F0s4 z=lt6})2(|tIAo%!Pzd5>3>e_9^loTbHz$=?z70$5(SCi?Ta(@4X(0dt(Z`E?*ZEE{ zFfjZseat|ATka&>w)d}ziI*L2Rx?$W_V30ttr{?ofbcbOlF&@ug5!#KX5X{aj)GEM zi|@4w4;a-oR~8({#pFd)^g);7wHO`rP!!uxuv2-k4s^aP^!eWPESIQz zqxbT1f6ua|6V1$G-O`%A^UZ5Sv}A>BB z{+58{BWckQd1Q2S!=vWT;$_G4&H4QdpQbXmakQ#4uJ#<6%e3cr=q$*?@oWFuzXh8k zrCPhsI4{j|h(F}OT{I6z<`Qu;$+|NVB&_XwC2?*l7wZTjNBvb!0byqWUL}kih&ERA zi+Ga~RL1+5sc_x7JO|IT?%eDv^E-?O2K%ys6}2ol2L<`79rIF|_zNv|Kt-%Tyydf? zL;^N+Cy-MlugsyHxh>}2o{GAJ=qGcmyW77t_7;47%uheS`b=_rp8z{V8t{K)y>(nv z&lfkolpxY6Qc6oV(jW*5(%sS>(hbrLO1FS?2`e2-cXzLJcgMnW{TScp_j~5WAIsjo zb7#&ubLPbR%q4n*)Y<-&>?p=BI6#2Pf;xrTD9`Gkx)xUby?SEdkY=|!uo^!hRa2|C zIyEc3r4OG0e-MuoSKwRHGqJB2rV=71A25awqlt!)ML)s0LCTseHrz7Y4u&IUbWb<# z7kIFLwMBtkj<(I7Ln4+)_l7o$&ieH*)%|GiBT0uk>=#NY{kk}qsUCD}UU#J%9Q#lG z=-%ArHC&UGC9$Pi&y`$nHkEMh61Bu4GMHB@g z!o^I;23p`Eied$XNqt;Nrx(SF%B*ufQrEw2;$F4dtI4~u25Lw+{d<}65-(3XpE?wgfL_M7gvAN= zmj1^S;?MKHZwT(5?8mx4x^y|1?tV-G!XubxDd5x)zN6S|XK~zyXd{1UI2l^DVSC^) z7@AnZIv%>vi^QL*_HuDVa2LGTN0>=_BmQ60iEmc++~n}cJ03EPKTnu5zqb-R&FeK; zq`rk-ZbU=OsaCG9h{qqAd4}t>rtT0k2`pAU^X25;S=7HQ@44v;rhX^<^fZjIlX~h< zDU$ypZZ>abVR;o%gdQKT=2U6s{K(V@d)F$;{GNNBdYOL=MG_IMxZZ7_MiforThb|v zDOfZH3S|*RuP#EG7d{{zsVFWZKjtN;TI(^cu6YjfQ+$E+-D(sR2De`lEdm&l2I8I0+7)p*BL91xd{#F@o{Og zREA8aM62P?H`v47(4-3zHegB@TV5x5y7TnbjrUP~GAC=4g52K9nyxaQFn=5oL@Qk9 z65pdW3e4QsAl4Br43uf~srB8msCu#EiIt4#;0u9s@C#|R>zT|d|66{&KK7JBRkhqR z%;*`OR}YJRLcOn>Oidy&4^)f8--<;mSc>L#uNjgB85tzh_jRCnz}p%}WZG5~t-QSY&{(ynXaD)#!8x0g#s}NsdfUON+C; zX2T6$mwppD8v#Q%tFi@WA5b-C{$M*PXpB(Lvm!7tEZdAbH9b8&6*A!BYZX&-XVI*p zXMm4bTmRPcrjI5KCx?nsV@Yx|lCeF`n=S11eEzN?OXAD^k ztmpeLGglYPR$Um)^cs+!fZR3kwT2o?K31 z^EH$LB%rQHG0l{m;ujSEHH**LmK46wvZ`|XJ*sX$A}5nA<*A(#i;X=WJ;iJd{$_5k zbgM_KwnbLUd&C0^+uNhs%}Sb@MauNKisnwu49LJ)oP#10pj$Ts(9rSY!lh)E951w`UsRSXE^XwlJK{-6Q4kxJasA3=_P!) zXUTc~VIOx0b~~Xrn|BSi&aFCiNje-Uovp*o$^x-^z8cDOaRhKd|FzO_R_HdGX6m7x zmvKGUUpi6Pb8hfEKB1>Sf;`NUb3p${kUiF~Z=0_lQB#9{?uLI{1$>VGD2A=T)9D#( z1NV1J2;qYRehH>$sIW|gvHjzDO27sIyK_V;UFbE&Vo8%n)--~zg#Ym>;57X1W^~X_ z0VSP&1sk!JPE#kQL{KWr=7rGGy>G0`?{!qrZ}02}Rx(9(wL#rH9Fbv5K3Y$_=pOiU zIfNt(=sE5i6&^lZ%U97P&fd|<-#o>3vK(;P%2Pf}!zb7n;rp{HLx5i+f{FAz^?e3B z0kiH`>lsZtl;0&42=Rjl9UY5)`1)@F?Hv5NqQYYu7?iDKO(FT4f)h+t%+PIZ{Eq>MR9VpPM;3Tg zXqHxoY!SlOPk;4ytS1O5Ayk0q+R=eyIzN+~<5m8y$xXkwEGH`>}> zgJ;o1@H)AfWBGWCCwEVWt+Qm8BZ%azxbIB6`-&O9iui~ycDSx z#vhS!8J`&1fRB9!gaaj6jZr~o2k7Y-|2WBqT$cHoNwvvgN(_^2;g10B2ml=D5209D z-H#^sp2^Dn!(0W@z2WQxUU$4tpFktUg!c9$A85IM&z`~_ZwQ)|P-rvcek2;i|7VQp zsDN4^;{L}dRp5StSKcJCG;qF}kqg>A>;E)&h=tufMwN(@!_?h1+Fv>2Bi`Vjfd98# zb~DD_E9GMXu3PdkBodggl+W;EjBz!MS%C%pbcZ)bGMl&N!5Ht4qQ zKW#U0%{gGInRqDAV}CC&1e_f~NH~!GKMh*&ZosABai$M&4uk6hY-_ar-%?hH1*8D? zfBn1xbavVyHDJ6Jy+d7? z!XRJmC}eWS{|%V}7-Pbft9mJj)$oz~x&8lhAbL1Voo#O|&i{|5(rnNy`bmD%z(C#~7=Ku5L7JTN)RTa4I=8Dz5Q`0BBM*e5D& zo%WJ0@B9277j|}bGOx4I)$ZtNp<0j2gT)3H1Qb-%xn{51ucH1W$~u?d9cUQ|KuPRK zUblRICCmZ=&4mwB0WUH>@1oy@M3`km@8M>8YcwaCT$t?X)2ku=LlOa3R^eam;b*JA zEd+L<it`Ierbd zrQv3AJnnREe3jODBznEzgqGJ~f}`R50+VyIW)5f%8qf0*8u! z36vE6IqK1E$v5jT+W%;tJ=9@QrCF*97<2m~lkL1Wi_m+L+065P7D$PD#cTR_GG2S5 zm5$IzUCPgI@9#E+hm+VS>FBoVHZvN4fgod&EmRmUg=jQ*nXBJLT-6E#^<`Di6%~c9 z=ZD;P+FprP-jr=}aE70_1qG?^OV_ogx+QF)qcy%Na6Yu9b*w+(cD0>rGI{MlNdE}= z!AUiU${X3&FGH4hh|F4z@3)jXp{f?E(8Sl$;gdp|OD8Ruc%BXt6eA=~F)=nzNqeRG z%sb5lGlZ>VzX|W_@7MD@?#ydTw}_>Yx!Kiw(A3qvPlev)jSn7jB`xuWzVFTUpvp<=rcI&C9(}3hxT{YE4X5DDaN6R zyBDVGRMzUxIK`Pz{Kh;629BmU?$8D&r1UZ)xuh{ z6m}_LXYVTb_x-ozZ1lVj-&QGN#Vt{asZwjR!;c(laN8RwOPNSbi08U{mrN~)!&<5q z83sKr;e5rvEDqP*AV@*$m_2u}s$zoC0GX(X0QYZ5EpwVsTaR$E4j zXv_uW>W{u$THakCK=2WA^Q@Msz7f7%OhU{&cec%PA^qMoivmL8}_`zOazvS*--XAeaRU*!vpM-mrZOVb|to5e!px|0g9 z^gQsxT17>LZUR=fZ@PCj_+=*a6twr!QXY%oA?mlnRIa1iJPf|(eOT;6>Y9*jd%b~( z9C`8N%*x5>k7mHvVZaC5@1J@N5ZtAilR8OKrt=`&N(n5?v6b+8W2^>=>N3=@CVxDE zr-`zHur;s(I-|Oy3@B9^1p^eYavdSyQ&m-6S{adOYJIL$xL^@Qr(%&iTfp-a-B_Zw zh`21OL>P;-+UN-xl;m=1;VZUoSbv2Bf*RMnoQhNN&Xwe&Dk->}R+>*9i>vG zyz!l{&sIx=6!eOC$aJ{ItX1|;b;Zs*eJhd9rJL_?XrB0%=)?IxIxlb5<)Y@>zV&6Y z?X=P*ZPw@dNsI&2-Af^2O4a>iMCeTMXR6SUan~HJ#JsGeCFHA;BuYH}&W4fIiju0~ za8!I46BD5JfS4(?&qm{j^vd}W0R!kQb*&{@W5bX(GB8_!v6I`5=H}*?P#>TDRb&hj zGmFZpP+VG21ToJ6L9l8qeY{Ci^N)=XQ9Y}eLS-8=DK(_Nu=8`DFX!7HxK+AY6)fV2 zn2$h)qOu<~(m}b)&(Cd*xe0b7?MmM{*=P`I_FTpcX7BhxKAtSc)}TEUM0W ze5*mLBCxWl^xodgoG*Ku%Izb+sGz+1disvvagcZ}u4$g9JUY~!qVKR}N5eMKbsJRB3zs%kUcG@iF`dfjZ;sIirWFT^NY2h<0@=Ryb1bCC8)bI?JY6 z0n~CfC!0Wa>ywY^1_h8zOPI74E$vGaYpZ%SvokB4>&eggZtPSGpY%7G57mRi~oHfsSk>i%U?*YSv zg3#u%kE!uQ5>xSAWCJuR%w2Ey@^0z_OM!``ctmQ$L8x6x6v{Chm33Iycwq1i+St`~ zAvbl`!P^t9ud?$5qz;O>d$DEXI}t}pbQAPjN*7c#H_!ZOii#x12BJcq5>_t<()paH ze9+L`lP;c0KZ|nJFO&FmHsvW_1@&X&czIEU{x}gvAsm50geHQ2cH8qaU7=Xj(m*vh zk~1^h$5!XeRK1zQd(bGpP32SN*|%~WcLK3vz(*taN!5Aw~y+bw}+2v z0*01Qr(EJ3JG5e-bqW$K2(43n)Us53-qpuVoC^bF*X^hkIT)STdAThR0^7GOZ#V!jRmpT$k){FtZeP`VMj+ZK9Sbfi%c;6!G8P;-IkRk_5C81-OVV^ zb`~T3wIIw*11+Pgxnwxw<6c{&K$bJ0ut;U*c%;^A*COZyEov#A^CX?AA{v>Izg!Is zBg&PZ&*YPuf3i@i>l1ot%THv33t@b0?WV6MTRpW+DKj;OIU6NX;~uwi=o(dm7VAd% zpm?9FFTEAJx)^eT&tiWF(p*g6oa;S6d}eGaRzorOB6#22=Z*!ZEf^zYY5Q2pu@v78AVse3L4oa&>NV_*azjIApd>oi< zqp*Q*xZ)Bx*DtY;k5Dp#CU546_hMWS`4SRH7a2bKAJY^PjY|-H;k-s6n0{RbS3mho zgE1vWW>Pv`(DqN>lw#+zOcC`f1#+iVPN)VD9K_N5^48g&qS@Hk@HE#O%ck>jnU7I{ zXVboXVK*Q9W>O~Lezvvf0%b}Bj#^QYj9QbLwa)s1=XKL!7V!LM8`9Oy(hXD9EFUNZ zZi%Onoob95Tx!#d5Q1f07yW>ee;zgDqT1+weTL*8tL?fQ2nPTj=Px$8Pe*XZnh_G!kX_XfHtDys+N4u9{tkz)oL+FrkN?*S`ZN%8IJYVcC-wT+bqxygZ;s zH~zw!+*ZZ49%)uz6Jdnirpy}N^+xE)6sLBj55;RGB*#7PPWy6)E+ZhviG(^f&9Ad! z-ck)dNX>DJZB_|q`G6Vd^oA^roaH!B!^_VVJve=2=7vMAv0NgnF}-*(h3^6;A)Pb& zD0)hpQkg=o=Wx)^k4+ z^}OBI`{Hrl!JLJPCvWB~%bTuN!c~j8XlR0#Wm9LXo=8xFB1q?HmSu|@Z%X(sTpJy=|>R)+d^i{DJgR*Kgmo}C$d@*tCAq#pa*n-+d(LracaaO&5 zu>fRS`;4)uiIwQ&heKRXOB=qRe#2RogIIe>CV}AmV9?)tb!ptkS=65ZQ9jpqr_L%c ztX5W0U&}sO2f~$-q4M0ff&<*CH02dsA8qCeUdY!i&&Y8}P8>YtMFid7HQb!K@V$`S z;0FPwsD_%_G@x2bber5ahtj1JnMrx=*8pFbiJ-sE0r-ah68dm|Ia6VHaBz^yW2;+j zm26ntRI0q678p!^aMjw4R(jnkEu``i9;;In_RR$&UX9#mUzi%%o0YFOz7of3h6l2<0co?a3!iFOBE+`W7f0Y^x?*zB=@9)_kOl138n^&t5?V zKolLtZt-9W<_meL<^@k=-#dfTyl8ula^)>)rgmfony77TJ}su$xnSLggLs%^IdAXL zF>(7tos7$!gGoi&;T98dyrB7L1PCYRLU5nw4>*Pryvo=qX%Rzsp zlgqF|vmmI%L$cxWZC0%I$*h)(&E0Uhq@3;(pOIUh(7#cjN;A3}jQSzX*HsNTwM;Kq7_#^(yF39f2fPvYDLDt?%GctVkBG45; zNsxl(8RPhatJg*LJ6f+Xe2x1P04?DaPRJr;zxGy2N~-1HqJW43k66(ffNNU!T!b~bb0(lttdI*h!2>o8y=}zveNW?h`Nde5&9Xc$ z&ywNc*otKyNh(kr3sDS-`>y8dC+2!}vgj#c%>+?v?t=q4nhFw5tPg#E+Ew`HvXTX4v69(z8ZTCf;w)f?ti(D!I#Xc;E*RrW>E?kmGX%x6;YTGS5U5n_~;=21@Su#GK$Fv&pn z^^z<{Ksi!sKGD__71?&$?SJT#mS%Yv9$IODc?7(7z)apCJD%PvDaME0_5dbFOzF28 zmo-8?@R@Qi_dV3t+iPvJs>@WbmoHQQZlk9P*?$s5h0}V${AFCqr{?SBmOeA=hO?4q zVN$?Do6Fdf5|%kmQy_Gujf|~k&rl?U+FH#y`Wd$~I{2KJW1-ZC+!XZiu9gB`mc6$x zq*bZAc-RuKOT2^jm5T3Kg=Hqv8`M$Zc2%R2__FewJI<`mre zqQO3l5w5Ny>68LD=PK%@D;bCO;?E71e|D3Tp*=s9y5?ZXBOp*an2A5XBPY4z_#KN~ zl+0qRLIG2 z-+3uXIEMb}2X8T1mPL2IJBKSG3Xtp(pD7`WUa~!WT*Mjbqmc*yb4hHaFaaLr$l?BM zmmQ@x&bV@&-}gC+Uj_VdKrymwFX-+-H2TM>1c>+gB4KpJQdJbIjJOoYK&ugkYGL;7 zge*<>msWeI55zVmN$xBN!}YbsYt33Q>2} zD?dUONOkX5JC*T${&u7$v2j`Ba7ng$R;>Q6FU0^6u7&c2ZnU78Uh_?2$w_!J&|RLa zGE{gHx8+LVVEOd6v%^9^(;E?#Nwb3}`jl(JdM6+Qqa zc?jPK@Y%MEAzhql=@}FiU?HIdcE%Hg)5W9u-!@p?d-y8TpKZ@x6#t2~aILSFf83+( zG2YKO{AMbmr|ILh?#R@I(O&#uen&mbcp#1~N89I8;2)Pr$u@dli@U~8@jUE2F`_~k z{{_6E1ykQD72yHNMqIjKbCNNib6%SP_LDP$`S^7S%X8^B*k#N(TD1rw93nOi4b2nY zgB_z`(i-@}mJEc#nfr(NXT&RPM;CP15##GIs4#^a;!nNoa?tLB{<+xg=z4hKd zpHeHEa%&?6QnyhTd#&fYpr4z;vLm;umiMx-RjlVBe!-S9vH7!pTbE^|{7MS(4Iu^Y zZl%O1e$Q*q3xJ(g`@4=Z=@g@vym8e^|F~ zJ;j2WnGIZ>DHYwR*)9Sk5Ei|Rf%oS^IhMNTGnccBH((CK`o}>7h74t(1##E#4Q?9!$sb8ze zQi*^%(mYpZun!7SAWBUum4o&1lsr~Vj$6TZ22JLNpBtMbPe2Q7Bj@odIXCC)H^0r% z3DKK^<&`aF__Ff%>58TnLq#;)Y{^I}PmX6iQ>|V(*Tc6=fI3%V$oy}CGU7b^a(vYD zwydrL)!QUxzYx~R_RD%wLE~3mqpm9@Sz9S@^eo(rE$_R_G3DutrtS+>66bU*8ai>K zU6~u@76%%zcBd9%q7%VPX1tD#-}IYL7o(@GFeNbe6&hSyxvTQSf{S4+wzE#s=b~Pr zbr;0@gZ$JG0ZQu{WBUJc*mgj@`b&H4X3YXMz~T!&dc# zA?Gz82b%={3)t>dT0u)wuKiR~y)p|WxZ}-N;e)OwAOp^#Fk8PG33@nR2Di72`{59Qr3A5>#~b{M5u+iC4g1mj(#?jcwBtt*@(Zsji~dH73YiW!)|ofs zdXIiemCZVf14o8QbbZe(`l4k9)KDCzuDC<-)D${s_4ICCURPASZj4jmHA!l7u8`l= z)y=RM#S#yJ7^zJ`JO+DYniBaAGP9TJYDPZ%@V=30jXORNdjw#~=K8zjF7alZTk374 zhU;C*+QIp;&r!1Jbg!x!Z+_NE8>=*!J_b!3oRA3`0m~z?5>2(D@L>-X{+ajK_pqL3 zO`!rve~UMB_{Q~92F6jXAs-jUhl$~7t3k8>EGEa>?Rh0o%y;P8D1sj%=p;-WL=%H9!<}MFXr27Ma`=w9XfL8`9zK>f}V1txh!pOS=f9%Kzs(;%EGfEm4N7SMc0en zOIKf?Hmz3+6^_|gO!Yr&P?=l|E7Qnu=dG#jxZ5JR?0i9nYT9Z7>AFC&;9_-K}b)hDsOt|fB0fQd2mw81Jltln&g@$ zTkZ+XPUOx_BxYg}=CPlnk_!d-k6N&6z4;)bN8SFPI?A4(Bt@Vy41Oo4^SxwN(X=T! z@?+#fhR1?k;i0d|Eup6QirJn(2)TC~rtsp=zUC&@3YkQRLf!JY>Bl`ZMpaOZl-=~x zGkN%y)zf+Vug-0r`Kj9hgBn0q5NlVC9B!ni6#Y=s)3B7U%(D9VT_7#N`NM4j8W^d(Iq`bO zBN;^I(c=Li_u$lv-K)JHz7GcuoWyQZAM0(B@aa+|qN<(Je`D`Gm0M6CJ($U-pRB*W z8YP1@X)~K7z|IF9Yy^#jminH|v4_vni(DxH2)X1 zelgV2jDuOjeScow_Sm*W#O6hqR7yihKL5>y#r$WumU&#vL9^R=XEZyk9AN zAaETuvlfsT6dZijqY4{hI`+u92(HWobNRq8qNMq@3aP zUQj=7rD1h_jv<~9OfO?Hs`666F8cP18`-(8qKjibV4}r*Kb$K(T_7t8|sGPJ%9;o}r3uY8t563{BT7x3k1Wp{GM{E1$ixISH`lsW4BD0tPQi_! z9W^h<|909Z;H3Kc-YERgES1O^ZLqaPR?Y4oRHMVg3H?S*ifLW;{YsOisynjW(&wOz z+1fsHY)mm%@nyVJvG8y;@cyKsvU*+y#(^xSzt9XVgXVE-W!079^1Wz&xxdMO>_v8f zZ(Yq^BfD@SIjsSY{|6qJLpx2k@gMw?wid5>3H;E%H-H85L)hf+?W6mF*Rq!}aRJ#B zpr@~}VrhUPi@#}n=>eDe;JHDKsG0DnKR|#vOrXmat1d#l8FKJnVCGhJyJtS}D_1Ne zB+v*%7#`stFw2P~t0=hLpyE|g&`uT-ciayE|FQ`B3-_vL6UCy!d8bgZ^a4A&)8Hlj z6!7A>ApU(R;5CH>=^BiGaWXp_Mv8?M*}zLi7)-!Kpy$p#-{C>Y?>F}LO6;5uVz7y) zrT3RxM)@C|rq3^CiS-O3foyFTylVa;u(G3QKEHh)%ZtFCJOHHX?zVseANtsLR8}u6 zBnMn3?*g ztJ9`QJPr4$842BxTwhyb)Ty87@0SLA0IbHw#sp@al-StSt@`@PopdmnJBnhVnDiek zQ})GJs6nG?rq@NRoKT^6N@{9@^?Y?-UthHF{mds7C;e=7@D;{i5{KC6rJLOk0SF0@ zC*UMz(}iWb9{m&r-CiEL*lFmfNuic$!-3@F7967e!~Z_Pc|7za9l(fz#`9#rg6TqD ze7=5uepdin1Jmz!JHqxv;&oF~ z%6Nv%QddX^)zdQam`=fh3NlgZFGGjFErkUV}CT!r`#-9yFdWSF@kV zWjVzNs`go;;&`q^Zy~8Jl%+LnwD%zWb?4-Q6_QLWYk`~jG<$+kdH^nBuA@pH4xxyz zE@SSC;v$jB1Whxm%eNZEm}1Ew#%RI0w_uf_vUNC%!#dl%Z7qdNHm~lYWW&aIzvpLU zIIKRC7qK@fO7jWBXG%}}#^!|?EhJNxv{iZ;`b}g>vhfhteSPZ~48~DGPnC30o}Gwi zU#tZsH^qyVlk;D149I}V!O-nix9!Y_+|LU3IElAz!`cmA{kWoH6MkRMFw`rVw=2Ea zWAE243PyZi*!X=V!@;Vw>W4cp_=}KmbISHKUG0jLPGH<#Y^d2chcW|iP>fIBkcb5KP z!s=u<1J0$`zCzo%-@nUfLb?rCgd!eazSpgK?%CeUcR5b3LwGOFNERLGHK!Lyz@F!W z#ddjFp^lYW#4vQ_n5%aabkwO3$_t7CJE+Xye(c0mvtA@4=0cv&Lyn$loYW5`L{GB_P!p>{#e)A45N9|ykhe}v@iYUpp)`FO3p0fB@!3gy^ z?KW(Ss$8XEc#9oJ&Ad5Uw^`_&?IQrGph=Sa{7E=(%;yRongI^3olAxq)1|dcgg*jZ zRU25HFs@M>Bn6|&o6!UZ2vt~2u86R_fk{Vh zuGZ|0+z6)QRn6!jxe5}HdI@|y7`lo}qkcx!bue;ME*o}R41sS!L!Y8~u7}BTD}BCM zQ|`%lqy6FinDh%>riWMO<{xco@#G?xn)o_~@sGkHbW`Kb#0?BIWqTr4t}a)@OvP!N z2)FY=1c>g2I+bkQW>Z(fq>1%dOU>0TSbMoLXtzQyn;2hxw<-G8!%S+_i~%Am(}D=k zUD4sfi7XS7Rq;&#{i0%KHc?YB$ylO|8s+6}xFSRFiTFg3%%&O%@!S>j%|6=wsh$ug z9XLL9vA4GeG}zAiI<;~IX}LG>p~`W4EHxB}StUB5f5b1e?usRc1o5bp$4_jG9^9$Y+@p7w34 zn*v9Wa&;-{%)wg5+THrb*o+3*yhwL_pPW-MOZuU3Mm;7W&g3iu>jSxB)?*zA*G8{M zJQ|QZ?BJpB>zEth&6mOu_VbNUrNPZR?(<`#sLd4H`zcE6l__0l<8|yD5c^x&AE5bS$D$G3ml3r zcE_O6b0;~Y11CK2-M%7Dr#P(HVQ|Po+P>I4KB@B~W%~T5}(c$q8-S?FA`&}sZ zs=h^d`qL563Loa1S#`x|sWdC`S{f$DJ!YkzUlgV_!jZ6v*hWGx>i;m zg-XfV8-$HM6c)rkfpKM^CPmh#85p*Uj?41mKLaIcFQ%J3;^yi@?>l|0eWIvcRCcAU zxR5gYR-F{PDe*Dp?**qoyZ+rXzn-c1`**As%4mVH?~nVP2Lb392oDeMN0CCms}Q&6 zO3%o&;!X#SN&1C7n|9L#*L4uK?`t54G)bMWEVIkD`OPOY@AB-)kpdm3x05XXw-POO zmuuwhi~BsH<>m04>JoHW=nnx4IZM7CT#U^80QM8#H?zhnws;)OkXk|`) zX|Y$Y)n~#2(ojY6he&K$j zPbKxs83JcfGbPL`qIcd9SI%Pm(njNa!Wr@AW#10~(j;FniE2bceIUho5K@Dl%5|w< zkV2Mg-nG}U1q}`;i<05XjCBxt>Fo9Xi!SaKRGaFz9W5>%HKDAgET~#MX~Z5M7x&}n zhInVPn9!oicsAv`fS{n3hm6Df&cgW|%8e^af#U4PY7lDMz3F5tQ>TW^3uI8{NNxC| z(XZ0?mnqa7oT#8O?-#?J=?rD{(V{gEP4QW=T&RIk9F!Cr^JxBbVfBmH&8QEMyk}O4 z+7Vjp>RB3%T_-1F ziOKnA!)yJtd+6%!4*$w=D`vmY+JkYvRaHlfYbfK5J0Iw5>l3fTzakH8CoBLf#9=yw z$m(_D_)G65aaj_8=7qJK&?gK2AIvvtw?0gRn8zkgDi^@`4h|0^kJQ*gZvpL8HLYv8 z&!wCb&iNOBuP|>vbJI7`<$X`&?Tz)Ow)Qy-rl7l{=VK;4z2rX{&PKESJf3ta^Nsc1 z>9BtI&H)7aV);)o0X8ox;4>iovzF@_Hu!4-r4#LT@IZ8UNj$^<13CvA0Jb~;<*vWf zwfjW%d%b4c1YHkVfBNTPEj~R>FJqxZC@cj1+>5wA{b%oEI{}3;I(nXsO}v2N=<51E z`1_6-;A9E?4}xtA{9CK%(|NoK`neYHe?z&~K))^lwE#aq6ioil$s_T-mL=@|hb6a> z1DvNV0`2Aq!)}lWbGH~KZ+(g?tEzHBJaLKY=xE@~J1W28aK&Zd-RrM^QK+DwZX!{8 z@FlAjV-#~h0WW*ukIY0a&9XyE&x?J$rZ6eW;VE`)ck$orWf$oz2 zt9w%D*BN|SYbY;FnANM!iA|55)E!;=s}~3{;Yxmu&cgOXR>sumbqC(xg%_RXkS^bd-Vno ziyxegM?m3SXYEII2pLUm&XYgG+Idxq?qYL5oldlQ=!i5p3@zNBc=(}i&#ssg||6&0$%C_v~G6nV2m9^#7(OSTX zbKu0AAqC1Cw{s^)bMcydod-wr<#^1y!bTm2ZCAh(Wm)ZBt(qp$>5%EWI!7WNM!u+G`e*g;>wnr`81v-<^o zjuXL~EO~b^HZy`}_vWhxW^LtaN55ugk&0$$CGW)l#F|8gQrEVG#8L-Z38@O@*t^>5<@x*taFt5SJdY7Dyh&-4l_@=BxWM==D;(~uRiC;`pqf8 z9~-6Q-ixm9&mAx}f^sqKtK7wZ%%kFg;@>s+f?f4S)1wPw^ zUfXu=KzVu2xo+x3?{??$frz)5C)F^cbNzWGf3$2IL1J8-0D$K*?26#E``Pjd0U3br zo&xwTV4rv4nBJ#pfTBqFKPT$V zjGiz0@;>Cp>2z00@pn2R?#Nf$(Vk%ECuZ7XG%D@vznn2?U09}kjQRP2LF&3YI2}*+ z>X3Q7@X3B9<8kj3RZzJ=j_bNPlCKh>=B3iixK2ovqblE%U^&hEx}3^UOR4hkVcU}0 zBxBMAIULgM1+iIfm`1v|X|IdW%~sBGd$8?7?Wjf71GZ-AR3tG^)KfMJX6B)aD&8gZ z&BLo(N}4<|ss98mdn4`Dc`RMnYFEVB_U7aSmp}5&00ZJ@3UlEEIQl4YnRYP*TiULzd9x3=s^egqps?{O8bh@{uD)R ziss|bf(~x0IejZ%By@FQP16R}i-X1Ihm>x|(L?MnFUUC{nexIbb*l7b?&C@6hUeQ1 z8V^6fsr}aY2VY~)OwR$+rl8<0#pk+n6xx#K*_vtdLamnl&h=;cz1@V zi9}c34~JUIJrtGvtJjh>k;9kCLQvCshXTjNlP=<%4Vn0Tev>KPiUWQVH!}$a?>o&g z4=(4VwOh`9y(RX!jHQK@SCqkTGagD$g@p>PcKE@;sYg9~Ddmwqv)UiI^hie3PWm`% z^lSZP*?sa03Q$BK-mslgW*X)7XKrq8JWhs&H)ZJk8^~zerFe*a7-;OOI7| zkw9xy%)~`#>Z#8D4aPQ^^IAklCy~qz_=gZoJ?KS)R|B9I?R7;J+R;Z-_2;D-R;T+T zGq_R*lQq=X!R6fNCj)!Mz~=#i`)dDRGCuc4p@PiU1-dk-XL9RgsPJ)>xMxbAo`qg) zxV72v(S_p6^aX+2$m7dQfiW%WSdeca=jlMGZSy;+8Hc0PxYK0n3oeGpPL1wp()n{I z#kQrm!{POW6wuF`cn&CufWyh!u@01kU*khi75nB`a?>pa%^q)DTtc#KiA8Y5Q?MMl z+#TOb{lkhi>CUdyG-*+tuBT8wX&N5zPK~&qN0&5QTEE$%T2aE8F6_mG`HicG37&w$ zMesfaBR#!L zV);f$b^(z(WL_iHX+4a&2I2zUpuPGps=dfrsEpd;p7RKIo&_A3V3kXuR{Qd^7xnJ6 zu2^tE8c1X*2kJ`(`oLv$@3=GIG^~ItaTZSu_NlYmFp`MwZFdJwRT?r%xF8Wam*qN3 z98QxKW>$JnzVQP8Lt(8d6;tt6{>S;;!h^J#i>W;b5sxJ%0bw+vphYqxfx4lU;&t^LK~wb2{o(6Wa~A zCZJ*1MuMskMlyxRCRobBkVskDSZ&Z~=?>u;{Ft{w|KCIodkwj#!jK%vFRBIQ|^ znD@6W$zUJofwUV^^fRZ_{@wLQB3Br7j>kun#P8$t89=@sK9Kv%bH`6b#JQX0^%wXU zx$d}WOQNj{OKLg`AbODYGD9YP-NKT}Ft!5y}> zwE^4A+{H?rv?_=>x?mCOx=-nev^_K?W|N&F5cdnluEWOF>6Qks1V{qFH!BK!cc-4d z!;{fC2S4>13Jdu^q`h@eT}|^pcyI^_5Fog_ySr;}hv4q+ZowhJ-QC^Y-6gmMx8M$Y z^Ss~Z{q8@zRlnUTin{mQIcNGz_w-Ene7c(eEM|8QF_u<>ThHo2aZnq+-%GGxtYU7j zE|TuZp39+3_T4zYo0fuX>SOm|S4;#f+8+)aS>1QI>$y|8#$)EfC-_V$DXlMw*Dzcz^cLZbRV7U>|u9EkwUpzEtv;N4vJ=?(`qc20{gQ;nEb}J631^ z0exMa&>3Xyx6SS@x^V9kebqg%ZmPGjg8}hQ2$s;)>&XMGd%HJx5dW^I5eX>vk)heG zwLe>CH~TFt$4}_)^x((WMEB+OVk&)&KcZ5d+J=$cW}UC>m6lpOBO&i?ihe+Bf00#M zlvLgJ#rw?{#^+fsn#=Ou7G_e5>euly+8SPmM>7DZ+in&Eu86%+5Wv7l0Qb{Zmy$b@8I67w8d`0&$X@kk7VhbbSk zWU5%vo-bKZv))<bus}&f~6<1pm&Xe7%>k3*L|fv$eqmyLC}k{>Y`}&^hWWpd|xl z-h4Oe6t&(u_eTEmNq61nk&@jfi>{6qVL7etjn&{tY|t%OyLCpV&FUS*xD#Dstjbjj z7+fCTgWh2Hzh^J6eBMtt@aS|b?;C|ky~`y>mV3WDlSB3RV*0L=LS$$YEF<1$>6sgCRlI>zoqAR z5Ozw$CU=T02538DXz#-uJ#EiWTA~u^N^wXgGz*z5XC$h8%iw2n>R~GAk3r@w^V;`+ zn%a1z=VW&F@-GAh!o|}|1p5dbo%WA+9OsvuH73cQlr2Y?o(k`H@b~oU16x*8xTY`A zpD+jxUC#q9*Pskfpk4n?wIC#i!8*B%%h(y9+ELEtNEbmCY9@;_?Y2)2gf__^2K`2c zQr_;e{pVdaqr=^_4Moj0=Gl!d;Y-su4%T z8rO9@5aQ@6(q5^~+!sH*SEJg65u%wdeh+V?-|LV*-S1&k{mRQ9q_%qR1YI~w-a>HF z^8)1-sMKnrBOuUwc6l}mIa_UlfrKpAY67KNGQHQ#ux(e&f}!;!=6x?O5Xr!L6{656 zlugB?I>b)3%YGxG+AmktrGrg?J!95w&X*_39d#Bx7~hY3Ol9hHGjd)FiXHTc#lf}7 z>gdB*xTL3WY*#tbQtm}NC4`VKR6%Fz{S$heL;{&Bj}?9ef0ibR;$x6wO-Uyh+i@~J z%!pK3CTz<1P0%bE<8DxN6rIFsVkXmY82D+#vCy!?ZS#!|_|~C=5OqGF|_yYPHU% zxU>}9FRyiXkKKkXCAF-L`y02~pQ6f#_M<1-lPF6yspu>UsKohH1wh&!D0${hwZ?`&P3mPI8Ne@KUO2G&P^o>~uWdCOJx2$!$!6+z8|dqDCmK`y79*1{9gv0u(e{m#aMi`#f(=fl%Vkx<2pmJa1NZwnw_}Kv$eXTUEiX7pOhBMDbNKR z1XCjkhNI;nuiKR*U0>hb!h*2hYaMuQ@`o4p&!=I%9W7mN#WNY5gSWnabP=ELU7WWc zKl%ObL9p>cNR!X3I+$L+{1RE#Z0HyKwNiHb5wxA7ZD`)LY<;_fsEP3o zIVX$G=w)BiquBzQF$6pw4r*#?8njiLZ;e(OZf*~Aa{S(Rry3w^{6z}0DH^>_yV*=` zp-vEE5*CJSYAZ8tP&WC1h}=l%TG>Kc&K;LpZ{Sc}>O_O;yE=%INhF>qK?%ASl`F$F zGhvRQ^`e(ECnTJf`9^j8vH@G4Aql-WIqkW$siXUL5ewk#TE&@x?(b^!-ZwvAUFj&( zaWZ%~xh~UHU%fS7R!H`cbGU>lPq`f3USOUb{0uu?DsppM=4wGHs2(yq?9+_?*?G*T zw6wMcMPW13`0+WEKa6gYt%}8M=y=)EzlFkpKP&O&AikbNoN4TrEp`6NWHn4}SCi8r zE@`>FghP9TVZL)Ou(YqQZ+ zhSLM#?hxCnjRN{o-zPNLlNyJy3hnm>aCmy&g*@>gM0q*h&F#rOy&u2d~ zt7aJENFV|As8(;yx+alRX{7ugcM@%mYAIy;3hjhEjP}R?l9Rgfyi-o|S8DAB0@+;I z%&(lo;llayI09z=^ayb)u4kZh|HQCfaAxwl=a3*Te9${QjFwjjqlHU}j8KXdhvY-J z5j@Ic(zWrX#izaL;2mz>wY77^KXtDg>6AQ%_{dGx-~Y2hS@1$Y@`3&kE{+%C*g<3d zNQf7-qCEUssZZSE?yK(lYQgJP@6c;@o~)T$;4;oH6rkYZZK!fx(JtyZ zuo?~j*c(gIqCwMQ8QSgu&O=$6MDqIo&nubn`7fR6H|qooiKG#*66l2+A=;po+1?}GT(XuH`|a7y(?;Ge zpv`X_`lgr7(ZmA&`pTnj*&f=-4!8nS6FPa@Cqz5AmiJ7uUV+;%&Bje#&rb2O3rU1)}Fj<6TkiH1>Tk&1fj zAxjYLc7~}$k>L?!qk}*?^=IQ&iPwUx^VN+JYeMFW zxuS?&XOA!jR^``CB-(=P?H632y7{iT<`w%QlQ+kNlY~&&Mv{G87l~a32Sxx|!iag! zVV|<~985Davtz6>`9p*(ho`{x{n`-)!v9#QLc=LKyzcZbn{~S0b$v#iPtJ>o%vo8p zGn_)}N!aLxGESaWAJwkAMh!_5#ATJWyG9&11n&0#Jlh4BB@GX)wM!xZzg$E*;e}xq z5bIeE5-0OEQfv3k@|;Z(?5(5=Qu}MIjExHh431t8vi^+G-#?VvcHE)??T`Cu4e!|K z=m*Eg$EP}`?`L!f_}_p0h@KQd8DAO&^E;i>g9hcP>CKB-?A)IH0^|8{y|Q>Zi}S*c zJ~Nfqb!~0MuCK{Q$7l}mpB}#tM~W!e(>I+nF!yJs!Pmj8y{U=hBKV zRSPzgPEj(e3ic0fdka^E5R^u?(HzsadqwLjhs`&j1Y*~2JfH}{)Vv8zyiG}|fTj(L zi18fXa`w%jg)+;{Er!myZHCUkf7C%LVi$8>C3w|>Z~phoj7jcGxc?Z9fAu%(P{&!9 zBVt58D1}O`>&3>^l>;JT4Cp^l{kpTgeF^eG)zPdDcYq*Pa?m2UWp7V3=?B68(K14? z4H2H97cA;db40|zOQp8w^#uEp)rQro=gQvxL~sOpSEGqc7N2)^iKha&5f`X(u0!_L zqzuN6;D45X!k`r$@ye;=xuU7TC~SCmc+cD8z3pv#koO*xr=W|>Ywn@r-kkrN3RYvPE7H{I{vIHOA(DWdP~_`$$Et&R{n0 z_R(U;Et2P3ufGlF#W*|Z$iuW5bh`AYn^crr&D1qlR=!f&($~si?|t}p2#?dbq`kWAeu8kRYLEMXg8mr;0H(qy$_vA9NF2440<3U0&OF%WmZmz^AHzO?ADy4^?Kl zo{nlWr?}W=B-x)HUudy;>1L2ybyr(`V^c-A1p1i%TA6V|k z?3b#^dg|@$)T6L#GIkX^Gd|fG5(PZsAkaH?G@b-6$~(rJh}`VieWx3yv>YoU2wDMK zoM`O9kxiXW>%;s~#OUgzD*vqKB)RK`!*0LkFUx*6V;l9`k>l;!aeU$ph0;RJL?%77 z?}(Rh&#b7;P2N(EWh{ULXCnyF>1)R3N$+`cf|H$BQS-yE_+d#9mH;7+?`?ZXx87hh zvyWtVP;w|LE*hI0Yb}DnTO{x7Jt6YY_g*k*pFx*J-6M>=3+W5q*L7!tBUQ=8(!+e> zx0%)Zmuj1^;}-HgIWf^H$@TXv*4c<7YLrgPT}lbq?q*u4?vg;c2;!uCb1Yk@@s)aK zNltONqjru0k?F=H&7|0-clnJs;fhQerF5p8`wXU?+go&gK5EjbUyTruiWm(vY(77~ z9?l&0ji)v%cwI7mntLfCm>

Jr63j7Yx|IK?os7k`2{N$YFJHDm)K=v=d7=5c=U~ zY8gdNVKDnvEsk)bEA|0e%p~5$?2StnyP=Y8a8uEQ7w`AffgJ}LxZ^G$o4X7$y{c^dXg!mm&~2qAsg7%ciGLaJ@rLAVvPmaf=$ z2o(!t+|5kED7hPxv(7)SZ0mNI%g`T}UZZH}FH0gmng%kCdx^c64LLEfg-VcWo3oNTvW?PCMEgs|a z@9%G}I^}TQ)=H-u)GEevR1iaTM_QdkJHF1=fkN>YzsRH>>tE`j)7KU;oVI*&7t=D^ zKlV@1zkjz}xQh%jM}#FO!zp1V4PiVV`{f#(iHwW8(X}}7&0#*mmRfYbW3@JV(=Hx7 z?_GbvaHog&ImXTHd?k`=&I@C=^V3vt)Zvk6>qCrNx9581zgPgb^C7HfuBr&m2=RMg z6YWK{F?>)!8Pgf$?xA;shK7?5BrO)@&&Z#l;FN{RX$|&?RwjAW8yImlAG^7gTZmLS z6D_*DTx?eIhGf*L(kgZMy-wX$+rb4xLPh$_DdsHi3u%9Z=ki&N5%N+h(nQ5*Q{-Wv z6($=vPbx6a#e$@*6k|O^A*Z%Ea??rddoBdjIHmL8s>0464UK{fr;mpp9a%`Jc&PNX zzzAc@_9+YE1v_ZX7vqz0i2W9dmosdQT7Pj{&{{9IGCk4f2@TOotd`BCNpkst=jmK~ z3&o3|H8q`eX{WHlDdpF(nH)*l&+{aBp_Z&>{A6HdqWkOczAw3sN-R{Q7+o$hrCQ-B zIxE!nPz*E5553ci3~^LU6cMSg9XX|@qjcwFd;3%XlEK&X>s}^&2~fh8ls^?>wvbc; z6ckG(O@pFR`SzY|CfTi|EA=i-`WMs3)=X*?x)|8cV)YiSu;B}sWkdyzvzlmLr?A$# z`d}0}W|1OHVk9>ZyW8pUqMxs#GMtWpFO}iiC}kMJpygXr!!zDt%-P86;Jr{JUb_<# z=)xbrkh^uN)t1f3D$0y+9HhSXgC7*uioFV@wB(CI$g4cp;&ip24FkJMmf7^~wAp_> zP(l+W$E^{yUklSBpJn$l_i{LU>l5{@^c=rgC@t7L!qB_X6&0_1(*EOh^%k&Mrxw(M zXqhQD%p9ZXSiVK<&QE{DH8BCdSqiOoJgXC?o;alT_FR8H{MICn9cwCDh{FT{Y)c8W zoQKFz`i6m0_=(&5R%v3^r^2(gmc16pMOz>d{6XczBWrh7bo@;;M*y|oe*ll}or~W< zb^m9hDwEuC(Pp&82yr4A(K`C2%XvjV!Vx|?>&?r4`e(yWNB*UCaDZ+bVV`&vrPGkC z;@8cc5q@Ca{q59+NCsh_0rUOdf#nsv6ZBk;2prhHx5i_^M}VQo0l6?7s)I9q1H6Vv z&^y|HHZpW8v|iD#w!GNCefm~&&|=Z2RWIPj)VAG2FC3%p@wlp=gLOdAU;S=l__beN z@NiWVx;r@um31=fVsINSfJn{wXbh~oTnDv(8#cGGlv!0I9E!)s&ZyVM(=3y6LQ+2H z=^E7GTMjd_gY5FEqxg{%!@A_F`!1=je@s>P!CJ8NK#`QzJFC}TSQ{vHUdG=I(y)@R zk2W|P0`|YDRyRXWp*}y#&IZ_n`_qWJzD#~p+-*_-M;Oxm`GbLZzKcC%j-A4yW5o>W zHR&g~WO^dl`jh;|G!*9wSDY+n9+~lEPv4yk{`(~q%9zs%#3|#||w`fCW%P1)0 z+kc2>Wf`aS&XViSvz5%NGFkZF*Hf;08vNDZxl|rNE6fO%C$$)^J*A+*@!Ji zj-I+$Rnaie`YwmW#Vn)sO2}1t+DOmz5*pv30T(VWXzTNXrm%I-$Z3;YMDhhR6W*q$ zhe;draVkcbib3iw6pE*lfygE)&`q2CMNCQVIsR&0FRchv2@B+JUTLsAO|z{9Xn^Pa zv`IX#fo}`xSs_DwE3<2SSBHiZUtcDtQnFNp4F1$#I2kWA#cMx2aDHevI`Qe!@E()V z?m0rvdj)ItrM3b~rQT0VB}pNlp7M3>aajuXJ`s0Un0kHgP1xnD{YqHQeLMtD5K46bb!<+jJ)NPB_~`sLsw??m`~z6a>cI;Wx2EGbYwEb~MJ;TH4IQY7dFvm|F;{RNHyyWoKnfl`HFcU?NzTWJJ%h9DQ_0~H5 zaI0!}Bf?xY-sWQv=gwe6K>;`@T7(VwM2MzQ+yVr~CFB~5KPs>gx(ke@QBaab%>V-{ zFF|v`C#jYnz}D0m(MVK&#U#fk(DKq?+(t1c&0W5ogdu!z-osENP` zNVx8qEFID(*S!-f3bU&pR&E{6W_CfljVc{_dI}nwH^(#@vIshjW3a8VVHV|0rw#(f zND$Ro3Dt?QH|S=75C<=tdxn%0nA@15GuoY1VJ{+q0N%5t#}hOt&oJm!{KHtycS!L7 zflxB)--eVxw(ca0GCHKBLa$x0g>~b$a_cxYfk1+H3hSw|A^_wc?mf&qOZ|iaU{yGw zmGnJsNU(rm#_(Sg%cKLYnY}ubQFw5G0HoBHxdI_XfS>^T15NogvUst7A$AQ7DHK3h z^H`|aq{EJ3^Dy>5JI|*#X6&n<+#CtF_3j`agEhljhH$h`(-9+mwiT&A{!Ig0Dzz@c@8m? zu6FUk{j_wJqQce-QgKp9fZu(mv-P&eV0c}x%(dQ1->Y@dh%=so6bv|I2sWq%rS%j- zZJSbF2i1&`{6aq*ga_cox2$V1OS2{rCgLQ%G&e^Td@LX!nLr)q_};|r(OmJ| zSk2nea(3l7i|fR8wicJ_qd3#J!@P)Jlbo(v^H<*NL~F1j=A_Q-ZXDWe^fNk?-f75{ zj7j`LSZbLH8Ngf*^`^9%ACO@Rc*XuIet{RaqY%11kNd5ImvT4(O0?F>5A7to@ z=HA%fQVPMfWHV)GYpO|ghbf;qqKs)pN46pT7&O-@Eo{@!@W)ae&}RVx4XrsX4J%%H zMz)&6*0_i@NkkiZ8^EHR%3iwNfCKO|lIaXK&bTjHm(MXkK$??vlE)^s}ep&=g`I#$#D1}k^BV+~u;(4^EHjbjXPuNpER+{6}PCz>E z%a<>pqJOMSPDWlu89wR#u4|$-o|DR(4sSPiDx>zjnyY(uqSAM&L$2-ltUt5iyL_?E zg@cRII}LbVFC9^Da)(3*Y@RH9ds4Wo)TuiinQeoI*Tvs1e{$2mx}#cq9>IHV;wf<7 zDA9K^*SKG1tWctQ-{vDS8Z2i_N19a3&ndOaI@bO6T+j>f@p%sBPWf6g`THmtv+1aE-eHZ%k)W6$*eXb)a9>5;zY2DhxG- zRjUG~=b4bI?|N>W2%3F#36i~EMl|!{oX*edtc)A?s|u!C(;j4lDLn~40l5iKg>fZ! zmA&1y<<<;>qm8JTte}p5#KlEa*3nopX3$WzSe4MhW}BK~QK{cK*u0>YI7xZQ2>ytR zjj+Nk=@j%D;*+TTU4^=iDPr^UGtgV`m> zR%uom=TjI+VD~l_3|SkWPU=HgG}K-@yqi0MC9`C~e$>iI(R{Zu2K=&!lx+1i77ZvU zIW23e%da1zUtY zWcb}rt>C~2(laz>7pT60EdA)JtP`k9VlY3&bC%^Q6n@b{(6bU3ipDy?+RqqKg%fA- z2pCvrhStqBPpVP9bF;05nGiLZG(LCGfzIY%wLmc%AO7B3O=HQKmex+j_EzU5ESe!b zlN*L^?~jqDZln*Qa&m{8Y-nAOe1VU*L^27NKc&`J-;%+=UUlH>(nY^(x{3vP9=dc! zZ)pajOnWFiQnu5BYqS^^qf&G%G{GPl+Bee+r}OJt88UK+I@!C98nL#;bF4>w zAx#sOzaOKDEk>z3*RxS|ras8tKGB4c1d)LO63hEU5p`EwC(STT=&sJF3(C9{6YQ)i z%l`4+ibf?qa2R+Qp392$f>HpXMw+dGMq{rsXk-1&RAUh&DQpaSe>DXOKsiNG!m8}D zXjy+#XN733Iy$ica))0Ym^!2OX57?=I3^S0^S|$CHXOj0ct*c?4}2?Ft;G4}*?^k# zhop1~*%4&B@i&>jM>ja^$j|N^;G-dS(p%Y!1sYZ)mznG@4wSbLmnJ2<-}^FZ`<|4} zbdtj)iO2H5r0iaKdp+0{+~nq!n5j)+UCs}U+fTqe+ zn#i{@bJxj8^s-tyZC1>&v%2jy&r3L)>rFM+-opl8GZq+@RQ~!W2hC1G_IDI08HPf| zj^)Pio~S;16wsy}JZ}m6coL45{cws=Z-GqW0pw|-mz>t@DPKxEb(BAvt`PzU%E?|S zA%HxPwY6)<;Z3Y43l@J=Moxt6#317UCXtFK0T{ArcUgEkYocO_fZI_%ww}IZ&8qa- zvZczh8&pBq*o_sl$AhIyg&~1_Q99E^tPmz08>e^#2?#LPl~95moZynmFFF}D?S%vq zKSf0B!P*OIHh=H?pitj7l2hZLA6wnySePO0fdP=DsEPntUe9k9*T|#z4L~3&HoOB2 z7|tOggtM#siF^?m0VSU_Jc;6astGA=#D{``y2gd9K{+=VINr$P1}1;( z5OylrK7awOaGBahB4l0g-ieM_v3uK&!%JUejgHuD%ph@?(o=8~vxcM0ppZMOrH*k{ zbSN(g=|CkK9&+g@V-mZjtk4k}#ZQldwpuq*=(!PJ=Hqzl&%#wsU4`N^L&v+E&)^Ec zVcm|Q%kA=lFu)R`I_JEI5kj@&3O{U}yNP1CzusomU{RCcvWht2a(*qoyc_zfq8+C!jBz zAAGEA7_SPg0=^ z!kfp!cYm1%^8yC#Di<5(l&k!)4%!#2Jmq*P*WXv^X+1{!4Dr_=16vcWdk*hk^GOFQ zVlpVP#Q?yN&(>^Fb4}-_8GR|n^YTo(^(Cuwt$MpoV+SmX^A$*odCk7|StIm}h`A6A^^mxr51^I$mjLa++X*#Zn zCA{+lJcJ1pB=M{TDO^$(=tm| zEj@vSpL#?tB?W!etQB%9kG1RWGQFu&7UY9SM1q|50$xXm3^}J+t-BP`@`#0o;L^Of zdiE|)yOcNk3x20LI*L-7w_Q88zC<=HVA{gxEj(U!Sn|h!Sq5gy-F}e)1Z3LO?yp>? z)7d0~5tIhMg8_aTX)MBMb=1^no*520akd3$Mb6>b!e(>&)+a$|?`@8jROUPLr{9On1#O2`zQ(KbqzRU90I;e9%J#oZTUDiE?4L zF3o3}Lx+zX!tnL!2cL)&P0Zy>ytm?c8}>~?k!MNFB0aP*iOV`&{AMh#BIh~ax>_*J zrztowl3Cd+e7_HR!aRS1%qo7ZVR@K_9H9Ie&e0G;^OmLzrk z?YxCOvJ^D%{KoB|m8HjOGL9r_E&X+^VAv}KA%h6rCpCYg0D*Q1gO>!JEUZ;UQfd*c zcoXUKU^)P=*JN* z1^r%G$o!lQTWQ5)D2^TEn3=EZOB^oGFE#e) zGm}4`ba~U8Uj%D5X-wy3Vgcijv_d| zrNRSWNdl-LHVn*@m8o(cZ2-tFpUu9#IWTbGI~WzqS1Md`v_AlB@1v`yYbQP}z>q$h zf&r8T%Z@fb6YsMS?7KeSO=AI9<+f&H^EjLMF*bcvF&`?kLk$s-SB`0|N{e&tZZ= zb~;tq7Zunj3H^aE1M};w?!F6wy_T$){1~K*v|jOc7_@?iNfQSJ;K+t;YIdj?q>z~x z(g!ef`qxzwKfMH`h8nS}+kn-}7_34ome#3$TdwSu&iG&&1 z1^-oOtB#q`C~CUImdDwERfiV*;o(g|tE|c725EYO2k;9&J~oDm_~JK)_#q!`oU0C&Dnr2*kvO!H;#xgV<^R01U@BCeXmgV9~p&ngMSdfT6WQ zLD2hA@Vf4_tY3W|%F%HaWm8__1&0-CvJB&0)}O0vK4^Dl%(j7@zw+53p27eJYVSDS z`G{*Z>m1W?GL@o)hD9RNsCi0*cz+}h(hKO5?C>vHrC@c`yOUdTyO4iBcD>4BB!>?l z^^YI^G9^_&#McL?TV`0Ot&?$AzGZ?7>VjkB>DWU5Wn&^6{mEUD-bZ5-Xg^fZQDNBi z^ZI%oUPdZKykF~5$N)fLe#68G^%U7aX<cGh_LK9_yi83Lr7sv^aC*I%k>9&BfGNcMaq0 z=;GX>*UtHrBM2#`@0o(Yr zUfD49zDZ5p#hu;dB-nIc7c{@Q?Nd{%vsI{@eEdk@eKlL4i#x5`n!Vm;eD@yA+vwDG zt{T^JOdp|*;{6_$Z;q1nEg`YNHp^o(Gx8HW8Bd4Q8A?JkS*!~~+Mp5!=%Aq0^k}C6 zQ~YiA;@QeLcF-!KsP$Q#f*9%<*@XOjTBo&`|3Jl`-M-R2}ge| z|MlHgPRuh3NdErm-~D@_ERnRp`Wr!9kXZXqCEsgb#2l~Ecu4ORCY9bIb9-j8(4h&b zMYeCv!r@u55wO!fNO>OQw8k1n#YVjW^jO>*X&;oR29dt%v^)5G9Z#p&Th&Od^UB%W zN-Uh?i$=@pRJGCFb**?AoY^e`x%DTF2flvzln@jyD!9W55?f>Hom_C(UNx*QvVt0_ zYcI|5awzfJJJThX$4m@9Rj8jneNgE)K2<$9ogCpehgKB0zlF=liH*&E6)5?JTBm2C zJkHjTNsa2k{`zD`MeOkwkSwZD8l?&3oN6$RkUngXz-6x{BHha-p6_Q2De6)RONy;%^wyNdaQ_Qs&d|%%}<9QW2#=v zI^zve7&9OVVl=dlZMDS7S{SULCXLd`$?hrFL8C$q6DG~s(bMl2qji(mSuDn>SBn>6 ziw>$yZ{~zINTdy2E@uopJMoBk<^Tc`C4z%A?)CHi(8R}+zfgWco*H3yYe(VSrIIP$k_=Q_(E*?2{q#%+Z1_NGuXLwv7o2Gc+9|PBm3PU zkwYDa3hoR1QK&0RWQfGAX1rbUoLD3lX%aMWt<`QJ`*j>yjYgW< zB43ojVs2@0pdvIvi;3DGp2H&TvmXMkBnlu9PE_>(?rFIh z$`Fnt6}d@ejDwQil()H~hMHbND>qW3k-!4vmxK-bMs{PSYFyQ4p;{ID1V8X|Y}Vnf z)X}BPSqum-#TIBts(06I|I1h$<24=T2PtCG2J(Y7(W)6RE}6PD0cO|pqvF>DdhHU} zI6hDgAiR!ydbWaqtyDa*pJr_Un%IbR<)rdsEGZBoe`4ix1IQO#qG zGWlv#KgH6cDqmDtRnDGizUO=I@iDr^e@m#qL6PE#^A0nI_GZrZ+fC~| zYG6mc<=uLH#|bZf$E#QJbR5AGT)`CWqm%rTo^W;>m z^*T)b751u)-ExhRNIZ;RlYApfM|b0q+SzM@2FTa6=bIY~9otQMSTow6WyKx8z-5@r zWgW<+!1$f4MF3h#CF?gf)vwmJp9}n0-MLfISvH$jDL8pw7gE!U=6b?q<4>QCmOx;1 zL^RZM(5GA!(g|N1N7vx;VAvm`rt>SgGs^Lu%4GSl1Qatj4_`Bajjc`3=fjI0q*NBf z>oMj>Mr#^VSc2?43_ATQbdkyfUlU49cTsc!5$^xy1wcav1j10YuFkk$bSLG~L2i1z zW~%3V%>&b#@_`>&JX%yYWB@Fc2wIwgkwt0$=h7ngv}=uz$3}Y?&(&cc`_e# zR^Y3(Q87uU-lk@(u}p|!9yuH(xLl^FyZL;c##=dWnqE~iF0XFm!LX~UQNRDYvKJ34p;!JW-CKSaVs8cQEi>++#af57Sn2XZ4$iVfuw~{&~FLjD?Z`%#f|vq zR3yLn3Ylm;VQ_%+8{T`IzwwE-TgD}}4DZ=A6uWreYXT?D*dpR1NAJJL)r@#Z+YTt; z&wg69M?)XXI|zhg&Nh^;p^&m^eKM0b7qBT+MU}a zrmoff+8+#B!*3D%Cj zMRm4Bx*bI?ye^il$kZpTfLx^J+Bk!%8A9D_UHE`zZxVZRxq0lnitN~OCa;Nc{-Ro+5%voDUUs|{=3_cpMT+Z^d z(Zr}Mv&1(3FN*iLp3Bw?#@Y0h@?y@EPMw-s>xKr@!uNwK^nZc}LGaTvJx~iV9V2`Y z5K{aA{J%g*tZ!Ug5dT6Ak%SI2LGAwi@&5>>`hS4-)a36}8ff&0{oihfDQ(J%4txDG zc_z<3ELvFK*3a;Vyp`wV)lE}g`K1O81pQ!E=brB9J}G+cu06M@?r&qE;d7z~JqZH% z9^t0oBA#Yw^?qkH8TkC{)?}=c>auO15pn>=3Ks!KXj<)(?diWHckge!zOEPMY7Sz3 zPjG~>b&^|Jzbgp#+9-kM?0{h<_AfzPkRU|yKPck=f6(3k*D+aZ^F7wGZID8sD$9N$ z(;5KRf?~&PRFjkK8)~-e&LbcC5~z@Y{jSLg2;d_=ziSbsWk$2tvlOY8GreSs!?&lqKOc^nt=7ErNbke6 zsQ(kWgb&nT&&zncs}w%zP-s^_XGD;iT6ibJ=! zd2lqNJ45eV?g3i_?Xy>wgCH7bw-A7v-=AqSu0SyLVrdR{;de=MvF(PPMXBo83Z{)# zt9CC9Z+Bn2tZq)9@SKjrMJb#Hy1>g>P*|Ahly0-fnTN^(@4)SOa709eY1Re=%#AOB zU9sQslESh8h?VIrWe>8{!ETl5RihK7ZWO;%9w^X(53XvcnWZ08n{Tfis%IugbjxAj zsm)u`4uG}qMR80StN_3%KRtsfSd7asfd9V9oE74gd7Zc!!;I%1bN_Z8L;mFCRG6+O zd!gQ8w=51bqUPwM{VE?pTtw^LkSe#Iqx!8rtd)IY6Y7$jZGAT=Lkv}i+NZ(Jd98YY z<7W}d(__-Bc=QnnmJR@*<3zG+mAJ>_pN6fU5n=H?Z?>Z+KMPq8TCDMzzO?lh;t6yAQ+x+{`|fDXZR|ug|y(t-}pUAvyfs zIam9BOR$Fh@-embl?MPE`7T}`<$@mWiPt=Tyb+VhYQOlnQ0~&yyxXLsafLcv2tLL5 zm|ug{eF1>-{XYCDZzFa5-i|4~%XwGFs5|i_bX(uEux>EkP@PvHqcPWX5YTG$N(X&u zWMm6-m0ol8#qdyw?yZ?)x?^Z7g?r`Q@G_-(=UQ$PW7k&~ngJDMP#Y*14KzlBj zm~L}?uWOrpd;}&cYQh)BbL^_N^UaG|->$KfF>P|0-?diartr?>8SI^oar#~Bmb}i5 zeJ0zvHWSiU$4UI>k4HnioSW@@{zUjbXPex$*xIN7m`S`fO+?qMy? zt9Oq35#}=QoCVL-Sgg7SJ$?Ew{7f!c=4k|y#O)CHPf7Uhas&E&o!)&Ro;TLzCWsjKmm9=v8 zTL?d(Xx@#+liYXbm_(%aA0Ifs9cz!Mf*vume*wd~I@NVb|9bLG{GhjFwrleBW$f+m z+LQsdeZQ6b1B_$buUBaDCvm!pt!iRs2?eiG&GE!2|JxQ0e{@?!_T8N1F87jcHNKCh zmex1-eegk7dGW1Sf5c$kCy8HqF_CaNu(8NdDM)vR#QHIOs1gD8L!K9>Te8nhPfNaC zoVJ?3ndkO9FBK*)Rug__-Y=eW>e3SyvUrva?k+dwX2IR$fM{yR7p0d7*h%{BK<30HNCmJ7nJfJ4^o(AhP$8Cn|K_vd^LLdf%!OC}@XBK?N%ew-( zpkF4^ybWH@jL<;xoK_YlArx?IjX4>SIPdAhY-#Dy%+8A*8fg6?-YVmz0#qh}!#`?`tIK1O}p z5&U!rH&`7{1iKHk_*M704g9>jJ+zMan2RxX7tHD}{blAM6RLh!eHT(8GT&OgQk!+R z<|_(Rhq68bj*PCm(v2gQeS=-~gcn3g8IL7ny1o_|tOnN1@|6GMSAHw5 zSEZj3W~E(TJW~_;sliL(tiI^=d)Pt~#tb^g>CCPc+zww&K_3sl5zHIx2jX{9;o}}I z1mduGtc>CgfJ`qvR{S+4!|`msprvf_eJ^#ZM*h2`0?_bWw7;+XU&Os-R2<#AJ=j=) z5FkJZ!QF$qySoPu5FCQL2MF#ijk~+MySuwPG}g%U``-WW&YhVx^KE9p{il~MhO|R@JORV2s%Zu<9X^_G0%ki0h6OKItx>2R2j!?5v>C3y z?t;6f6)K(i+g}NfU))dtR0(SI^jka7ET-yEUHU#wjtz<~GuXR4-;uOk)ODErr#8v0 zzQ`*H0>=_yJiJ*19$#KeFDgN%AA4+e){+R@?0ybH83Ez7$NMFa+qTiOtx3`8v2UV3 z%j46x=LvX*UgKgPnYVWFy$pP!l#cqU&#kXxRUj;UhM^MzMNGUguw$5;!o(E*)HUcZ z@c2O7_Lq7Gx! z{!0Db>&Jb!{7`B)7+aY5j{W$|K{@fo^Yvpwqvw5E=Mw)CEquc(;I`utB-mY9>eZ?L zy3n9Z+x4_=L%;Te`=A|$Qr=1fy_Mw#rwnd4CDQBtb>EbK;t6zBnMUT*cKeLg|A{e3 zK>Ovy=aIn}@9A~)X)g-50l$=Ej+oHP^iVmmjd-4!(eGkZBFAh9BGIa>3|Va!1;YF7pXw2673;9_!h$)7=!C$Cr*N7!=Q&#_q0Cex zMMz*UjAEBAES#TfM<#!!?fn?nRS9K0|llG)m})W&{S>2h{2C4!oeSa43?vZ!&!-D6l5bhHujr%oop zMr!@FjVIKILxG^x?PjSw%xwjQWVeE($>rEt%f9=9VImMNBo{N5Jrdp9Z}hKnus^>OaDn{I zd;5R{SZTKgRg-3i{3fsn--F!7k-u46E*KBxq4t`#pPJ00Jr1->fX6a8vyh$Vpr>C%_LH8wSabM2zlK&Rxu#rNlR}x=&vS zK*ZCjP}SQp$LGhw6SOAWfF{Rki!S+nLX##uV)qa&Bkx1R^s-JY&A53p}Hl znF+lyCb;h!g`A@?@x^vd)WpU#d!frbc#utMO*KRzW!<5g9K(hHgYIx|Uil!_Tv0`x z9wBON&nRQ+#rPPX?$TY)GomblEILN%pHJ&90MmZ%iF4v2UI)A0h<&b{mBy$!%50=nb+5MD4t3ud_ zq&PrcLT)=(Y1zF}w|9)HQ=6Au`iiwwWmNei@g(Z!Dyr|T8%4#$hh)~7Cb9fotKAPtYy8z_P=nJZwgz2~DL(6s(Q zCXFYoB9tJ=!;4XF;p~u~iogCLNRDS#VUx|r>+Xtp2j?mEBcR9g%F?TUU>-Lxu~!FZ z6@m0785*_W!Q5%IxrkqOH_^-Xg;KaYso8RABiAF*b%f6RX=#hdMXOwqS?i)_o7KtY zeabviS*960c17Kw@C=i8&Y8JrPDEwNW2t3zd2B6#MntehHeWI)3w)!ZCL}y#NrooM zcaBt+FNY)nz+dq5chns-n853PV0xj`e$38>uKxDnQ{QIY(Y|@a6Fvdz3#Bt7$L@X! zO{B>-dQDLRu_QX+QwJmAUcYP}t%lH7g|)Xye+u(BJoGn-?T9hC?h7Nn*RQ`{w@0aa z1mcwS-h^4o(LLO4lsiE2BeD{gQv5JWdGE$m3P7USi}};Z&tI(N?E?c{KB}cc?>kp! zsFkWeq$N5ADrwxSNQM@aeo%_ZV=`-IrmK==3N|x0e)iAGvrjpm_40~I{;Kv6gb=$+ z&;^rdAVd?VszzDwaS!Vho71YHe>%;)+=}(g`6GHjJkM2h4zWRYR89T2mAuC55|J;N z?<}IxWLP7WE&NnBtsi;h9DVcm#yNuL%XaMyE zeeqhw`;eKYTt>@Pl$f(F4HjJEw`OjYa&_i>0Ia#Jj2#!z+-DBLjgrIZpnRVTx)y`J zPP@Bj{iKpjkT~=)*x#sRVbcxdr4228`POlF)^cP?$zT~_MNU|O%R8DI_x7YYX@%C# zO73#8a7#7FHwCS_4!^Ku1sT<72QOh!8p)utcD(g|`&D}e`-=gij&D3T-nmlTDga|1 zGO`wAt|OYDeBoNC%7;^(X8$Y}Yq11KZbYYNEhcSOYLJiJCgI!Ru}z}s!17nylhoHl z+NF9kR89&O=ST^Gm8IabU)#-U!}i8m#%zIj^SbIz2@;~C`Ezlj(et_}M(9I>y7ll6#@Z4q7?aJPwOu5g(}-E1jW(!`zx^%fY2eg=o2n)Qa+&&)kBe#8-fC z)=@^Ze2k8glv8RDt%=NtsPafao}|2Pk6pqpTOFQ97)GiLl}h9v!-6^s7TbO+>g1G# zS9~j)gs>2+f?)r*^Eb+x#NoAejhI3dCq_iCwrLBE=NG$-L?`K!Q~l(Xz&CeBi(lt$ z0|}q}$I-}+2($HTWr03PP+heTCFSm_NQE!n6X6m4Djknbqd$14vvv3lgiV~V`P$+B zIBPzJ*;C{`8u=*qw4i=10WRRL5fGKD4P2=#u*8dNe{e$49{;T~e)AmliJG7j6a{>% zm*B2G1_Xm?V+b23PK~LE) ze!=2y`s7qK;?Y7njP?1YCCPk5=8em)eb<$Tr{QSW6q}d`!RU5z(`&JrR&ZHh_&P|T zc7ObFP3r?SsAAftx(D^pN8rBi8N1eK9GWBl<;c(!@fb-QmQRmAc51P;$Qywz;P$ko zt@J}7fCI{*uvs7zVl)kqbfBJi6?UrIaIuhxeRy;APg?wJ!K8b(zL~Y-^GtEf$CJ4+ zzfKe>wcNxHv|YQ9gn_dUt)L$tJ|{EYG~TP>H*^5dmGZ{Wp0S9njIYScQy9}oL+_J8 zbsd;w?B-pL(z&OO6oWp(^&;m=0_Yi7a)X`m1sy1Jk_H{(636gOPymWK>R&C!%|7xN z0N{?VgO&|$oda4mkm0wJ?~suUcTr3E1fmzd|A?(rP5KiLJ5}?kH>SZ5gGo|S+%08a zTsZ+UX;XM&Zf46Me{4KUwKpD+3}dEQudJ5i1jZmtTp4j`+7d0V2{@M)XiWXUirQ|DCMA;o537R z#)=%S+;0xKF~yOh`LgAz77nCYpI->lhGKk*s)WvfiePGDGp2h{MI%SHlOEotr?pmG zw6Ey9@QzW~Xmv{W^?hD0gi>SRP{#+>sEhg89S4q})aYWVL&Ju>I^N=cMjwCImKgp;iX1?c#_B+-sAm0x~gzT+Yw<+y+IH>I+`eh7a6E;=b{b4=(%n z^xA28-+z5Uj8US_1H;oR$2x=XB?vUzzsoEAD3V?NR$n^TZ<7$}7OIAN^?)rKbCBn*zn7jreXQ{fL^vJBzm7!Df6>m=*-NeBobTsh8yy868UX{F zTRh<5o8zz%!KZ6w8KcX%^-NNlS^U?%C&-0VNz?c7`C*f|zn%y3SKk#K@JfRb@xT`~ zcP%c09)D_Z4~6q=mcLa}{rrM>+W0d3?YkBRwUd2J$u3DFo>FZYv-d8&SaF6h8KRB> zvC7Z_&Y^5{Jatu4t%Ig-&)9j|1D4Alc#bgS2*(3?37@U!gB$%hJir%f76lfQnU&^b zaeiZ>{0p09tak!zP*_ftVoHs*<{f77q4vF#GV6!PPs^KL%I`f(MfFmlB$1=5ot`&o zEY4Ibvy9eruvW6qY|dDu9sRb;Wti-@>PutV2Hh9usjvjokD< znX%HJ4qP&F#pdwbe>hv3P2=3w_P|K2hSKmfYF!lERHhpq3C_{5k1m!@_NqwC$ZwYM zpcl54ZS@VyVA) z-2T`~zO$ZA!2@N*8*tUUn`ubNJI+PQI{7ghueQXuC8E@oH{1Gg^!fd0jGR<`_>BEw zp?tTKJD|xG%_b&NFE$aclXMFP5YJ(ibxL|3r~BDL^OsE~9awJvUAMuYd+!_xGB9aR zR`KZv$j#Mm(VOqrZfna;NyMEmKn3sd>!1DLPy7J5UQ%eRq`FVzn4|fe)o41`nC4jq zkvuI?@~~IU3a>v0LI+C(+Pp64(?frLvU$?{20Cs*h2(%u9?|vWAg8;Tp8esWQX(rg z#O|=EMO!XDMSm&Be7l(tze~uzH_Q|pnY!a@Tx`TZZc6{4Q^(}_1rFA=0_-$eBBrDQ zA7~%HK17|ZAK0o5t-~~xo)0cO^*8;VeBP8m9`wlIv#IvVqj7&m-ZNz2mzJoH5}rO~ z!zIaw_1yXpxqsq$hhDcUs*%h+PCrZO-D(*DPbJaWd;vCT(yoAy(Vc3**A5Z`(|Ot$ z9s*_V8d!f_{FS7)*o8g*MXvqy{M1*k?j4kc`w;IJ5C3}|X7uadsHJH59pa1b%}Ydl zDIwsO1V2&{+MklP2MKjsS{%>f zv?uvTXdkW0GUE3mk{y~c;14?+n&Q1rlMbnym6vCR zFL3!}9R5@|TON;kyzI1brnHD~<<|8V4?A{jxw*Nisi~O&rdfSESqHZaB?sj(eZnU? zBJ!UZ;_b z>@v{ZiO&y3tb`^?YF*+P3d{W{ZWydV1q$!hD}ASjw$J(sg|1o&LJbSKx_UOpR2kWF z>6UW=SK*Cce0+qw`7fukTaweS{%}J1pZ4&?a|^a3<(F>1 z^OG)Dj%L$T&LZVtS|`CY78cV{>Dx?&C-5dT=1e3i6T%sE>~0U<_)70(vl zGVnQ>b*2vsnQKA_?X0MrRYxVJdjU(rE}L8FKLb*KAnA`1{8spcD{1yyJzhi5jcd)2 z_It}w${+GA|2*2wqJD-p(g4NWm>X1f`pFbL)}cOBs^cLkht7`=ef4plEvhUY7h&B|^lVxiCzN=t4fjxlkBr^H)+8;hCdAY%?~H+@9wX=MYV0Ifo5 z6_5`%_DZCpgW1yyim(DH>>sePj)cMP0OuORE`ncHiZFx*oBQ+Z9P4G?O1A5~P`yey zx!LS`yIqcjH$l2}-HPK>&2-H1l8`N5$;qmgUc@D7hBOB8IQc9*8SsQZhohOCraRnP zVZAjzUxEhh7uc-H5(qh1)X&wvp=?V1zT(18&9z=xMG2rve?pKdPZk0Sb2Aes(0KusU_DkUuRu$F% zetM_>)~T*(sJph)DHR!R{o*V0h4hBgKP8&#%#crk=IT7D00zW*7MYFp13Cb*t+(!< z;8qcNn>}jbg8!+ayBbLC)Qhj&+ze2-0J3tUt_4eHC(5SlE=6KQa z>OaiA9-so!8Z0_F_Yba_gMtN?ljbrKm$}N5edPyNe1L6+`V7X-iOmaI$_ES6A z=p7Pjt>(1)Wz0T8zJ!dW`x}ii1)5cYo?RhpP9yOJ#mH=(?*ue%ribOg!pqW7eQ|qu zKpkcl&B6t**E?Vckc+lGz8y8OWs zoj8i}YGY_SK5zVQpL6#BOW_RA z!YI?3XRi_t)Leq`-_V=bTkoW-Wmi&z2_5{p7^YV0#;$dXul}YjZ`y(9BAVSrXBHyw znU!kc78qs4$3~V^ri zkDR5SwHjT1nMn7WEi~x=*yN30p&x`vyLH(qRBUxzrReboGFIIYQ&~!IaA-glQ+O>i z3Oiez-GhUZg99@;sJJF~H4-Mai>2!(Ok$NyP%2K_UaN>EB4G-7rCv(9ej$lGNPEmK z=3yjIZ-LdC&)mC@bE(lXvqUxF*@&5gp2RSePi~a=p!_bs#`!IAlxcAHz9ijL0=A7f zH(#DuM#PGqyzf<)&!dP6bcz{b64!R7%}qGHVte9chn=^0rqDK4i}4^pAlY9q;ZT** z=6ue3sU(lm{>=U8ouXwLE*tTrnSRo!*72L&L^wMIM77z^Q>!`E88aU2TE6&rRH8!w zHVyE%dNCn~2dwS)Auzy)L=Uo@ve7V7wEkoEXEmVFfr3t4S=;y2TRADfh8e!hv+mt< za3+F=0>mdl!PjMLTP$+BZHG?<-GnNc`;D)?T*C(G0o{Z{{2zAu)@C4Ku=QDC<*WVk z6P;T{C#yIEhkK?I9@}tcA$e{iC7ppJGhwY6( zQ|ppZy#kq6LdA`?;#IeGInZS}e`IZ)R_)m-s!3J{71N(4~2(lj0p&sc=CBXPp7dwv`)76ce0^W$#nj)SCO6B@cKNi@4LYe?+H^ zN}rvVerSqaKR+L5YaSW#!?zN@D*^WSlB<+S>3nPdDi2znuyR1P(J-|^<#w@g;(!DA zj;<5NfmcSnur&`|+)CNU-??W6h5e*-TRH!FfKSUTQ`dBfYsP|~rkB$8bWM~V}N zWxk53?@k$OLNC@}l+A5Ibuq2KhRHDL{k}y_gPTJuH2+%(Gr-`wIUpcSk0~fJbMkTJ z=`!Kg$bqYz#?DE!hKh(@J9ju3i3sC>A(GaEhaS8L(9A4GTpWoUi!JT|fu4o;8@NG) z^ViA4pIIE94@v^)8ZxR?^`2VM0Q9F7>8 z-v*MhDH=Kr6toLra^a=YR@j_Gf(Tqr;vdkWUtH|_O6mAmQ0j@fI&r+iVeUzi+hAV7 ze9IKrleGXz)ERQ0Trg=J$uaI0>dr72yDYckSqVvIjM~FJw_PRaEUE~xpsj+BN4#T| zjYZX-T-EcZA*BcLw=|p}S4Sz-7@ZUe(+&l>hApRa%T)eR&kS-xfb4gkclJ?|n#!(? z;?bw4Slan_d|S4fI%Y1#r%AURM}3++27|RN*UuI#p?44TpB{d2&S!h2+A$5*I$*pI zUx-AhGvvitn|_lD`xLDh0d z$l)=f<=Hs3>#BAG9la2m;k&rnjxWv@?o9(5B0!lE{x*3{rvK`1M>_PNg)|gZNMAb1 zc)NOmI~1VE0c7=y0piyv&v%MF~v=P5nEDtrLd#{0CNm|vexWg{R66sP*m|!v=sYf$I??JnoAk6`Z6VngN)TA+kW?hn>N-RrmSv14x>5b2-n(Q@l{-| z!=HDAia;!jtF2Wgkft>Rwd)l$E9e{19ue@>z1>m)4FCf)UQtc>>wm#!Nm;&$a$TPb z5Hz{`zN_pI#);a}?zxLf@af>-GdgswX>V^(l`+y3>nEJK8H4P5*uhJpIZ0S4Xy5L9 zTf(AFd>i%VZ?(&h$+U8HKLpKxkA;r++emowrdu#PsaWu5g?uqK1})=A=6!^qpO&Bb(y($f)g2nL0lY)%_%1?JCv+v!b?!;zx;T#W?1o@ zQyrWbTw+Ng8M*5l?>2suaQ0B|WBoFi*-!SCGP;1+sG1A$Q}H?00X=_N8577;E@mrh zkk6g3D?7lUjUPTL2_U6DrX>wiO;rZ`AtSFD*}xgJ_(4a0s)QMyqDn!GOc*^PB00_M z;7Ob-PE*YDTA%n)-Gv6Ks4=UtN%KU4?nEnO8+7P|d3ZyZpx6^75xN!_6WY_)W)+$mPPr?ILW(^h9f?Ti5;`zJ-CBe-`==`v*t2R?|K|}%?10R#@F_k!c2$P z8nv)w*`|4|f?~E#``H~!YvE^9Aklbxey>^i2V|KWuLM3$! z!u>^H4hh1ifGB~#IV6&@u-R2ot5niJfvS@Z2KHNJyBwYp7)VsjhC&@HDcXNHIS(}M zt){`G&1;>#aWRU_`6z@T;A$ z1EMhvlfR+daokRS{!j7i_*Rkyz1tsLll}w5E>31CyRGQ8^TmXyUv6A^GpZKvHdUKD z`}qhytxZ*`{&7aye3-0XYtodDt{zw%-gsEmk-rr0b&B?azBhMNh{xAw?)wJtHM9Mu z)~))QIj)1t$nqDrmO@Rf%tO`lS7JSOcHz?Qy>Z+Eo|1W_1pJRpIlZR_|K72}3kq>X zdHr7r^TA5Byr`qnf#1za`no z{%u>|Pb0>snVlO*XvTrMytd7NCpUKJG2UK9AjMx|9mG5DZAj; z#TL`rcaOov`U{;2rs30t0(AKec~-?b5Ww;jYGJ`Xm4w>TLrU}Fx`ged&g!>#MO9_( z*vvq;R?)~%1&&EihJbg2tceX>Lrr4=>+-Uii}KDK_Fx#xqc_^J#MJV6?a5vDMK8|!j#tJ{_b-Elt~g&st;>#O9`orRX@SQl4lPyi>r*O zS&ymBQ)&LMNIhbGGBTe_q(St^#q>M+ z*dem6bhYaFgsLZ5uj$Y6qSBb96D(?@^!BMbn5Oh`b9x(Td1PO@nK?>nXw*wt@xJjA zA@Fg`UQAMDwU>awE}xNMXTAZpw9+qdd`G`#4C-tQA$vlc_TTb&ybhjDCFlx+insL> z#qkR|=gCKeqJ56vTnd{#iSWikm@3m=S+$lx&dV-7o1;xgUS@yxM#3KZmZ0)5tXbU{ zmxJ^(d>S{jzhXdRwMMd|65K+-tw-PJTMh?{v8~GRrp zcR!qtPS-mASNZ@Xtkedy*b5`wr;LR9v=E!3?SI2dw7S8a1ickthy3r^MM{&>l(W|~ zf%~e5Nc#I9c-u-3@X*vAJY5ko!H*C%$J>__q~?w9uF*m2;xEbLjLpNR z>ir2VaI>PhqJzXlh@`d*3+=?j1Bq2=Oy+v53kHP^R8X+*@V&OO||quLMVFKPXt z2=3j#h?0SeiZYc4*_O9$rlbwF5(b515WI6Wcf_zOXyh%O@vPAqR))g(=B#}R9FV0s zP0lsVO_3yt^ErcR$82W;*&o#UZ z9##DdGsUcleK>luXDFGnN_JTDN~`gf+#PQ}mRagK=vqEpWcriA{CwNGJW}&X&7QvZ z*QGA@b?rd+Xm88J`KlQuD!3+qVSy~eNHvAYva>Q3&veUo&I9K&bNHgsQ7sZ?-*JWN zW@{=>sm#sne&W>`*6qeO7`V+N@5s684~JvyMc#A5>FzjM0`Obe_kt5J(W!6TVQ)rl z(XEay?ZG>!l^o0&W7GAzf z-M8X7+aUt zx8%M^0d7drcN;AY z%TidK^C4AEOCbcom4`!In{rraM5mCuGJ{_)@p+g*+hW7WkKopB$WrfCpCt`7UfXqT zQGdDeF7uK-<#}FV(}B??ELg9`-YKA#Or#(B?pmw%+=#Md;CVz)%jZiH0vcK=ADJJK z$Gr7PXflGvM`_Uf2Z#yRE~OpM&$k&sQ2r-sryITWHs+;_fE8+D<%3pthl{hP4wyyz zx>d?sPhg4_$|r*8dq-U4xjZY%u9Qp=e-1d)8l-R))&lg(c)>G8J-x@PaETT_e^#>G zH>UM`emh#mZakx1Y-OMZmX^E+Xr7H}R1{_UIf zEkd)%{{*6symT{(f}1}aB;1Bj9>Y+$X_UAQ_CLD-OZ7;J3^vtWimg4f$2fDh)5nhc z`}@Ym#y}oO&f#Q5-29dTD1aual!)|#{&$|k*mmp9!{F>~9)6Y&b2wY(!b8!{j~7M3 zK!lUsapn5Cl*@OS+PO8Y`sT$+AJhb1;Ep+(3cSmURk*o~#598Sg-$M3fN5L)%b|en zIZj^T;yS+}?A--&bv1Gctb)GBRDkKz#d@o7wj$BzD_1oasQxF-f8b9Gm`5WGfd1ro zxL%VUl+RgcLFHGr;>m$zlq8%+$1jXJ7HbW$JP6rIe|NijgeRS=zg?`)*5YR$`2H8( zTo1SY;dWznqmP=P!}nps`G~#gA3V(n%u^|gzg7(glg!<;d+(xW-VOLHT)x~Fvb8@S ze2%~dujBJCmTe8Aa4`Ws672WOmt#?q`4<(9g#?~Ozqn);(g(QJM zB6*zZjki59-!rDIyZk#!LmKeO{rz>oTB3;S=Re?UfS_$>_$igu+IjXxl3hR+_TLLq z$Sv}NE*&D~n}3eo=&#uW=m4fEn49jpBZ%VE9aw|^1ja`*!mMa#KB=40WMI>#^njViDDp3*}8 z|9_$B|DQnn|3?U&7bw3NZO;v?XlrXLD=Tw2oEXQ)u*m%{`i$88r__H6SEOq`q;T-J z$>X6yk?IQqEF?^~N17%Bk<9&A?>nHw?Sjy0U!$P-Ffu3tK6SgmyQ4imn+sg&G;+8O ztg`ZYop(b{I4H$CH2rV27{dP#_bVy`?;A~U2Asha>xTgG;^$%ruwM^M;zNp3a{j|< z1ZYu?U1lvDi7VOJl>WvK{FQs(qCWuPaw7j}(9ew{O+cr!`{7#``VCLlQXP$J5;a8E zmkM!R2$zrSV_Y;#iz0_f{Lj@CWx8KbEF$Xm_ao#WKK&BJ%S*)n`1G#?$ULa7&hboc zD6y*r?zenA=$1P;7@te?L1vACmoB_NC$lUWnZ9jxjB&Z6VxWZoz2?h)4W$3<8yGmT zpw0p5x95v9(~xFpv1t1WDU^lbTA zX2?RzGrM6~dskjG@{Gf|*Sum`Tlk$GyRlYylHR6gPafa&l1gNfezFNTFAK{3u+#53 zsNWLr^2lh$YE9Y<1K8isn!Z9IJYdir345x@&lM^PF-v(w1uu`UZDw@{6_Z$qDm=(Z zzc|DD#-jh|%n<08VC znJxp;%u?_P+mP2yIx=|fZ^!({_aB$LYjMo%Iqt}y)%am`Zxsv8{snAOa7y$5g*r-H zZX}~xzD&Zk!{7*A#E|?~XM(WU3f1|MeKNXC?zxgMw0VcwYd&ieveVkbs_0mZf#p-%%kAG^bGm3RIs&94% zAT>3<^05S-fjQRY(N;#{TG%xR^_GNG8^YJ=amTD3mhvfW%!o&|zb{fV1Tp1?Trqa* z`&_?2hY>bNGEM-ej=KGdM^%zAl}QjQ^$<8Nx!*#%Wax$xmH0b0;SbF#~uhQRa&SNADqJ^&6Z z@{W-#D*v>7)IYZL^Epb4AL%_JB>b?znAy3o`?SL3z0tvEqx1YkX**=8h;W8={I0GE zJux@0LHT1GPf`6!!A3DhGs}qkEYzS|ZU8=jTe7bf{jZItkoJA`%cWn;wRzM#E&=Ax z8AJq`DE_bpuD4`%5|hH$wx89ieY3NQ%@u}e?xwW@gn6h82Rs`ovvl}k7+XNDh+J1i zu!uA~&x)p@J7j*!K9H+dyQNGA_#g>Y8xH%S_Yka=D#(a3;we|>+Kg4RL)^ve^FaPxE$|A!mX z?!>DaJ4Y(hdxsip_9n!kcg^7a;`Kz?No&pf_u0NSqrEp(C$6*c_9#2U zLN+(66XPhjDM%A3rOz>5orJiYe>l_yIu1<0otG*tjKKRPVzhHcCMy>;`l!~}I?o$LS`|{| zwMj1%uicVQ3Ez3qk9U6xIWxy}Hd7rlYp8ov_UM~@0)?vMX&x*YAKi4`%wT=0Piv#H z5*Bcu3Xd25sURBUj-24al;e&9da{YFsjJEUF)2ja(-Cra`)!-EC=)oC^N5585R5Sd zuZ2Xu$_~-<;kO@Hapss*_PL_~6XOflb_@*I*gB$(6s&+uGXsBE*{gB5xk_$Al4x?) zo|_qd?Ct&}&crLN0IKcLlLfjVmv}0Z7K#__*#Q)j8OMJg)$OxL{Ln-}28ZoeK#t$# za$`^XQ_u_Kcwf1is$+NlEYsartVaynao_}O(f7Hpgd=>+JXoqN1atRy(rEwRUgR-cD}sgN$;n;%Ap4<`(oUiAUcdF-T_ zNAl6~$V`tw*hH)6KiSSIq#R>F&Y5h=bxYAH)Rf~ilcuO|zvgXSrl@_~;Xd5nMQFa+%d-HDp_I zy7RL!?EF}$QahnquQMtDpn`~$pIU=?Ml6)$V`)$ELqF}z zlnNB%zDrj_cftU62a&6uFYXr%npi%F*|S)(+TOl?Id%IgCWVS`0_klfvY(PNwGSHS zx|(Q2$dT?D((3ijAKec6!>*zB-hk##&R7a-k1~0sk01pW8=%fx3-xEbxRik=n{nQ| zW~4l^@zAl0PR`@8`LQFZwTrGPn-UC?MrM~+ly06#6BB&h4X?`^1()LbboAJ7G=50eNMWMrCI8&!kuwq5vH|6N>Hk(QEx)tL4Y5 z<<(}XRRz8AmxVaFmHZ|(r|7r?xgUtr73sEWm z?Yh`hzQRTMx6W6ed}nA08_JS(*M3=eKoY$=@>20jZ@9&9w4#SU=?$6%-v3FMLyhzU z#jM7}E|Jt+W%oo`ctmvwv(|+(k(64criAQYX%14q6=Lrn>{m`ZkyIrTD*9r*KZkwH zSdEf_U-vq28yT>JO7m3>Fr}oLcN+N#t>n8OrDiUj$$=;h9_<4z)P?d=Lu8vPINa*; zXg5`rvXE?0Qqjtrgv#PD)?zV4AjIn^0lZSh+6;dv-}orhdu&-K-?<5;k$l=PP6-nY z8XJZ zAAv0W6S1(D%??p#AeA}ruX;e8<&~-iH5{PAz$1#EF(Fh8u(L(np)$+SHLxUVV!W|nFH%ut@mDZ_d} z3|~Pdo$pj`z$82m3ys(n{-e^LNgHIo3Xhb-)JU<+w*}!a0O11y#{P-WfVRgkaK~@U zItto2T>{^Ky}$L71OA!NowC=&(CejF2$d%!bC4gU4RoYg%SQ!7+h6{i>~_r6M?Bg* zdcp7AC8AlvSVTQ=bn}t7adkC)xLD;NIC(8bj$%C<#ybZSu*clz!zIbQt!YQ!Up;Aw zj{7IY-QJ0J-=RLDM#cL)=U3b!8RLP@RBUQ2<7In4HVr`fowYEkWAiSg=gKL!zUni|Dp8D($I@F! zmQSvBDYO!K>jwM)U3Ok(UOhFt8c729z$sO4Zt)jpG`0TpShP88brh8PA0>~Fq-1}u z#ML=_$B$1iA9fC*+}&&;v$G;Zqsai@bifBfdSjw3_Dm+%edq7IjH(x0(G7377%rmY z_e^|kW`;k%Z{%nFN3e4Zai2fq8#V=`-F1FyI6(YY731}b4AR~2ok7>m5EFOuk@)&c zu6tJEhzP16u46Ymje0C|8YYi)cMM_dL8!FKZBgZ7E{JWuqBDlqnzfX8xyeoJA1hg! zq|JuZ&*k40OH&FJ#$ZIqH4DCp*2R~UsM92+EMzv2N*ZZtt;_8sk1cT#yqX!2*2D!K zs*{6Vow1~*)y&0X$s&YC6U>Xd`EPlNR~(KIGT}~lAB;;**YZnMb0(JD|Nmo{_n32Hq*$YZxOkR?=7mY6&&c2 zn}m2O^=}iUm#gTC?hg<7ri)_wJZ_qnzi6Lq*S9nYfOnIV?j~VHnrlthckFtL9+<p%VPjE0C2SAzql#;;M18% zsB!#pLftC0GGeXL13+}t=i%ZsKb4lS(e~kO<7^Jt`AcG7udmLJ&9Fl;=B_jJkNit_ z%?JM6g;15nvqqin4l$3c%J=`Me3r?f(C8{pfPl|r#_}2!v!=4p)abK0Bi={=D4lis z>I`{RFfn@fq(_~YSX&AUT($!&WQEV8PS_x>K`H+0fuhqcRx6x2!)-=skz5xP;2(x4 z#~dv!E%$(80k057Cq!#Jwl2Tvy{P;+65_NfG|fj0Bx%lxJz+)F7)y)~$%{vnib-z? z*ab)ertHt?tdsHNF|6nIe>q|te)2EjZfpwD21Bb2nhsLp*Hg>?2x#BL#2e%*s@GBk5)c?{ zT@K2g3;XbWW2+RR5AU@SywD1?@t7joGuVv^GD->XBp)0gZ z77}q~ksSXe`bm;WdmscF;}L@Y447*hd{)_Y!KvFjp51jJ!1(q`TM-wle2Me=wbXgD z(hNc<34H#^h~=k0j=cNQLVsD(P2DBewPt=XY5Kwm0Lvp`b%Hzl=-$)38&Vtv_-Xhl_2TwPi{FkRJ@ zMw-S4y|y{!kh2)LmQx5!H~bP>f<^=hY{b#5Ieh7*l6Q}INsq4^8Kj`&`JB4*bZI1` z1jbO(8cB~KAF%%L$|wYqD`*!z`GbV&0@NZi$__u{OGa|XG4rh7DfH~r%A3ZPpTc4lGpj zjF5Lrmht?&5Ibz?82uN8j~}n=YIA&5Of%Gz=35yw`o7c%f?Pl;?M&X6;tx5m)Ld*8 z&%tEMI3FjA3E{iGGFo{B1?1%`*HR9`O3yV128e3Wo+FZxXCovQ)AyFBrNTq>ug$W; z%p;@obY{&xf84c2&eb$>=^1KT(F};0*c&gINPFBDppVmXTRNCV+A1Yo6mE|)vUT;U z#O`>%f6v$`XkGTZWKWlH^7YPz)fdI^Mj^r;x>V6Z!@e3?1uN}&>k>+2h=DlE)?P6+ z@gIM~_@Uaqr_whjKb&xO)M5Piv^Tjf`5kn?b`ElYq$b~x1hqVZDBkXw0)U9ZOm zrhCNgz@OUmA)*RN{%*wDpP)aGAFdp+IU3=7O=4r$v~}JX%w+V>(zr?TH3*4b&uRs1 z8nd%;GxAVYw=U}`f*b?^tZ+iI*0IX3<_5qKt}JFMu_RGSc)=(@#yL~k(+;z0k98)} zJjb?9+D4b<6*6PuT*=^YY2tMO+nCt8(o4>pp%Y`f&>msqk%1!Je0pz;{JiGZ_>h^g z2${zBeEPQ724l4{ci+y*pChXDg|{wZUN}H+Kc!6Rm8&QEf=SNBjmMOU|2rv9=9oTd z{>4V*+GWh?1NA)Cy^SU#<%tWjBMEh^(FC;&=TC*fN(Cx1sYtf9xa_~O*Jl~+ueVDW z8grC8B!>YKw4BPkeY5Mz*zFcxBXEhdY6A@ETg{LV23Jh+gS4{qu1@eB9qukBTsVor zYob6(;RCr8AkpA=wbi*(?mvuF{0Q$?P6{t=M+yl(Dz@~PX=duMO+4&x`b;~bS`zxf!ul)!wQZT%SUT?!jcrOhh+k%{$wUXRhkf7MGNOU9NeUDLDq z6X#0(kr&Q7Gby<5P5{%5a>*NFW5sSn1D&RIN<~MmSQ;)9imncpg&!S_uIUeHZ4NI z0x+%mGi*j`xvxkKxvf6#yYOnGCFRgUWO%g1T0a?Jj{qpMA&)1RN&SDcj3E)ec>RH? zQ!wJbOEG;dWd=g5$jslHPkb=lcl@LQT8J>O@c&nZkf}zXMa%zC2pxR;UkV|!lP{>k z9}E%+vc*X~z*fsPH1C%+5IR5ClV{eb+IHU-@2RSt8$8?++pWafT(>Y zgY$Pn85m#Jv%*!4FW_-p3rvKmRhE-gIev9A0M}U@KJJov_U~6=sJ`5uM(_WPx3>U_ ztLgfEi4r_O0wGu!EO?MW@C0`W?!g^`GuQ-ocXxMp2rdJI3~m$L8QdAp>0T~kHF483>v?(V&3{nmf2eHOweb1!XS(qiQyh~Gdt3JOwA0oS{n=ANG-CdF*j z6pJICmgRNtzqPh;4UK@*zDI!OqLAMuJ#C>0S{OZB*vmgdYf|5}QS;2nVJZ*C<>An3 z^`V?!&EvIB^jSc{aA24Z`n3orPr89|TQ0lRQd*voCk$$x)V$sO(68fQmxr$bEUBw) zWqGknCrpp!dbiS&ELQSo?=L0-75F@luhKiPw~7V`B}hr2yYFLb}s0 zuZKKU;m5VqnPrujd3=8Y`KzqvxADtNd>n&9k%2PxDd}AIOpHDQ-VxSY5)*Yy4d~-C z#I(pD_`H%k5Bto|2;5n(Sh|J^i)yO)jkDvBR#6Z{r;b z3ZvIKI||Ah5(XSTRUL#-U&7{FEsLWx4YD1bQcjdF+~qltgx{TeaT9Kj3S3o%!+f$_ z2EDZ*$iOa`m*ma?w%U2gwHAIJ%yb}@CEGUp)&{!S==<44q5;tPtD3i(mix3A^qGQV z4pp?52V3IzSILDrT2;GZ(QvcF5KL-do#e}r1KEi zywte9VfL?ClbzChd}t;d#q#&JDGwb-;18_<=NVg8Gk6kVXl$-O<+6>|~&y^aI~9ex4h zP{$`wC*PMy1Sk8I%j{>V)X@h&-4s+3b;oITdm*drR@ANkES{JK#O&&-CF1zd$vfDUua(x?eq0kpHv*GW$c-`U>W1n%^dO7|E5~u#tA* z@@W6&`S`Z|1u@+w%I8lp9BNI_k)aP2LLJ=to;a`~@ymbUU0iSw8*csoXE^S0Y^8lb zRPhLe@`%mIV_8%iT(j?#IHf*fz-{~$s6QmaxAMn`o;I>!3rk6fjm5o=&voc4n5xxU zlWgidC{BE8s;>s+;$BBl>=28D=l}YsnR3Zld^1mlZaaOv6hA*dQ2^aMpg7*`iA70+02Gw8Sy#Sd z`0K9Q{Am%o7w?~Mf6kz;f3&$czpUF}6;`qqL94NoGGF|kp3R2Kf6i=)_>}iQ^&_ks z(*M-20`9T@UH|1S?1g-M`=|W(&Qe{FqWr>0^kdk*_7GQ848M$Vb!&*ft6S5YQdIu; ze_-LC@{YewUq2(?pK3^sE1%Gv#pu&JgZv$|#$jes5bbtG<^T-f-ty!utqWP`_U4DeXvb^8oj|&q?R)W-YEcA50gB7*Y zCQk2n6UU0;m33F954(%_iRlBv)xHqVQVdy3Yv#2mQqmR z7P=L)y+(-&oW5S?<&7*z1Mazx7isTU!wi(Ad?9aw+Uz_-cE6p`?Eu}v1rX`)=OLmm$9-5f_v442jHqR;jx9FMja%J9j4kMx8#W-RK!6sJaJ z99m7ubhTlMC{yIN7lSHvXe`ARe3uCM!lEFzs{$@q9y#dmtLY0m4sBs=M~_ecbL5m% z^AIvEgVhc+nU1-nZePYbjEzaiE&lU+!nFi`7x~+UxJ1tkhTI0Uq7Ad-BlzuJgXUw;%5l|v~Q2>QQ zF|%``M}LA+1HL&AJZ{b|n1)xeLs1aW|ARcL3O+RvUR@&NIdbl#vK9ND0yVRGMPkg- z7&W$0gZS8PHC$2}E6@)$blgVr<`czcm4Y;@%Es##-Wz)#;$6~}agINPxIi3|m-G<{ z>n9M&t>g+35dog9auOus4Gtpn^d>SnPja7FPiH5q^c*Q#P*${6L{4xgLQZ8^4Spv1 z{W6n-H7e+lDAzR$*HFcqBIQy7KF-oPJ|+nDdEt0OlFloRK``Fs8<(mD)Bk4ZbC3Ot z0Z)#!Y&yWaUO)SreX$Jgn@P1&EU1O=G|35FC_#aQ7!4dOq5GQmG`}bsf7`2Le|&4! z$UJln3Q;bRsI&TPR}H;0#?h&`zbfoZS~U+pt&JIKO^(G~QBf z5yaCaGqfA=#BNbGKp$OWs%43!=NEIfp=tkfx;LxvWRZ!O)5;`; zdYL`yvwd#PUGWY_!gAOzpyaLoYIX{W;3$fxq!#u{lZRE47)AW>y-?1&s`06hxxV^W zZJtbS7KsI8drg60TBUQvZ|HPAgZJG7L{eg8C#eQcIaqJ)vO{LR*$Jx3u%pok&+o8z zX0QhD8b3Kd$H*ijT-MnlG_DE~+6rX-782u7eY$gqu1yvtb;r5UrJ*o@+>MU7nM$dl zlmT>^iRVW{ZEXRHjgBc>yy1@&%qc|80p_sDiCyrnO>5_AHFPJ(7#Xjma5 zQGmWt5(g)p7S%)yAN!2LyLhV_QcY6oJSw$}k#V@(;#&HFVL-X?7yn zFq?(@l;Ij@`z1Al5z|0M+R3dFoZW#PB`Lg!Mk{m6@DT1)cA3BiSwPQ-zgULfWBC-w zXoC6Eg4LGwna>D=0lrL0^R764_XY(9_S+0~B1y3a>1y*d=6NIDdZ&upDghHYbohrD z2~kUv0+B@LPqL33F0`DenVhsVHklSj|7?@&$_3YU#!nQwvK^(zC&R+z4ULjdsxjE6 z9l{Dn#5@UOvpL9VQ*_9pLheeAC4TiYZZDST;Y)szV?u~UCTeY|vi>p`u5eFxj^q{FKZUnZ2P# zNx0yYdPVxV{MBa`C~an0(r>%zO*1|+&$yr+SB-+k4b^)2xGAtlL8PacMH^9;+5#e4%VMt*9)W17ZRFb^jhx4NW*l9IPp zgnkqsz2XB8jf@=Lchr|v`w^N4c{G4vZC)EO<*YP`3U{W-V++jy6|Sr@euo9lP@b7j ztltl1IRMNnR$=pTCPcWU9|>dZz!ZJbbjYK80ZbF0RPR3HEKD-ctItc$iw<4O$%w+= z-I9}B^6^W=`t8{A42T&w+N{+<8>b)-Et`F7r!HLsknWW+^u@R~I&u;( zTI4M^vsiA~we)XODqEIuNNTCOu4vxmXs|4X^itZ8_ysK>hCdg2p#r8h*kaDjuZ4V~ z5!sY2nZ018>yL9MiAuI=_I#Y%eukh*E5uQL07zdx@{A$kfjj|_M==o**`R%-MkIZE z*d|r?z7N<=*W*ORqptEG!xV`5*^qwFu6nw3ks?kNi|K>rLkXWkv#Mz?+I`)$pjzi%4CQmj)qRI}63O z1rhPs;+I;s%gb_-$^2`FiC#yh7kdTw+jy{0vT`T_CK8$f00x=wUPG1S@X*Jff83ZXO5{lMD{?Oy)~}lrvC!*q zCe(?L02(&a-b^WNtP59Ha~Pv9w@H$NWbC>5Z;U(xUxcIe!%?Q9{EUN(hJgUCvP`U~ zc*cH4;vd%+V&x4?DO`P`od$mjh|fJY>oseM@nurEGSPyFicEs%Ac~Z&7<9z!ey6k& z8%#*hCE6zy6f_4}h-UFG3To7*^4C=1@e5HYQm%Pgn*aOTMj!o~nwY6zW6k@Nlp9L>yh~~=|mB2}xC>f1#Ma?pqiH?K*s2_4#?@9Bb*pN@#0`q2X!5slr+Pl1f zn5yseZ$x7Pdm%gKp>X0i0`$yw?GwttWx#|<{ahKHVXK7nEJ36UTV}PoYjjXhY^evm zIL(%pYoO3_{z27sk&1x-eezS}JMk_GKk)#8Kh{MxP*u!_p;{HpH`O7}0AGd=F~XNN zhoO259|_VP8%hz2aP;7JcjcB`!9giY%jmeD z^)sP@t(@&w0p}~^ox42`)ojc#Azxa};VUgU&XOUH@B}k3NAKlDEGyf`$fv1y-&Y=JXf`2QjcJ&dzGWk_PmDz{U&EhysQ3=u~FT;G*_&;!Vg-F zq0MQEB-PyMgLPb#2avI*^2_e5%Dh6PIz7*pXz6E`G#Zhv(g73}kN%Mpm+Xh(U zl&|MS)dr@YP_zy?X`#5(J}jCCoA~H{z_Fi{#XAjhW3%Svme>9Ouy~+?9=z1KFYn0p z6U>stNBW3FS;bm4X3?nvBVTs>w{74M`651~_C^aR^Hb_H5G;8_!aOexazee}`%|=#13Grkdba#~e&%R- ztpu|;Kas$};Mmekg}<+dO%ibN=MW(WvEBDh*j;Jn3C?IL^%$?dOEV=GOzP8gPlWaO z(}yoK#)^&3%g|`;es7%_3G?tTX!W74u?h~=py{D6qG*k5-$*s8wY{Z(aT3VXI6#DE>iUf;LFp%+cc=R8 z1bvmJ)pX~+aU%(j;=A)bOr0c6NX*YKJu#rM^usoPLXBq4yWyp=&Se7F!%hP_n7bA| zUy6q0b2=_Ku~Xc$EK;g#VI;w)#>JX*TY6JU!yk^0Cr4q50n{2QBC9zw3H|Ur9`*U3 zh=eP-c|2|}2eNw>pbKpS9qz=g(YASx06BZK?WcCw)jPIeKM*enu^Bmw<0RA7;`)1D z_M}Pjd~z^zf2MbTfRbH8Z)CWk^Lk{^hvC3oe$4kX%#xjI{d~5G1Yp>(S8tMVPEL2`=oH^mi|0^7 zjp!VeJ-q``zBq@OK=OiK?Lh-?&$EMGrEteakd`issRK=&hTvr-6EYsYdq_J`RshxM zAoq!4gcB>pLN!=PMs=l*-`qZr#!cnh28pN9Cd;cbi!3-4G-tUR?WDVB zl<}}>juI0v%d&2U=6seFR4&!B#tT-blQ!_)tosmzemFkfeRy(W$qsBCd~EtuV5=-t z@bxrGLUwg4AB|4jRXTyP_HcN_x>}y&sIGlEtzkE+ ztTS6kQx-5A5fl?HCC8nUdgr)%q8Ki5%Lfu$ip%QtkYjf|-OlW{XwyhBlc0DAOU_1T z7|M)DyPw`7(kD@Y7oRZ}Mi9|U1BYzc#!9*!gTam#2{wWsd@wSiYogOxX_*~?9E%#n z-1PdJB3RNQh>3|ks&-Xl?Kpi8c5eUkThZtcyF52dUVHCvIt{um*9m9H}K@>c5E4DFh6ej!I8E@?D3=WQnLP2|1Dp;WnjjZK`h{pkRD z3vsCzwZ3tOfSsR|iyvVy29ST~ld`B$hf=RNaVK4JdhslOE@9hr_A{pdV;+^}Mxa`J zNi?}Na~fe|RVPecvEqo>a1M`rK7*e0dqtsuc?6U5w<6dZReqsLVl&P!BFIPgl+tJ4 z+o4awg&Mz|gJ}{gzdSOMV42s>BKI)I!Eg@UPWpT9`4N^0xb^~x2Skwe+?By0N?jgJE$43}Cb#1_a+T3|&`b?`8jkBfgtFTkKj&!39 zGK9>F=S%ZRvBL6v-}Yyri?*{`KgNqb$(h!>3CI=J78Mm$RaF7cwwy@uo9Mvi#JI&g zO}ETIwIO#x_RX|$$Nf`OgCa!4Rrr(lJ^{d#-le=9#LH1l!~3&$axX3l^byWe#cR{P zLn(^1Dh$H>{T@oRpShH=&XB2Gd$*aD_-yolED{#D5NW{_o5nwleV!ssJGng_W^laa zI9%2Ox>xF+&l%RaIn~p5?OI}5oG|LOyQ$E}yScfs$?OU_{>#{J^E(&z^z^vQ*Nvb5 zmZ|8$B6|zj^-kcl?##)_qgvDk&+LxeqGWGtJ@o}oXQV|4<-+RtM@UFXm-DZd zUl&M>U=eT;xoi?#4*!Vqp0Z!i2WV}Iv{}|8g=0i}6Shxcc(F#$nGNr5E*F3ZRHbhb zvTX_*j3w|O?PDXpw^qdAWm?GVwXEq_4*!Xgg?M;?e_q5^AdG@}2S}{#0^1d=Xv*i; zx@Iv%gH1^>mr+rE1>_zokr{T_Z5?H$*`^objBm0lYsbPL$ug#D_@UXTPQ<;};!_%N z$6s6OcS7>ekf2)@2^+8!wVKB&f!idapIeP-PtL6vu8MP`kX|pVKkg|91$~O)pZ*ey z_t<|~gGFDxSzmn-vQPgiOV4K1kglEol>g3k{$C;~{}mgBL3hjKxC@~g%F4>>>T_+t zvz7?7rMS(y@E~Q<-%?6)h73{kuk!y01^w3!{8#SvU&bdj z519+>=eH(<&M3VMtGUQ;HVM0++HVJ`2f=sU**jq*E*X?`Yh4ejL6GZK#rmYz+aYD#4-r~ zD!hR_jJbVaP>SSy3ib_>B{lwBOW9P~Z|PC9w=C@jn6iS_0ictdlQsEet?Njoh=bFh zk_P%fO_~E!2n~B;N8!GzJxDy zdHKc8A92hP@$6fs3EbvzYY?Ho^R2m{%mM zOJ@&P{1v+8@LyTc-@H(N$Db|II$C#gw(si==o3TmaDz>uu*@lz@k*@4I<)O8qu#1w zxNWVflM3(S!|7@^X>)<>mrk-=H6t`?HL1X;hoS2g9jz)^El2eC4MsS6nhP1NtKnXV z!<4GXIET6|XS|-Vo#A@n{pe~s+7eFQ`B^Br z(@Ryka^fXnFyYB8mXe;9&3-#zzY$imf&rF&_j#W+ zQLB-EO!RX99x*N5A=5ams4LUP7?f6%8q-t!r07|u=4`Xfkzc9W0cHjn^9A7a`*lZ0 z^Y#G7#vxDR-8N)}c+&Q9zgc8<4L7DP^x%G`u*(waPBdd)Ab+mn#?Jhn-ktztp`*49 zM=c2-O77l(SHM4(^lrfWKa!y!?LG1GFHjqV-2$=6O}uXWleszvQ(V9)E|x3zIgbyK z+|DD@)TmiDNXz}75Wh1JXPG>_N}%2M*(M|H?DN3Di(S&0Z4FSw^^^-?p;SO6E27E- z(Yrb!91xlLV7K6n#bYzj8tR-@t{iQ)+`t2M-{EBsxV{^HK<#8@@M!?GFIzMg(ApB- zjV3?XXH{0R8SL>=et`y9H?Q4{T)@| z+c#zRH-B+h8LV7lX&gk&nG1W=D7mPTNjM==Pxy0Gjp51UOCxO8cneF z=t1`P5oc5fdY_p}0zsuT+5==gkI5;PEc3?7KO#@yEX0B$iwvLs46Vef8Yhm(43Fua z_5b-(!8E5z>YH28(K$eW-{K|_saOnbCKqqm<*@GoD$YG3m&Oz8xdJAhi+2rqM#_o& z?!gU~y{WcXI2P-IR!)5y(m77sYl}Ut0V694?|*M8dh?65^B1cXI`f-6Pz7}sKtc|N z`6opX8-E(F)NPkmZf2(%121-_YS8u&a(@}Q`;hsQ|FoBd@`Sv8^Ujm2%X9SH3k}mMGH+H+R`31;&Shsb1Z+i{qU1wvw$sMStMC-_G6; z+TboTc2td>uD0cZw@7t3;X8z_m3m5i@AEAei*d}e&^_u4dyVZ?hUTRF zX@ysJ#9K(Zlmx@1I;ce6S}jJ5w*yJK5s_jqT&%hp_U7D|N|q+M=et+9+@D8i-y?{9 z-BF$5?k-Y}jb~Qgu2Z`4&lg)pPGrV8QAur?s(FN>Z5YAHpOSMt(L`UpB?qA*3G|<=#u<2@n~@#f|pE@KBYd ztt}^HoTnAtgd&)v#XMXYd^3NPi`%oLjgG$E@b=DsrakZM?JeM8!^|Sp^t(&=siy{& zi#BfgBB#;aIr##wk%S)~my>k<8(;VzoDL0=t-B>&jYRCW2W{z?AP-d@@)OCK3YR;D z+n8CxRI<$2Td=K0X+VfxWa_1&q>zYaRIM?%k0LYgA?l$AmEuv0Z@*MKx_2qumgz?D zQ6n|tY`{x>vhv4hYUovs;ddO5&v~(resvhY{eU+fH<9|mGgTnZMECnZd4!myF8QO~ zt`N7-x6 zja3rAarhU06#rUxNr|qJcds@2- zHEXH^hNFBDvz&o&Lew*N=(jJNhn{ydJX+lb$2`wDb6bf&@Fc`JXm$Mo%U=g#v%m|= z+jkd&VNfWG8JRaC)JlEn{`@HJymRgIb%+`~Jj41SSbgh+ zyzQl4v?Ce~prA)=2tyCZ!N35s3NnRZ;645ToRT-PgL;(L<#3@mIfnAEvC8-kX$cm5I4Mi$QSnXJeSkMY`?F=eqBouClR2$#~tN!OOzV3Vgufdl2zawE5y<6Ids<=WyT|`6p%b z<(S(u{@7R80N`A2w7!&H#06Gr{bpdeL*R8|?A=xB7YZSpQ3w(wiA<`d;O6#tXaM9$ z@1KLWHJN#P!3|Hn)hp7|&J-_+9#t;0o17Ed>NEzt7mqLDsJA#RP zC8F}G29*9F=7_w#)QmN8=eBCN9DG8f-b9rlLA(*~rGMrdpYxgP@kpMOkk1R^_cI^9 zP9gl#GEa3Mc*noa!u_Fj^t1~~bFO+TK)|WrpH%n-b0r{!+ilY|zVa=`-I!-0C0gpO zy82n^uHK{9YQesJqgA&ziFoSZuRcpe%~x1+Lh8`fu>=IL5n1Kzo@K~nkO6Upny}2(n2xkcrNR1fk~4=+GZbhby8Fr7$8r^*IUkUP%1;hHSlSERH-rEMXy0W^jk-~q* zUVYD>Y~;ut80IZa+0weP6cL%4RNL*ri~czG9JSC&?>%OrGF=K@^q^L`hwh%=F9PaN z5K;|7e^pjrm)d84dgYHxW`t5Nio?;1Ix9R>Q#7C5lP}50BxhsbnJe`t0M zG6P!RwzbVp%*B&8scB6q=&7o(>#O$^CNt1vR`eX~{d5X*pDEEDV~eEpi@L5pRljkm z%8$)M4|1GGBikw@!&%)RHD?(Jxk&_1@)>ylY#!Gds{g^QTBl1O3P=bd$@wW6cl)s^ z0U-XOt=*A5_=Qip5z|S0lLfZG{yx(@aK`)_Q^E4eocY5_6XLDdj51Yx9NBmM$I}xf zRd9d#Ty=x|58j14lS;HrHHDUa9I8d@>j6PbJ$pva0mu|q?Y8`JM$+wJtLJcH)Xy%TVCVKGCbnljpw*JuWQ9#Y|$ENl}lN0|3pdOsQO;m)-Hhq)<05@6A zh}T^-eK0btPRtqgi$^T`kVoh1v?_q?ddT}@Xr(cuV+Vby1DM*i36ewQ)%jZGuYS%Y zNr86La?VEX4O0Evg!rEJ$JrREpE8z56+?y#G56hmjb%P54P&0Q-SuN0$b_}?2*#AK z>@}sR$?rHo+F&JVPqi^TgjVLx!8=#CtYxU^M)I z@BrwaJuEt6o^M|t986|cARi-4I{1#!xZSi=cv@Y0Y*y!Y_42(M9R^%@T|64uFG}Y< zd)tYfwMuInqbzygypBQ#QNQI#f@jJy%l)rpfI-M4klsxLJpuAJKY9{y0H=-ckDXaz zjY4#VXX0gvV0v80X*a)(mgE;vTDhu}vBV0?kEhKE!~#+`ly9Y~(|lrIdCVW1bXim{ zXT_{?)quAS*p0E~fcrP@E0kxdgPXnEno_p)^T1M-C?~gH176EqhyrwO@$TJ8!w;v; zTkhhoekRVyYVr>r$x>Ork@e&t?cblS>@48AFbgrq={UmGB;ir9X(u6D|iL*NP7XrsqGX_j8F@)sru}+CB?vj6J3xPP(UO zX!|ii=*2fBLCw4~g-X4>Xg!o}iF|NIMu6Lz-z!dsv&&PQQd13p_cjZfP+xvx{tDyf z_SO~})v3FnPCT_e*MW+u5y^c7H_IhiBRy|skO%^#(TwficJIbhRx|b|syjWDfYvm< zF?1WG$=C8&b#(aMJ*Y2^NdMI=V!y-p}~5!ohaWv zu6>u~A0*9eSUaHWyQ~pp=$l!K4GpnXG0Ym>R1p?gw)`VosS6+XZw!ejjICXg zsX%W0ZlflA!EN{aJ}jtcbi(0pj4v0Hj zY^^gIT--(L!Lo2^Q>==%9X8zR^nlLCWb1Cro~Mbpz&jkh+v&xJ4UvsSc=~4ybTN7O zggMECp>TpngMlk;LXf9(zZx7Px4^trj`9>M^I^!;@V-45Vw*YcaGWxUVeR|_+!%zV zd)ngy&9GQ9cXvOE7^TtZrF}bTfd|Y?3>@w+lOH8w-)G6+x1o2(!O>vzl9)X=H`k&Z zj=#Xlf9Q&~hTNIpW?0)hR_}xbvJy;1R0P&Rx@55*`%PWD2`J#Ynk;T>o)#ZBROn z(tvfaGe=RkXm`9WB|%iaZMGdDA+6Is$4ce27WtIPgcmJ8x47SFWI9Gp&hvOmiRc26 zo(;JhYI{5ft854!#fEc$whnaR+tF}60F76*-2#74YbE_!XZobUvw`;3ULIpaR)sBZ z94!S5+XI=|v`Zx_gSngwf7m-wQQ*?+l zcoyl0DbS`J8ff#8KZvQ?Id`rdgiuFbv^a$Y1sxEFbSx?;BkK2yMO(jvy;ct>Q}wd*DC>7xY2G%x=I#KN^4P63Vt)1guCi?kZ}9f*f69C zuuQqUQL(>m8$bSsnk>C8mbE0Lr1e<*QmMvG`z|ZtUy2VbwM22{+X|(9p1$=V*PT|m zL!|XsPRl#RUf?XeHn*Z9_h;9<;=T1c938l z{OXx_B}u32+4tx(Z?}<*F!6nw@~4`Phc{-CQR2Q9401*IOSN)G8cPTy@Mz-thlZ?E z-hGCr%aJ-fcBekcF;B`?SKj78)L(1u&=f9rhy$UnUyEh2ZlCjVDQIq02u#-IHEOjZ7M zXfthQG&RV{O+-%4Eq3pV;6hWaW_gB+KK|h)`!6<2O`DE9*Uf6-4Sj%T51a@)SNB0+=Do^`TkrWVxA4zLn3h&%Ar|K_Dfs+P>kNPO(v6n4^NvOAvr z>u`V^k9DJPy>km`&w(d@5|BXgr8GxnI>}TTT2eCIj9I6Lp|(o_U*Dip+j@n8b1iG? zQz2#TW&)BZ_U)Mc@nKu9n51xa05W2#F2 z6cJ&Vm>njk%JShx|Ip>)=ga!s<2v*dus%adM|#LiqCt3N@JnWRumvYb%a(bbeygAt z3td5iNNkIc$4Wm%g$m%MB=d_#OpsZ5g6HFrxcBo~{qIFN+>|QMMOna%A4#bwG#i-1 zCSDHaeNX1$GlB3Z<(~)(6$tdQcx-hKvD^-|y-Rlc5Pj{hzf0-be1rZ9OGem{stpO1 zY*skYjb`f04YQi3))8==V*|ZRIX-g$@2Vg#ro18WOmj3mL^IXd*15gdSJT$Mhdm;$ zwnkH(mZqK|QQmthTc-_a&G;+wd6hR9B8r3e5`Y2~qLd9PWw&m3vJv-N8OiKS}s}4^WeiNL>D2**(Jp79Yb^4a6&?=B?U^tw; ze<%D#M98Gul6`W6eG)rUDmCYmT;zo9w&Y+Kd9Jj+(@>ywgU-pfgy+lWMzs=D(IGP< zGQrG;KT;6pGw1UgP31L1bcKv}ctYhvbmHhiL9T{tB)(sMZe~L{&u-OK5(I4Gi-pFt zyQN4u*EK1xZaL3(bB8EJdmKGe;y=QQKy;kpQ0i2x!Ul*6`OLtG2ugo3d}Cmx(~J8!!6*K!b)RoIXF0wP380t4D7sK3nk{V z;YgOL$De)J*x0zgUXR4Z#kF2+D35({sqvPLrt4TSt>GC1iHuzJ%RWf(v~vPDldL$Ch>kxgM*nSa5LL;b9qar2G|EuB zS;f9_qH29)$}#H$ne+l9;7dhhx{@#c-GQ;FVtvH&hsou|d{uhgB`+aKOL|O+rIZ6Z zb#cu%A6Q6aB=Lm|w|D9g_S0uP4{c^rFsqGXq1=OqeG2k&FH5oEwvSHk%`G4+E0BMm zQ3JP#gJ#O8ULiStPE2W?p#gYHIC!hhn!w5GzE{G@U1cz+=~`{0K-Rn-RHcrBQpJrDLMXm@`95U@w2c)Bfy}iA4;HHqGqN1Ff zT&Zfgho>jGkOPi)Vq&6Zd57aJ-q_g%sdW4&+!sqq9jwfZd?&APqi@6ciNJM@?4J*y z4(#Y#35Y__6bcAEJub#1JouvXEKs}7#Asof$Ej~#0}nW*R)`Kp5ORM~X8kx>wW*+; zbcx6%`{!LZ6f z&16RcD|$0ac0<_$Xil4^^kR@SmFWh6i%ZMG#E9MynzvK=QG>!ezVz9~uRiNAVtck6iXnQCyKi7fGAzV^9QkbmXe&ci<4 z>)=}B_vhdlqFP4kEB9t^|BcsHZ66P_=+mG7F6LN%D+aKC#aZevABkZ`=2D2vikzB0 zRo|3Eo06&r+WfQ8p~ce%~Rv&yv^ zZD1YzY;2P~J%{26X*^CAuny1F)m2hn=Q6l@$;PEP(R;eg@g^qmNHPJp^WzC?c42rl zXjR#wxz_|-8O7I_%w`qxVYL16yc#|JJAFp=o2D)yWTq%&sSiN9#-%m<I671r zs9+)_6GXGcUK={`se)(E%5<4is~H7@eM>+m!MNDMwMXYd8}io+I>-}0EaB;PxetG3 zsd2?6)72o&Zn|hOym8@#{DY>_T7Vce8taf29w44MMhxlQyQwsI_QIu;!=N{aNvk35 zJ}o8&4`r>j>mratL(k9u;7_RQdQd&E$D81e+`pIJ zFu==H1+)LYRAe?WG$iem@N6raDb+{XgbO|jqC>k<*_f7~+3~YB1Wa^~DP=Ha&WP`I z8f7oav?)q^T>hDUB81B>0&eqVb$8%-m{JfEn@pAJ$v3FY-xE0tYj3 zMF1M^FBsHE@*9<3KnwEn4jaVd>F3kKsnjD*@kH}0c&pLmelb&F>5Z?xH)~fYml$;`LX&H9oLB5&{t-G; zl&v-bz2IEKLJ{*glHdfH*!=NtRQhj`dZmu?tg_*hgu$O3frjg)`DZyh+8%yC9tcN%~1+&0c+V5K79mJeg z>Iw=9hoa8p$Q_!L!z`+2_2b8%(XRHC1Ciu7I5=wb@fW0no=6sqbXUN!oPSTwOFQHm z)0js)n$(!wEhgs7e3dBoCB-huN9gE1BVDwee_)J>Tm;N$Z7k_KK_4}Vs(AaGVSNJ1 zzq66!yY}Gzi5-2emt2ho6>?eb(tT!z5>GPad`uF6ng{NMn+Cbl(3Ct-^Z}a5)kV(N6Yd?}?x-`DkM3M8{!i?lkl@?Z8c+-a74X|6^Nb z1Ggf`7});Y_xiz|*#0ZCm)2~)sWst*bhv72$DEAktP_L3Up>XAc69x@CvTme-D;UK zd1=10tmN8fyxh~5&G_Vcan>^VRae)VUA^?ldeO_x7Kg9SnA)vn7kwkzZRg2foxe&W z#q7*=3x7E&i?8JlzPqWbPy3;8@f`neM%+@uUD#q+E5Lrxj^e78vc8uTD1eQu)nq-o|yO2S!s ze1v|f=l=Ex|L4CjuOq{Qh(IOvlrq$q{I|**IvsFKNBGeT$tLy-5EVsL{r7J?0a`i3fJ7} za|{CfKAmXsZPVDTxNG;J{0C0n=UdLsf2GyGKt0UZP;p`X!BZaguG2SWzD-V=H(^rq zi&q1*)$hd1q(-dk_`Y2W1J`Ib`OdwM3W5*Kq(ty$5(P@FdVdt*Qc-Epd zOFN05?dX@#4K6x@;x@{ui{tj`>xC$We(~Spsw zY*6iyUm|yLl89(tg_UKp;|ckWgHwu~F0b`I`@P!Zo%FrX-(R2IJDat1ZFPug!K<%P znTw8GXVf3Pny;@ySBAWL`SPT}(n#GN*qIqS>+`wy7q4pn94E)L2Ii;_0b8g3{xhrmTwqAZ{l*;%;MJu&%V#e0 z-OI(DZ!z~Adro)a3?Oe+Rp}Rn^``gl?aHaY5EfNsyqG=o<|%MD8F&oGhyQPHJ9;TQ zO0+%HTNRrkDA>ZtE@IOv{=v!8v+u>BR(1~Gu-LawYCKv#Q`W8fdHZ>!8E2i||LMRJ zEH`ns0=gR`ye z{w?!B#%M2mf8oI2x4y4I3sE=7)q&3&5tt2J+{wT|DHVOciXq#$FBxpMKY(Yy-7oD>F z|Jk_xaN_2hPv!)B<@@gerw>?=fV}p)u)g|o74Q^GhCeR%z^T5&msIL)cCGr-kWBE< z#Pc6F()V2`5AxDn8gy#SWT3eW7U7ytbua$;`{B;M$^RtU96|19h=|inm1qlIYH3@~ Ydg=YFl$Ue*0zisAUHx3vIVCg!06tTm!2kdN literal 0 HcmV?d00001 diff --git a/docs/docs/assets/images/stock/transfer_order_list.png b/docs/docs/assets/images/stock/transfer_order_list.png new file mode 100644 index 0000000000000000000000000000000000000000..b371ac350a5b62bdd8b86ed74a1dd21d4b382e04 GIT binary patch literal 56809 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 z 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 icon("plus-circle") }} New Transfer Order which opens the "Create Transfer Order" form. + +Fill out the rest of the form with the transfer order information then click on Submit to create the order. + +### Transfer Order Reference + +Each Transfer Order is uniquely identified by its *Reference* field. Read more about [reference fields](../settings/reference.md). + +### Add Line Items + +On the transfer order detail page, user can link parts to the transfer order selecting the {{ icon("list") }} Line Items tab then clicking on the {{ icon("plus-circle") }} Add Line Item button. + +Once the "Add Line Item" form opens, select a part in the list. + +!!! warning + Only parts that have the "Virtual" attribute disabled will be shown and can be selected. + +Fill out the rest of the form then click on Submit + +### Allocate Stock Items + +After line items were created, user can either: + +* Allocate stock items for that part to the transfer order (click on {{ icon("arrow-right") }} button) +* Create a build order for that part to cover the quantity of the transfer order (click on {{ icon("tools") }} button) + +### Complete Order + +Once all items in the transfer order have been allocated, click on {{ icon("circle-check", color="green") }} Complete Order to mark the transfer order as complete. Confirm then click on Submit to complete the order. + +### Transferred Stock + +After completing the transfer order, a {{ icon("list") }} Transferred Stock tab will appear showing which stock was affected. + +!!! warning + Similar to received stock on purchase orders, this tab will only be accurate while the affected stock items still exist. Furthermore, if the stock item is depleted while using the "consume" parameter, it will not appear here unless "delete on deplete" is turned off for this stock item + +### Cancel Order + +To cancel the order, click on the {{ icon("tools") }} menu button next to the {{ icon("circle-check", color="green") }} Complete Order button, then click on the "{{ icon("tools") }} Cancel Order" menu option. Confirm then click on the Submit to cancel the order. + +## Order Scheduling + +Transfer orders can be scheduled for a future date, to allow for order scheduling. + +### Start Date + +The *Start Date* of the transfer order is the date on which the order is scheduled to be issued, allowing work to begin on the order. + +### Target Date + +The *Target Date* of the transfer order is the date on which the order is scheduled to be completed. + +### Overdue Orders + +If the *Target Date* of the transfer order has passed, the order will be marked as *overdue*. + +## Calendar view + +Using the button to the top right of the list of Transfer Orders, the view can be switched to a calendar view using the button {{ icon("calendar") }}. This view shows orders with a defined target date only. + +This view can be accessed externally as an ICS calendar using a URL like the following: +`http://inventree.example.org/api/order/calendar/transfer-order/calendar.ics` + +By default, completed orders are not exported. These can be included by appending `?include_completed=True` to the URL. + +## Transfer Order Settings + +The following [global settings](../settings/global.md) are available for transfer orders: + +| Name | Description | Default | Units | +| ---- | ----------- | ------- | ----- | +{{ globalsetting("TRANSFERORDER_ENABLED") }} +{{ globalsetting("TRANSFERORDER_REFERENCE_PATTERN") }} +{{ globalsetting("TRANSFERORDER_REQUIRE_RESPONSIBLE") }} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 82bb9cd772..e477632584 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -145,6 +145,7 @@ nav: - Stock Expiry: stock/expiry.md - Stock Ownership: stock/owner.md - Test Results: stock/test.md + - Transfer Orders: stock/transfer_order.md - Manufacturing: - Manufacturing: manufacturing/index.md - Bill of Materials: manufacturing/bom.md diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 4448546901..64b1ffdf46 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 491 +INVENTREE_API_VERSION = 492 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v492 -> 2026-05-22 : https://github.com/inventree/InvenTree/pull/11281 + - Add Transfer Order model and associated API endpoint + v491 -> 2026-05-21 : https://github.com/inventree/InvenTree/pull/11979 - Add API serializer for deleting a part category - Add API serializer for deleting a stock location diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 394745db82..456d7de095 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -904,6 +904,26 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': False, 'validator': bool, }, + 'TRANSFERORDER_ENABLED': { + 'name': _('Enable Transfer Orders'), + 'description': _('Enable transfer order functionality in the user interface'), + 'validator': bool, + 'default': False, + }, + 'TRANSFERORDER_REFERENCE_PATTERN': { + 'name': _('Transfer Order Reference Pattern'), + 'description': _( + 'Required pattern for generating Transfer Order reference field' + ), + 'default': 'TO-{ref:04d}', + 'validator': order.validators.validate_transfer_order_reference_pattern, + }, + 'TRANSFERORDER_REQUIRE_RESPONSIBLE': { + 'name': _('Require Responsible Owner'), + 'description': _('A responsible owner must be assigned to each order'), + 'default': False, + 'validator': bool, + }, 'SALESORDER_BLOCK_INCOMPLETE_ITEM_TESTS': { 'name': _('Block Incomplete Item Tests'), 'description': _( diff --git a/src/backend/InvenTree/generic/states/tests.py b/src/backend/InvenTree/generic/states/tests.py index 17a2221d1f..fe33eaf78e 100644 --- a/src/backend/InvenTree/generic/states/tests.py +++ b/src/backend/InvenTree/generic/states/tests.py @@ -232,8 +232,8 @@ class ApiTests(InvenTreeAPITestCase): """Test the API endpoint for listing all status models.""" response = self.get(reverse('api-status-all')) - # 10 built-in state classes, plus the added GeneralState class - self.assertEqual(len(response.data), 11) + # 11 built-in state classes, plus the added GeneralState class + self.assertEqual(len(response.data), 12) # Test the BuildStatus model build_status = response.data['BuildStatus'] @@ -273,7 +273,7 @@ class ApiTests(InvenTreeAPITestCase): ) response = self.get(reverse('api-status-all')) - self.assertEqual(len(response.data), 11) + self.assertEqual(len(response.data), 12) stock_status_cstm = response.data['StockStatus'] self.assertEqual(stock_status_cstm['status_class'], 'StockStatus') diff --git a/src/backend/InvenTree/order/admin.py b/src/backend/InvenTree/order/admin.py index 70b7b3c13b..875ebdb763 100644 --- a/src/backend/InvenTree/order/admin.py +++ b/src/backend/InvenTree/order/admin.py @@ -170,3 +170,41 @@ class ReturnOrderLineItemAdmin(admin.ModelAdmin): @admin.register(models.ReturnOrderExtraLine) class ReturnOrdeerExtraLineAdmin(GeneralExtraLineAdmin, admin.ModelAdmin): """Admin class for the ReturnOrderExtraLine model.""" + + +class TransferOrderLineItemInlineAdmin(admin.StackedInline): + """Inline admin class for the TransferOrderLineItem model.""" + + autocomplete_fields = ['part'] + + model = models.TransferOrderLineItem + extra = 0 + + +@admin.register(models.TransferOrder) +class TransferOrderAdmin(admin.ModelAdmin): + """Admin class for the TransferOrder model.""" + + exclude = ['reference_int', 'address', 'contact'] + + list_display = ( + 'reference', + 'status', + 'description', + 'take_from', + 'destination', + 'consume', + 'creation_date', + ) + + search_fields = ['reference', 'description'] + + inlines = [TransferOrderLineItemInlineAdmin] + + autocomplete_fields = [ + 'created_by', + 'take_from', + 'destination', + 'project_code', + 'responsible', + ] diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 9c608e32a5..709cf3ba26 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -56,6 +56,8 @@ from order.status_codes import ( ReturnOrderStatus, SalesOrderStatus, SalesOrderStatusGroups, + TransferOrderStatus, + TransferOrderStatusGroups, ) from part.models import Part from users.models import Owner @@ -1766,6 +1768,521 @@ class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI): serializer_class = serializers.ReturnOrderExtraLineSerializer +class TransferOrderFilter(OrderFilter): + """Custom API filters for the TransferOrderList endpoint.""" + + class Meta: + """Metaclass options.""" + + model = models.TransferOrder + fields = [] + + include_variants = rest_filters.BooleanFilter( + label=_('Include Variants'), method='filter_include_variants' + ) + + def filter_include_variants(self, queryset, name, value): + """Filter by whether or not to include variants of the selected part. + + Note: + - This filter does nothing by itself, and requires the 'part' filter to be set. + - Refer to the 'filter_part' method for more information. + """ + return queryset + + part = rest_filters.ModelChoiceFilter( + queryset=Part.objects.all(), field_name='part', method='filter_part' + ) + + @extend_schema_field(OpenApiTypes.INT) + def filter_part(self, queryset, name, part): + """Filter by selected 'part'. + + Note: + - If 'include_variants' is set to True, then all variants of the selected part will be included. + - Otherwise, just filter by the selected part. + """ + include_variants = str2bool(self.data.get('include_variants', False)) + + if include_variants: + parts = part.get_descendants(include_self=True) + else: + parts = Part.objects.filter(pk=part.pk) + + # Now that we have a queryset of parts, find all the matching return orders + line_items = models.TransferOrderLineItem.objects.filter(part__in=parts) + + # Generate a list of ID values for the matching transfer orders + transfer_orders = line_items.values_list('order', flat=True).distinct() + + # Now we have a list of matching IDs, filter the queryset + return queryset.filter(pk__in=transfer_orders) + + completed_before = InvenTreeDateFilter( + label=_('Completed Before'), field_name='complete_date', lookup_expr='lt' + ) + + completed_after = InvenTreeDateFilter( + label=_('Completed After'), field_name='complete_date', lookup_expr='gt' + ) + + +class TransferOrderMixin(SerializerContextMixin): + """Mixin class for TransferOrder endpoints.""" + + queryset = models.TransferOrder.objects.all() + serializer_class = serializers.TransferOrderSerializer + + def get_queryset(self, *args, **kwargs): + """Return annotated queryset for this endpoint.""" + queryset = super().get_queryset(*args, **kwargs) + queryset = serializers.TransferOrderSerializer.annotate_queryset(queryset) + queryset = queryset.prefetch_related('created_by', 'responsible') + + return queryset + + +class TransferOrderList( + TransferOrderMixin, + OrderCreateMixin, + DataExportViewMixin, + OutputOptionsMixin, + ParameterListMixin, + ListCreateAPI, +): + """API endpoint for accessing a list of TransferOrder objects.""" + + filterset_class = TransferOrderFilter + filter_backends = SEARCH_ORDER_FILTER + + # TODO: + # output_options = TransferOrderOutputOptions + + ordering_field_aliases = { + 'reference': ['reference_int', 'reference'], + 'project_code': ['project_code__code'], + } + + ordering_fields = [ + 'creation_date', + 'created_by', + 'reference', + 'line_items', + 'status', + 'start_date', + 'target_date', + 'complete_date', + 'project_code', + ] + + search_fields = ['reference', 'description', 'project_code__code'] + + ordering = '-reference' + + +class TransferOrderDetail( + TransferOrderMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI +): + """API endpoint for detail view of a single TransferOrder object.""" + + # output_options = TransferOrderOutputOptions + + +class TransferOrderContextMixin: + """Simple mixin class to add a TransferOrder to the serializer context.""" + + queryset = models.TransferOrder.objects.all() + + def get_serializer_context(self): + """Add the TransferOrder object to the serializer context.""" + context = super().get_serializer_context() + + # Pass the Transfer instance through to the serializer for validation + try: + context['order'] = models.TransferOrder.objects.get( + pk=self.kwargs.get('pk', None) + ) + except Exception: + pass + + context['request'] = self.request + + return context + + +class TransferOrderCancel(TransferOrderContextMixin, CreateAPI): + """API endpoint to cancel a TransferOrder.""" + + serializer_class = serializers.TransferOrderCancelSerializer + + +class TransferOrderHold(TransferOrderContextMixin, CreateAPI): + """API endpoint to hold a TransferOrder.""" + + serializer_class = serializers.TransferOrderHoldSerializer + + +class TransferOrderComplete(TransferOrderContextMixin, CreateAPI): + """API endpoint to complete a TransferOrder.""" + + serializer_class = serializers.TransferOrderCompleteSerializer + + +class TransferOrderIssue(TransferOrderContextMixin, CreateAPI): + """API endpoint to issue a Transfer Order.""" + + serializer_class = serializers.TransferOrderIssueSerializer + + +class TransferOrderAllocateSerials(TransferOrderContextMixin, CreateAPI): + """API endpoint to allocation stock items against a TransferOrder, by specifying serial numbers.""" + + queryset = models.TransferOrder.objects.none() + serializer_class = serializers.TransferOrderSerialAllocationSerializer + + +class TransferOrderAllocate(TransferOrderContextMixin, CreateAPI): + """API endpoint to allocate stock items against a TransferOrder. + + - The TransferOrder is specified in the URL + - See the TransferOrderAllocationSerializer class + """ + + queryset = models.TransferOrder.objects.none() + serializer_class = serializers.TransferOrderLineItemAllocationSerializer + + +class TransferOrderAllocationFilter(FilterSet): + """Custom filterset for the TransferOrderAllocationList endpoint.""" + + class Meta: + """Metaclass options.""" + + model = models.TransferOrderAllocation + fields = ['line', 'item'] + + order = rest_filters.ModelChoiceFilter( + queryset=models.TransferOrder.objects.all(), + field_name='line__order', + label=_('Order'), + ) + + include_variants = rest_filters.BooleanFilter( + label=_('Include Variants'), method='filter_include_variants' + ) + + def filter_include_variants(self, queryset, name, value): + """Filter by whether or not to include variants of the selected part. + + Note: + - This filter does nothing by itself, and requires the 'part' filter to be set. + - Refer to the 'filter_part' method for more information. + """ + return queryset + + part = rest_filters.ModelChoiceFilter( + queryset=Part.objects.all(), method='filter_part', label=_('Part') + ) + + @extend_schema_field(rest_framework.serializers.IntegerField(help_text=_('Part'))) + def filter_part(self, queryset, name, part): + """Filter by the 'part' attribute. + + Note: + - If "include_variants" is True, include all variants of the selected part + - Otherwise, just filter by the selected part + """ + include_variants = str2bool(self.data.get('include_variants', False)) + + if include_variants: + parts = part.get_descendants(include_self=True) + return queryset.filter(item__part__in=parts) + else: + return queryset.filter(item__part=part) + + outstanding = rest_filters.BooleanFilter( + label=_('Outstanding'), method='filter_outstanding' + ) + + def filter_outstanding(self, queryset, name, value): + """Filter by "outstanding" status (boolean).""" + if str2bool(value): + return queryset.filter( + line__order__status__in=TransferOrderStatusGroups.OPEN + # TODO: is there an additional filter here if we aren't using a "shipment" + # shipment__shipment_date=None, + ) + return queryset.exclude( + # TODO: is there an additional filter here if we aren't using a "shipment" + # shipment__shipment_date=None, + line__order__status__in=TransferOrderStatusGroups.OPEN + ) + + location = rest_filters.ModelChoiceFilter( + queryset=stock_models.StockLocation.objects.all(), + label=_('Location'), + method='filter_location', + ) + + @extend_schema_field( + rest_framework.serializers.IntegerField(help_text=_('Location')) + ) + def filter_location(self, queryset, name, location): + """Filter by the location of the allocated StockItem.""" + locations = location.get_descendants(include_self=True) + return queryset.filter(item__location__in=locations) + + +class TransferOrderAllocationMixin: + """Mixin class for TransferOrderAllocation endpoints.""" + + queryset = models.TransferOrderAllocation.objects.all() + serializer_class = serializers.TransferOrderAllocationSerializer + + def get_queryset(self, *args, **kwargs): + """Annotate the queryset for this endpoint.""" + queryset = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related( + 'item', + 'item__sales_order', + 'item__part', + 'line__part', + 'item__location', + 'line__order', + 'line__order__responsible', + 'line__order__project_code', + 'line__order__project_code__responsible', + ).select_related('line__part__pricing_data', 'item__part__pricing_data') + + return queryset + + +class TransferOrderAllocationOutputOptions(OutputConfiguration): + """Output options for the TransferOrderAllocation endpoint.""" + + OPTIONS = [ + InvenTreeOutputOption('part_detail'), + InvenTreeOutputOption('item_detail'), + InvenTreeOutputOption('order_detail'), + InvenTreeOutputOption('location_detail'), + ] + + +class TransferOrderAllocationList( + TransferOrderAllocationMixin, BulkUpdateMixin, OutputOptionsMixin, ListAPI +): + """API endpoint for listing TransferOrderAllocation objects.""" + + filterset_class = TransferOrderAllocationFilter + filter_backends = SEARCH_ORDER_FILTER + output_options = TransferOrderAllocationOutputOptions + + ordering_fields = [ + 'quantity', + 'part', + 'serial', + 'IPN', + 'batch', + 'location', + 'order', + ] + + ordering_field_aliases = { + 'IPN': 'item__part__IPN', + 'part': 'item__part__name', + 'serial': ['item__serial_int', 'item__serial'], + 'batch': 'item__batch', + 'location': 'item__location__name', + 'order': 'line__order__reference', + } + + search_fields = { + 'item__part__name', + 'item__part__IPN', + 'item__serial', + 'item__batch', + } + + +class TransferOrderAllocationDetail( + TransferOrderAllocationMixin, RetrieveUpdateDestroyAPI +): + """API endpoint for detail view of a TransferOrderAllocation object.""" + + +class TransferOrderLineItemFilter(LineItemFilter): + """Custom filters for TransferOrderLineItemList endpoint.""" + + class Meta: + """Metaclass options.""" + + model = models.TransferOrderLineItem + fields = [] + + order = rest_filters.ModelChoiceFilter( + queryset=models.TransferOrder.objects.all(), + field_name='order', + label=_('Order'), + ) + + def filter_include_variants(self, queryset, name, value): + """Filter by whether or not to include variants of the selected part. + + Note: + - This filter does nothing by itself, and requires the 'part' filter to be set. + - Refer to the 'filter_part' method for more information. + """ + return queryset + + part = rest_filters.ModelChoiceFilter( + queryset=Part.objects.all(), + field_name='part', + label=_('Part'), + method='filter_part', + ) + + @extend_schema_field(OpenApiTypes.INT) + def filter_part(self, queryset, name, part): + """Filter TransferOrderLineItem by selected 'part'. + + Note: + - If 'include_variants' is set to True, then all variants of the selected part will be included. + - Otherwise, just filter by the selected part. + """ + include_variants = str2bool(self.data.get('include_variants', False)) + + # Construct a queryset of parts to filter by + if include_variants: + parts = part.get_descendants(include_self=True) + else: + parts = Part.objects.filter(pk=part.pk) + + return queryset.filter(part__in=parts) + + allocated = rest_filters.BooleanFilter( + label=_('Allocated'), method='filter_allocated' + ) + + def filter_allocated(self, queryset, name, value): + """Filter by lines which are 'allocated'. + + A line is 'allocated' when allocated >= quantity + """ + q = Q(allocated__gte=F('quantity')) + + if str2bool(value): + return queryset.filter(q) + return queryset.exclude(q) + + completed = rest_filters.BooleanFilter( + label=_('Completed'), method='filter_completed' + ) + + def filter_completed(self, queryset, name, value): + """Filter by lines which are "completed". + + A line is 'completed' when transferred >= quantity + """ + q = Q(transferred__gte=F('quantity')) + + if str2bool(value): + return queryset.filter(q) + return queryset.exclude(q) + + order_complete = rest_filters.BooleanFilter( + label=_('Order Complete'), method='filter_order_complete' + ) + + def filter_order_complete(self, queryset, name, value): + """Filter by whether the order is 'complete' or not.""" + if str2bool(value): + return queryset.filter(order__status__in=TransferOrderStatusGroups.COMPLETE) + + return queryset.exclude(order__status__in=TransferOrderStatusGroups.COMPLETE) + + order_outstanding = rest_filters.BooleanFilter( + label=_('Order Outstanding'), method='filter_order_outstanding' + ) + + def filter_order_outstanding(self, queryset, name, value): + """Filter by whether the order is 'outstanding' or not.""" + if str2bool(value): + return queryset.filter(order__status__in=TransferOrderStatusGroups.OPEN) + + return queryset.exclude(order__status__in=TransferOrderStatusGroups.OPEN) + + +class TransferOrderLineItemMixin(SerializerContextMixin): + """Mixin class for TransferOrderLineItem endpoints.""" + + queryset = models.TransferOrderLineItem.objects.all() + serializer_class = serializers.TransferOrderLineItemSerializer + + def get_queryset(self, *args, **kwargs): + """Return annotated queryset for this endpoint.""" + queryset = super().get_queryset(*args, **kwargs) + + queryset = queryset.prefetch_related( + 'part', + 'allocations', + # 'allocations__transfer', + 'allocations__item__part', + 'allocations__item__location', + 'order', + ) + + queryset = serializers.TransferOrderLineItemSerializer.annotate_queryset( + queryset + ) + + return queryset + + +class TransferOrderLineItemOutputOptions(OutputConfiguration): + """Output options for the TransferOrderAllocation endpoint.""" + + OPTIONS = [ + InvenTreeOutputOption('part_detail'), + InvenTreeOutputOption('order_detail'), + ] + + +class TransferOrderLineItemList( + TransferOrderLineItemMixin, DataExportViewMixin, OutputOptionsMixin, ListCreateAPI +): + """API endpoint for accessing a list of TransferOrderLineItem objects.""" + + filterset_class = TransferOrderLineItemFilter + + filter_backends = SEARCH_ORDER_FILTER + + output_options = TransferOrderLineItemOutputOptions + + ordering_fields = [ + 'order', + 'part', + 'part__name', + 'quantity', + 'allocated', + 'transferred', + 'reference', + 'target_date', + ] + + ordering_field_aliases = {'part': 'part__name', 'order': 'order__reference'} + + search_fields = ['part__name', 'quantity', 'reference'] + + +class TransferOrderLineItemDetail( + TransferOrderLineItemMixin, OutputOptionsMixin, RetrieveUpdateDestroyAPI +): + """API endpoint for detail view of a TransferOrderLineItem object.""" + + output_options = TransferOrderLineItemOutputOptions + + class OrderCalendarExport(ICalFeed): """Calendar export for Purchase/Sales Orders. @@ -1844,6 +2361,8 @@ class OrderCalendarExport(ICalFeed): ordertype_title = _('Sales Order') elif obj['ordertype'] == 'return-order': ordertype_title = _('Return Order') + elif obj['ordertype'] == 'transfer-order': + ordertype_title = _('Transfer Order') else: ordertype_title = _('Unknown') @@ -1889,6 +2408,15 @@ class OrderCalendarExport(ICalFeed): ).filter(status__lt=ReturnOrderStatus.COMPLETE.value) else: outlist = models.ReturnOrder.objects.filter(target_date__isnull=False) + elif obj['ordertype'] == 'transfer-order': + if obj['include_completed'] is False: + # Do not include completed orders from list in this case + # Complete status = 30 + outlist = models.TransferOrder.objects.filter( + target_date__isnull=False + ).filter(status__lt=TransferOrderStatus.COMPLETE.value) + else: + outlist = models.TransferOrder.objects.filter(target_date__isnull=False) else: outlist = [] @@ -1900,7 +2428,12 @@ class OrderCalendarExport(ICalFeed): def item_description(self, item): """Set the event description.""" - return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}' + if hasattr(item, 'company') and item.company: + return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}' + else: + return ( + f'Status: {item.get_status_display()}\nDescription: {item.description}' + ) def item_start_datetime(self, item): """Set event start to target date. Goal is all-day event.""" @@ -2216,9 +2749,97 @@ order_api_urls = [ ), ]), ), + # API endpoints for transfer orders + path( + 'transfer-order/', + include([ + # Transfer Order detail endpoints + path( + '/', + include([ + path( + 'allocate/', + TransferOrderAllocate.as_view(), + name='api-transfer-order-allocate', + ), + path( + 'allocate-serials/', + TransferOrderAllocateSerials.as_view(), + name='api-transfer-order-allocate-serials', + ), + path( + 'cancel/', + TransferOrderCancel.as_view(), + name='api-transfer-order-cancel', + ), + path( + 'hold/', + TransferOrderHold.as_view(), + name='api-transfer-order-hold', + ), + path( + 'complete/', + TransferOrderComplete.as_view(), + name='api-transfer-order-complete', + ), + path( + 'issue/', + TransferOrderIssue.as_view(), + name='api-transfer-order-issue', + ), + meta_path(models.TransferOrder), + path( + '', + TransferOrderDetail.as_view(), + name='api-transfer-order-detail', + ), + ]), + ), + # Transfer Order list + path('', TransferOrderList.as_view(), name='api-transfer-order-list'), + ]), + ), + # API endpoints for transfer order line items + path( + 'transfer-order-line/', + include([ + path( + '/', + include([ + meta_path(models.TransferOrderLineItem), + path( + '', + TransferOrderLineItemDetail.as_view(), + name='api-transfer-order-line-detail', + ), + ]), + ), + path( + '', + TransferOrderLineItemList.as_view(), + name='api-transfer-order-line-list', + ), + ]), + ), + # API endpoints for sales order allocations + path( + 'transfer-order-allocation/', + include([ + path( + '/', + TransferOrderAllocationDetail.as_view(), + name='api-transfer-order-allocation-detail', + ), + path( + '', + TransferOrderAllocationList.as_view(), + name='api-transfer-order-allocation-list', + ), + ]), + ), # API endpoint for subscribing to ICS calendar of purchase/sales/return orders re_path( - r'^calendar/(?Ppurchase-order|sales-order|return-order)/calendar.ics', + r'^calendar/(?Ppurchase-order|sales-order|return-order|transfer-order)/calendar.ics', OrderCalendarExport(), name='api-po-so-calendar', ), diff --git a/src/backend/InvenTree/order/events.py b/src/backend/InvenTree/order/events.py index 0d67a3e11b..4f17e26e35 100644 --- a/src/backend/InvenTree/order/events.py +++ b/src/backend/InvenTree/order/events.py @@ -37,3 +37,12 @@ class ReturnOrderEvents(BaseEventEnum): COMPLETED = 'returnorder.completed' CANCELLED = 'returnorder.cancelled' HOLD = 'returnorder.hold' + + +class TransferOrderEvents(BaseEventEnum): + """Event enumeration for the PurchaseOrder models.""" + + ISSUED = 'transferorder.placed' + COMPLETED = 'transferorder.completed' + CANCELLED = 'transferorder.cancelled' + HOLD = 'transferorder.hold' diff --git a/src/backend/InvenTree/order/fixtures/transfer_order.yaml b/src/backend/InvenTree/order/fixtures/transfer_order.yaml new file mode 100644 index 0000000000..e4293d0f9a --- /dev/null +++ b/src/backend/InvenTree/order/fixtures/transfer_order.yaml @@ -0,0 +1,68 @@ +- model: order.transferorder + pk: 1 + fields: + reference: 'TO-123' + description: "One transfer order, please" + status: 10 # Pending + +- model: order.transferorder + pk: 2 + fields: + reference: 'TO-124' + description: "One transfer order, please" + status: 40 # Cancelled + +- model: order.transferorder + pk: 3 + fields: + reference: 'TO-125' + description: "One transfer order, please" + status: 25 # On Hold + +- model: order.transferorder + pk: 4 + fields: + reference: 'TO-126' + description: "One transfer order, please" + status: 20 # Issued + +- model: order.transferorder + pk: 5 + fields: + reference: 'TO-127' + description: "One transfer order, please" + status: 30 # Complete + + +# Line items for transfer orders +- model: order.transferorderlineitem + pk: 1 + fields: + order: 5 # the completed order + part: 10001 # blue chair + quantity: 1 + +- model: order.transferorderlineitem + pk: 2 + fields: + order: 4 # the issued order + part: 10001 # blue chair + quantity: 1 + transferred: 1 + +# Allocations for transfer orders +# an 'allocated' allocation +- model: order.transferorderallocation + pk: 1 + fields: + line: 1 # the line item on the completed order + item: 1 # stock item + quantity: 1 + +# a 'complete' allocation +- model: order.transferorderallocation + pk: 2 + fields: + line: 2 # the line item on the issued order + item: 500 # stock item for the blue chair + quantity: 1 diff --git a/src/backend/InvenTree/order/migrations/0118_transferorder.py b/src/backend/InvenTree/order/migrations/0118_transferorder.py new file mode 100644 index 0000000000..ddaf02c8c6 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0118_transferorder.py @@ -0,0 +1,482 @@ +# Generated by Django 5.2.11 on 2026-02-27 22:00 + +import InvenTree.fields +import InvenTree.models +import django.core.validators +import django.db.models.deletion +import generic.states.fields +import generic.states.states +import generic.states.transition +import generic.states.validators +import order.status_codes +import order.validators +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("common", "0041_auto_20251203_1244"), + ("company", "0077_delete_manufacturerpartparameter"), + ("order", "0117_purchaseorderextraline_line_int_and_more"), + ("part", "0146_auto_20251203_1241"), + ("stock", "0116_alter_stockitem_link"), + ("users", "0005_owner_model"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="TransferOrder", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "metadata", + models.JSONField( + blank=True, + help_text="JSON metadata field, for use by external plugins", + null=True, + verbose_name="Plugin Metadata", + ), + ), + ("reference_int", models.BigIntegerField(default=0)), + ( + "notes", + InvenTree.fields.InvenTreeNotesField( + blank=True, + help_text="Markdown notes (optional)", + max_length=50000, + null=True, + verbose_name="Notes", + ), + ), + ( + "barcode_data", + models.CharField( + blank=True, + help_text="Third party barcode data", + max_length=500, + verbose_name="Barcode Data", + ), + ), + ( + "barcode_hash", + models.CharField( + blank=True, + help_text="Unique hash of barcode data", + max_length=128, + verbose_name="Barcode Hash", + ), + ), + ( + "description", + models.CharField( + blank=True, + help_text="Order description (optional)", + max_length=250, + verbose_name="Description", + ), + ), + ( + "link", + InvenTree.fields.InvenTreeURLField( + blank=True, + help_text="Link to external page", + max_length=2000, + verbose_name="Link", + ), + ), + ( + "start_date", + models.DateField( + blank=True, + help_text="Scheduled start date for this order", + null=True, + verbose_name="Start date", + ), + ), + ( + "target_date", + models.DateField( + blank=True, + help_text="Expected date for order delivery. Order will be overdue after this date.", + null=True, + verbose_name="Target Date", + ), + ), + ( + "creation_date", + models.DateField( + blank=True, null=True, verbose_name="Creation Date" + ), + ), + ( + "issue_date", + models.DateField( + blank=True, + help_text="Date order was issued", + null=True, + verbose_name="Issue Date", + ), + ), + ( + "reference", + models.CharField( + default=order.validators.generate_next_transfer_order_reference, + help_text="Transfer Order Reference", + max_length=64, + unique=True, + validators=[order.validators.validate_transfer_order_reference], + verbose_name="Reference", + ), + ), + ( + "consume", + models.BooleanField( + default=False, + help_text='Rather than transfer the stock to the destination, "consume" it, by removing transferred quantity from the allocated stock item', + verbose_name="Consume Stock", + ), + ), + ( + "complete_date", + models.DateField( + blank=True, + help_text="Date order was completed", + null=True, + verbose_name="Completion Date", + ), + ), + ( + "status_custom_key", + generic.states.fields.ExtraInvenTreeCustomStatusModelField( + blank=True, + default=None, + help_text="Additional status information for this item", + null=True, + validators=[ + generic.states.validators.CustomStatusCodeValidator( + status_class=order.status_codes.TransferOrderStatus + ) + ], + verbose_name="Custom status key", + ), + ), + ( + "status", + generic.states.fields.InvenTreeCustomStatusModelField( + choices=[ + (10, "Pending"), + (20, "Issued"), + (25, "On Hold"), + (30, "Complete"), + (40, "Cancelled"), + ], + default=10, + help_text="Transfer order status", + validators=[ + generic.states.validators.CustomStatusCodeValidator( + status_class=order.status_codes.TransferOrderStatus + ) + ], + verbose_name="Status", + ), + ), + ( + "address", + models.ForeignKey( + blank=True, + help_text="Company address for this order", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="company.address", + verbose_name="Address", + ), + ), + ( + "contact", + models.ForeignKey( + blank=True, + help_text="Point of contact for this order", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="company.contact", + verbose_name="Contact", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "destination", + models.ForeignKey( + blank=True, + help_text="Destination for transferred items", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="incoming_transfers", + to="stock.stocklocation", + verbose_name="Destination Location", + ), + ), + ( + "project_code", + models.ForeignKey( + blank=True, + help_text="Select project code for this order", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="common.projectcode", + verbose_name="Project Code", + ), + ), + ( + "responsible", + models.ForeignKey( + blank=True, + help_text="User or group responsible for this order", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="users.owner", + verbose_name="Responsible", + ), + ), + ( + "take_from", + models.ForeignKey( + blank=True, + help_text="Source for transferred items", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="sourcing_transfers", + to="stock.stocklocation", + verbose_name="Source Location", + ), + ), + ( + "updated_at", + models.DateTimeField( + blank=True, + help_text="Timestamp of last update", + null=True, + verbose_name="Updated At", + ), + ) + ], + options={ + "verbose_name": "Transfer Order", + }, + bases=( + generic.states.states.StatusCodeMixin, + generic.states.transition.StateTransitionMixin, + InvenTree.models.InvenTreeAttachmentMixin, + InvenTree.models.InvenTreePermissionCheckMixin, + InvenTree.models.ContentTypeMixin, + InvenTree.models.PluginValidationMixin, + models.Model, + ), + ), + migrations.CreateModel( + name="TransferOrderLineItem", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "metadata", + models.JSONField( + blank=True, + help_text="JSON metadata field, for use by external plugins", + null=True, + verbose_name="Plugin Metadata", + ), + ), + ( + "quantity", + InvenTree.fields.RoundingDecimalField( + decimal_places=5, + default=1, + help_text="Item quantity", + max_digits=15, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Quantity", + ), + ), + ( + "reference", + models.CharField( + blank=True, + help_text="Line item reference", + max_length=100, + verbose_name="Reference", + ), + ), + ( + "notes", + models.CharField( + blank=True, + help_text="Line item notes", + max_length=500, + verbose_name="Notes", + ), + ), + ( + "link", + InvenTree.fields.InvenTreeURLField( + blank=True, + help_text="Link to external page", + max_length=2000, + verbose_name="Link", + ), + ), + ( + "target_date", + models.DateField( + blank=True, + help_text="Target date for this line item (leave blank to use the target date from the order)", + null=True, + verbose_name="Target Date", + ), + ), + ( + "order", + models.ForeignKey( + help_text="Transfer Order", + on_delete=django.db.models.deletion.CASCADE, + related_name="lines", + to="order.transferorder", + verbose_name="Order", + ), + ), + ( + "part", + models.ForeignKey( + help_text="Part", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="transfer_order_line_items", + to="part.part", + verbose_name="Part", + ), + ), + ( + "project_code", + models.ForeignKey( + blank=True, + help_text="Select project code for this order", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="common.projectcode", + verbose_name="Project Code", + ), + ), + ( + "transferred", + InvenTree.fields.RoundingDecimalField( + decimal_places=5, + default=0, + help_text="transferred quantity", + max_digits=15, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="transferred", + ), + ), + ( + "line", + models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ) + ], + options={ + "verbose_name": "Transfer Order Line Item", + }, + bases=( + InvenTree.models.ContentTypeMixin, + InvenTree.models.PluginValidationMixin, + models.Model, + ), + ), + migrations.CreateModel( + name="TransferOrderAllocation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "quantity", + InvenTree.fields.RoundingDecimalField( + decimal_places=5, + default=1, + help_text="Enter stock allocation quantity", + max_digits=15, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Quantity", + ), + ), + ( + "item", + models.ForeignKey( + help_text="Select stock item to allocate", + limit_choices_to={ + "belongs_to": None, + "part__virtual": False, + "sales_order": None, + }, + on_delete=django.db.models.deletion.CASCADE, + related_name="transfer_order_allocations", + to="stock.stockitem", + verbose_name="Item", + ), + ), + ( + "line", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="allocations", + to="order.transferorderlineitem", + verbose_name="Line", + ), + ), + ], + options={ + "verbose_name": "Transfer Order Allocation", + }, + ), + ] diff --git a/src/backend/InvenTree/order/migrations/0119_transferorderlineitem_line_int.py b/src/backend/InvenTree/order/migrations/0119_transferorderlineitem_line_int.py new file mode 100644 index 0000000000..e9ad06975c --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0119_transferorderlineitem_line_int.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.13 on 2026-05-11 21:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("order", "0118_transferorder"), + ] + + operations = [ + migrations.AddField( + model_name="transferorderlineitem", + name="line_int", + field=models.IntegerField(default=0), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index c597e8c011..5973f4292b 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -45,7 +45,12 @@ from InvenTree.fields import ( ) from InvenTree.helpers import decimal2string, pui_url from InvenTree.helpers_model import notify_responsible -from order.events import PurchaseOrderEvents, ReturnOrderEvents, SalesOrderEvents +from order.events import ( + PurchaseOrderEvents, + ReturnOrderEvents, + SalesOrderEvents, + TransferOrderEvents, +) from order.status_codes import ( PurchaseOrderStatus, PurchaseOrderStatusGroups, @@ -54,6 +59,8 @@ from order.status_codes import ( ReturnOrderStatusGroups, SalesOrderStatus, SalesOrderStatusGroups, + TransferOrderStatus, + TransferOrderStatusGroups, ) from part import models as PartModels from plugin.events import trigger_event @@ -265,6 +272,27 @@ class ReturnOrderReportContext(report.mixins.BaseReportContext, TypedDict): customer: Optional[Company] +class TransferOrderReportContext(report.mixins.BaseReportContext, TypedDict): + """Context for the transfer order model. + + Attributes: + description: The description field of the TransferOrder + reference: The reference field of the TransferOrder + title: The title (string representation) of the TransferOrder + lines: Query set of all line items associated with the TransferOrder + order: The TransferOrder instance itself + """ + + description: str + reference: str + title: str + lines: report.mixins.QuerySet['TransferOrderLineItem'] + order: 'TransferOrder' + take_from: 'stock.models.StockLocation' + destination: 'stock.models.StockLocation' + consume: bool + + class Order( StatusCodeMixin, StateTransitionMixin, @@ -374,11 +402,16 @@ class Order( }) # Check that the referenced 'contact' matches the correct 'company' - if self.company and self.contact: - if self.contact.company != self.company: - raise ValidationError({ - 'contact': _('Contact does not match selected company') - }) + if ( + hasattr(self, 'company') + and hasattr(self, 'contact') + and self.company + and self.contact + and (self.contact.company != self.company) + ): + raise ValidationError({ + 'contact': _('Contact does not match selected company') + }) # Target date should be *after* the start date if self.start_date and self.target_date and self.start_date > self.target_date: @@ -388,11 +421,15 @@ class Order( }) # Check that the referenced 'address' matches the correct 'company' - if self.company and self.address: - if self.address.company != self.company: - raise ValidationError({ - 'address': _('Address does not match selected company') - }) + if ( + hasattr(self, 'company') + and self.company + and self.address + and (self.address.company != self.company) + ): + raise ValidationError({ + 'address': _('Address does not match selected company') + }) def clean_line_item(self, line): """Clean a line item for this order. @@ -408,7 +445,9 @@ class Order( """Generate context data for the reporting interface.""" return { 'description': self.description, - 'extra_lines': self.extra_lines, + 'extra_lines': getattr( + self, 'extra_lines', None + ), # Transfer Order doesn't have extra lines 'lines': self.lines, 'order': self, 'reference': self.reference, @@ -3155,6 +3194,628 @@ class ReturnOrderExtraLine(OrderExtraLine): ) +class TransferOrder(Order): + """A Transfer Order represents a request to transfer stock from one location to another. It provides a place to queue and review changes before execution. + + Attributes: + take_from: The stock location to source items from (or null to ) + destination: The stock location to move items to + consume: Rather than move the stock, "consume" it. Helpful if you want to queue up removing stock from inventory + """ + + # Global setting for specifying reference pattern + REFERENCE_PATTERN_SETTING = 'TRANSFERORDER_REFERENCE_PATTERN' + REQUIRE_RESPONSIBLE_SETTING = 'TRANSFERORDER_REQUIRE_RESPONSIBLE' + STATUS_CLASS = TransferOrderStatus + # UNLOCK_SETTING = 'TRANSFERORDER_EDIT_COMPLETED_ORDERS' + + class Meta: + """Model meta options.""" + + verbose_name = _('Transfer Order') + + def report_context(self) -> TransferOrderReportContext: + """Return report context data for this TransferOrder.""" + return { + **super().report_context(), + 'take_from': self.take_from, + 'destination': self.destination, + 'consume': self.consume, + } + + def get_absolute_url(self) -> str: + """Get the 'web' URL for this order.""" + return pui_url(f'/stock/transfer-order/{self.pk}') + + @staticmethod + def get_api_url() -> str: + """Return the API URL associated with the TransferOrder model.""" + return reverse('api-transfer-order-list') + + @classmethod + def get_status_class(cls): + """Return the TransferOrderStatus class.""" + return TransferOrderStatusGroups + + @classmethod + def api_defaults(cls, request=None): + """Return default values for this model when issuing an API OPTIONS request.""" + defaults = { + 'reference': order.validators.generate_next_transfer_order_reference() + } + + return defaults + + @classmethod + def barcode_model_type_code(cls): + """Return the associated barcode model type code for this model.""" + return 'TO' + + def subscribed_users(self) -> list[User]: + """Return a list of users subscribed to this TransferOrder. + + By this, we mean users to are interested in any of the parts associated with this order. + """ + subscribed_users = set() + + for line in self.lines.all(): + if line.part: + # Add the part to the list of subscribed users + for user in line.part.get_subscribers(): + subscribed_users.add(user) + + return list(subscribed_users) + + def clean_line_item(self, line): + """Clean a line item for this PurchaseOrder.""" + super().clean_line_item(line) + line.transferred = 0 + + def __str__(self): + """Render a string representation of this TransferOrder.""" + return f'{self.reference} - {self.take_from.name if self.take_from else _("deleted")} --> {self.destination.name if self.destination else _("deleted")}' + + reference = models.CharField( + unique=True, + max_length=64, + blank=False, + help_text=_('Transfer Order Reference'), + verbose_name=_('Reference'), + default=order.validators.generate_next_transfer_order_reference, + validators=[order.validators.validate_transfer_order_reference], + ) + + status = InvenTreeCustomStatusModelField( + default=TransferOrderStatus.PENDING.value, + choices=TransferOrderStatus.items(), + status_class=TransferOrderStatus, + verbose_name=_('Status'), + help_text=_('Transfer order status'), + ) + + @property + def status_text(self): + """Return the text representation of the status field.""" + return TransferOrderStatus.text(self.status) + + take_from = models.ForeignKey( + 'stock.StockLocation', + verbose_name=_('Source Location'), + on_delete=models.SET_NULL, + related_name='sourcing_transfers', + blank=True, + null=True, + help_text=_('Source for transferred items'), + ) + + destination = models.ForeignKey( + 'stock.StockLocation', + verbose_name=_('Destination Location'), + on_delete=models.SET_NULL, + related_name='incoming_transfers', + blank=True, + null=True, + help_text=_('Destination for transferred items'), + ) + + consume = models.BooleanField( + default=False, + verbose_name=_('Consume Stock'), + help_text=_( + 'Rather than transfer the stock to the destination, "consume" it, by removing transferred quantity from the allocated stock item' + ), + ) + + complete_date = models.DateField( + blank=True, + null=True, + verbose_name=_('Completion Date'), + help_text=_('Date order was completed'), + ) + + @property + def company(self) -> None: + """Required accessor helper for Order base class.""" + return None + + @property + def is_pending(self) -> bool: + """Return True if the TransferOrder is 'pending'.""" + return self.status == TransferOrderStatus.PENDING.value + + @property + def is_open(self) -> bool: + """Return True if the TransferOrder is 'open'.""" + return self.status in TransferOrderStatusGroups.OPEN + + @property + def stock_allocations(self) -> QuerySet: + """Return a queryset containing all allocations for this order.""" + return TransferOrderAllocation.objects.filter( + line__in=[line.pk for line in self.lines.all()] + ) + + def is_fully_allocated(self) -> bool: + """Return True if all line items are fully allocated.""" + return all(line.is_fully_allocated() for line in self.lines.all()) + + def is_overallocated(self) -> bool: + """Return true if any lines in the order are over-allocated.""" + return any(line.is_overallocated() for line in self.lines.all()) + + def is_completed(self) -> bool: + """Check if this order is "transferred" (all line items transferred).""" + return all(line.is_completed() for line in self.lines.all()) + + def can_complete( + self, raise_error: bool = False, allow_incomplete_lines: bool = False + ) -> bool: + """Test if this TransferOrder can be completed.""" + try: + if self.status == TransferOrderStatus.COMPLETE.value: + raise ValidationError(_('Order is already complete')) + + if self.status == TransferOrderStatus.CANCELLED.value: + raise ValidationError(_('Order is already cancelled')) + + if not self.consume and not self.destination: + raise ValidationError( + _('Order cannot be completed until a destination location is set') + ) + + if not (self.is_fully_allocated() or allow_incomplete_lines): + raise ValidationError( + _('Order cannot be completed until it is fully allocated') + ) + except ValidationError as e: + if raise_error: + raise e + else: + return False + + return True + + @property + def can_issue(self) -> bool: + """Return True if this order can be issued.""" + return self.status in [ + TransferOrderStatus.PENDING.value, + TransferOrderStatus.ON_HOLD.value, + ] + + @transaction.atomic + def issue_order(self): + """Attempt to transition to PLACED status.""" + return self.handle_transition( + self.status, TransferOrderStatus.ISSUED.value, self, self._action_issue + ) + + # region state changes + def _action_issue(self, *args, **kwargs): + """Marks the TransferOrder as ISSUED. + + Order must be currently PENDING. + """ + if self.can_issue: + self.status = TransferOrderStatus.ISSUED.value + self.issue_date = InvenTree.helpers.current_date() + self.save() + + trigger_event(TransferOrderEvents.ISSUED, id=self.pk) + + # Notify users that the order has been issued + notify_responsible( + self, + TransferOrder, + exclude=self.created_by, + content=InvenTreeNotificationBodies.NewOrder, + extra_users=self.subscribed_users(), + ) + + @property + def can_hold(self) -> bool: + """Return True if this order can be placed on hold.""" + return self.status in [ + TransferOrderStatus.PENDING.value, + TransferOrderStatus.ISSUED.value, + ] + + def _action_hold(self, *args, **kwargs): + """Mark this transfer order as 'on hold'.""" + if self.can_hold: + self.status = TransferOrderStatus.ON_HOLD.value + self.save() + + trigger_event(TransferOrderEvents.HOLD, id=self.pk) + + @transaction.atomic + def _action_complete(self, *args, **kwargs): + """Marks the TransferOrder as COMPLETE. + + Order must be currently ISSUED. + """ + user = kwargs.pop('user', None) + + if not self.can_complete(raise_error=True, **kwargs): + return False + + if self.status == TransferOrderStatus.ISSUED: + for allocation in self.allocations(): + # execute each transfer + allocation.complete_allocation(user) + + self.status = TransferOrderStatus.COMPLETE.value + self.complete_date = InvenTree.helpers.current_date() + + self.save() + + trigger_event(TransferOrderEvents.COMPLETED, id=self.pk) + + return True + + @transaction.atomic + def complete_order(self, user, **kwargs): + """Attempt to transition to COMPLETE status.""" + return self.handle_transition( + self.status, + TransferOrderStatus.COMPLETE.value, + self, + self._action_complete, + user=user, + **kwargs, + ) + + @transaction.atomic + def hold_order(self): + """Attempt to transition to ON_HOLD status.""" + return self.handle_transition( + self.status, TransferOrderStatus.ON_HOLD.value, self, self._action_hold + ) + + @transaction.atomic + def cancel_order(self): + """Attempt to transition to CANCELLED status.""" + return self.handle_transition( + self.status, TransferOrderStatus.CANCELLED.value, self, self._action_cancel + ) + + @property + def can_cancel(self) -> bool: + """A TransferOrder can only be cancelled under the following circumstances. + + - Status is ISSUED + - Status is PENDING (or ON_HOLD) + """ + return self.status in TransferOrderStatusGroups.OPEN + + def _action_cancel(self, *args, **kwargs): + """Cancel this TransferOrder (only if we're allowed to). + + Executes: + - Mark the order as 'cancelled' + - Delete any StockItems which have been allocated + """ + if not self.can_cancel: + return False + + self.status = TransferOrderStatus.CANCELLED.value + self.save() + + # delete allocations + for line in self.lines.all(): + for allocation in line.allocations.all(): + allocation.delete() + + trigger_event(TransferOrderEvents.CANCELLED, id=self.pk) + + # Notify users that the order has been canceled + notify_responsible( + self, + TransferOrder, + exclude=self.created_by, + content=InvenTreeNotificationBodies.OrderCanceled, + extra_users=self.subscribed_users(), + ) + + # endregion + + @property + def line_count(self) -> int: + """Return the total number of lines associated with this order.""" + return self.lines.count() + + def completed_line_items(self) -> QuerySet: + """Return a queryset of the completed line items for this order.""" + return self.lines.filter(transferred__gte=F('quantity')) + + def pending_line_items(self) -> QuerySet: + """Return a queryset of the pending line items for this order.""" + return self.lines.filter(transferred__lt=F('quantity')) + + @property + def completed_line_count(self) -> int: + """Return the number of completed lines for this order.""" + return self.completed_line_items().count() + + @property + def pending_line_count(self) -> int: + """Return the number of pending (incomplete) lines associated with this order.""" + return self.pending_line_items().count() + + def allocations(self) -> QuerySet: + """Return a queryset of all allocations for this order.""" + return TransferOrderAllocation.objects.filter(line__order=self) + + +class TransferOrderLineItem(OrderLineItem): + """Model for a single LineItem in a TransferOrder. + + Attributes: + order: Link to the TransferOrder that this line item belongs to + part: Link to a Part object (may be null) + transferred: The number of items which have actually transferred against this line item + """ + + class Meta: + """Model meta options.""" + + verbose_name = _('Transfer Order Line Item') + + # Filter for determining if a particular TransferOrderLineItem is overdue + OVERDUE_FILTER = ( + Q(transferred__lt=F('quantity')) + & ~Q(target_date=None) + & Q(target_date__lt=InvenTree.helpers.current_date()) + ) + + @staticmethod + def get_api_url(): + """Return the API URL associated with the TransferOrderLineItem model.""" + return reverse('api-transfer-order-line-list') + + order = models.ForeignKey( + TransferOrder, + on_delete=models.CASCADE, + related_name='lines', + verbose_name=_('Order'), + help_text=_('Transfer Order'), + ) + + part = models.ForeignKey( + 'part.Part', + on_delete=models.SET_NULL, + related_name='transfer_order_line_items', + null=True, + verbose_name=_('Part'), + help_text=_('Part'), + # limit_choices_to={'salable': True}, + ) + + transferred = RoundingDecimalField( + verbose_name=_('transferred'), + help_text=_('transferred quantity'), + default=0, + max_digits=15, + decimal_places=5, + validators=[MinValueValidator(0)], + ) + + def allocated_quantity(self): + """Return the total stock quantity allocated to this LineItem. + + This is a summation of the quantity of each attached StockItem + """ + if not self.pk: + return 0 + + query = self.allocations.aggregate( + allocated=Coalesce(Sum('quantity'), Decimal(0)) + ) + + return query['allocated'] + + def is_fully_allocated(self) -> bool: + """Return True if this line item is fully allocated.""" + # If the linked part is "virtual", then we cannot allocate stock against it + if self.part and self.part.virtual: + return True + + return self.allocated_quantity() >= self.quantity + + def is_overallocated(self) -> bool: + """Return True if this line item is over allocated.""" + return self.allocated_quantity() > self.quantity + + def is_completed(self) -> bool: + """Return True if this line item is completed (has been fully shipped).""" + # A "virtual" part is always considered to be "completed" + if self.part and self.part.virtual: + return True + + return self.transferred >= self.quantity + + +class TransferOrderAllocation(models.Model): + """This model is used to 'allocate' stock items to a TransferOrder. Items that are "allocated" to a TransferOrder are not yet "attached" to the order, but they will be once the order is fulfilled. + + Attributes: + line: TransferOrderLineItem reference + item: StockItem reference + quantity: Quantity to take from the StockItem + """ + + class Meta: + """Model meta options.""" + + verbose_name = _('Transfer Order Allocation') + + @staticmethod + def get_api_url(): + """Return the API URL associated with the TransferOrderAllocation model.""" + return reverse('api-transfer-order-allocation-list') + + def clean(self): + """Validate the TransferOrderAllocation object. + + Executes: + - Cannot allocate stock to a line item without a part reference + - The referenced part must match the part associated with the line item + - Allocated quantity cannot exceed the quantity of the stock item + - Allocation quantity must be "1" if the StockItem is serialized + - Allocation quantity cannot be zero + """ + super().clean() + + errors = {} + + try: + if not self.item: + raise ValidationError({'item': _('Stock item has not been assigned')}) + except stock.models.StockItem.DoesNotExist: + raise ValidationError({'item': _('Stock item has not been assigned')}) + + try: + if self.line.part != self.item.part: + variants = self.line.part.get_descendants(include_self=True) + if self.line.part not in variants: + errors['item'] = _( + 'Cannot allocate stock item to a line with a different part' + ) + except PartModels.Part.DoesNotExist: + errors['line'] = _('Cannot allocate stock to a line without a part') + + if self.quantity > self.item.quantity: + errors['quantity'] = _('Allocation quantity cannot exceed stock quantity') + + # Ensure that we do not 'over allocate' a stock item + build_allocation_count = self.item.build_allocation_count() + sales_allocation_count = self.item.sales_order_allocation_count( + exclude_allocations={'pk': self.pk} + ) + + total_allocation = ( + build_allocation_count + sales_allocation_count + self.quantity + ) + + if total_allocation > self.item.quantity: + errors['quantity'] = _('Stock item is over-allocated') + + if self.quantity <= 0: + errors['quantity'] = _('Allocation quantity must be greater than zero') + + if self.item.serial and self.quantity != 1: + errors['quantity'] = _('Quantity must be 1 for serialized stock item') + + if len(errors) > 0: + raise ValidationError(errors) + + line = models.ForeignKey( + TransferOrderLineItem, + on_delete=models.CASCADE, + verbose_name=_('Line'), + related_name='allocations', + ) + + item = models.ForeignKey( + 'stock.StockItem', + on_delete=models.CASCADE, + related_name='transfer_order_allocations', + limit_choices_to={ + 'part__virtual': False, + 'belongs_to': None, + 'sales_order': None, + }, + verbose_name=_('Item'), + help_text=_('Select stock item to allocate'), + ) + + quantity = RoundingDecimalField( + max_digits=15, + decimal_places=5, + validators=[MinValueValidator(0)], + default=1, + verbose_name=_('Quantity'), + help_text=_('Enter stock allocation quantity'), + ) + + def get_location(self): + """Return the value of the location associated with this allocation.""" + return self.item.location.id if self.item.location else None + + def get_po(self): + """Return the PurchaseOrder associated with this allocation.""" + return self.item.purchase_order + + def complete_allocation(self, user): + """Complete this allocation (called when the parent TransferOrder is marked as "completed"). + + Executes: + - Determine if the referenced StockItem needs to be "split" (if allocated quantity != stock quantity) + - Move the StockItem to the new location + - Updates the transferred qty + - If order is marked as "consume", reduce quantity rather than move + """ + order: TransferOrder = self.line.order + self.item: stock.models.StockItem # for type hints + self.line: TransferOrderLineItem # for type hints + + # The allocation is the only thing linking this stock item to the transfer + # As a result, we must keep the allocation present even after completion + # This means allocations to transfer orders don't affect "available" stock + # (otherwise it would permanently reduce available stock) + + if order.consume: + # rather than transferring the stock, we simply reduce its quantity to release it from tracked inventory + # NOTE: if delete_on_deplete is enabled, this will result in the "transferred stock" panel being empty + # after completion. A more sophesticated immutable tracking that doesn't rely on allocations + # would be helpful here + self.item.take_stock( + quantity=self.quantity, + user=user, + code=StockHistoryCode.STOCK_REMOVE, + transferorder=order, + ) + else: + if self.quantity < self.item.quantity: + # update our own reference to the StockItem which was split + self.item = self.item.splitStock( + quantity=self.quantity, + location=order.destination, + user=user, + transferorder=order, + ) + self.save() + else: + # move item directly, we don't have to split + self.item.move( + location=order.destination, user=user, transferorder=order, notes='' + ) + + # Update the transferred qty + self.line.transferred += self.quantity + self.line.save() + + def _touch_order_updated_at(instance): """Bump updated_at on the parent order without triggering a full save.""" if not InvenTree.ready.canAppAccessDatabase(allow_test=True): @@ -3190,6 +3851,16 @@ def _touch_order_updated_at(instance): @receiver( post_delete, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_delete' ) +@receiver( + post_save, + sender=TransferOrderLineItem, + dispatch_uid='transfer_order_lineitem_post_save', +) +@receiver( + post_delete, + sender=TransferOrderLineItem, + dispatch_uid='transfer_order_lineitem_post_delete', +) def update_order_on_lineitem_change(sender, instance, **kwargs): """Update parent order updated_at when any line item is saved or deleted.""" _touch_order_updated_at(instance) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 80a2765b03..c2d1b909ac 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -44,6 +44,7 @@ from order.status_codes import ( ReturnOrderLineStatus, ReturnOrderStatus, SalesOrderStatusGroups, + TransferOrderStatusGroups, ) from part.serializers import PartBriefSerializer from stock.status_codes import StockStatus @@ -2298,3 +2299,622 @@ class ReturnOrderExtraLineSerializer( 'allow_null': True, }, ) + + +@register_importer() +class TransferOrderSerializer( + NotesFieldMixin, + InvenTreeCustomStatusSerializerMixin, + AbstractOrderSerializer, + InvenTreeModelSerializer, +): + """Serializer for a TransferOrder object.""" + + class Meta: + """Metaclass options.""" + + model = order.models.TransferOrder + fields = AbstractOrderSerializer.order_fields([ + 'take_from', + 'take_from_detail', + 'destination', + 'destination_detail', + 'consume', + 'complete_date', + ]) + read_only_fields = ['creation_date'] + extra_kwargs = {} + + def skip_create_fields(self): + """Skip these fields when instantiating a new object.""" + fields = super().skip_create_fields() + + return [*fields, 'duplicate'] + + @staticmethod + def annotate_queryset(queryset): + """Add extra information to the queryset. + + - Number of line items in the TransferOrder + - Number of completed line items in the TransferOrder + - Overdue status of the TransferOrder + """ + queryset = AbstractOrderSerializer.annotate_queryset(queryset) + + queryset = queryset.annotate( + completed_lines=SubqueryCount( + 'lines', filter=Q(quantity__lte=F('transferred')) + ) + ) + + queryset = queryset.annotate( + overdue=Case( + When( + order.models.TransferOrder.overdue_filter(), + then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + + return queryset + + take_from_detail = OptionalField( + serializer_class=stock.serializers.LocationSerializer, + serializer_kwargs={ + 'source': 'take_from', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + default_include=True, + ) + + destination_detail = OptionalField( + serializer_class=stock.serializers.LocationSerializer, + serializer_kwargs={ + 'source': 'destination', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + default_include=True, + ) + + +class TransferOrderHoldSerializer(OrderAdjustSerializer): + """Serializer for placing a TransferOrder on hold.""" + + def save(self): + """Save the serializer to 'hold' the order.""" + self.order.hold_order() + + +class TransferOrderIssueSerializer(OrderAdjustSerializer): + """Serializer for issuing a transfer order.""" + + def save(self): + """Save the serializer to 'issue' the order.""" + self.order.issue_order() + + +class TransferOrderCancelSerializer(OrderAdjustSerializer): + """Serializer for cancelling a TransferOrder.""" + + def save(self): + """Save the serializer to 'cancel' the order.""" + if not self.order.can_cancel: + raise ValidationError(_('Order cannot be cancelled')) + + self.order.cancel_order() + + +class TransferOrderCompleteSerializer(OrderAdjustSerializer): + """Serializer for completing a transfer order.""" + + class Meta: + """Metaclass options.""" + + fields = ['accept_incomplete_allocation'] + + accept_incomplete_allocation = serializers.BooleanField( + label=_('Accept Incomplete Allocation'), + help_text=_('Allow order to complete with incomplete allocations'), + required=False, + default=False, + ) + + def validate_accept_incomplete_allocation(self, value): + """Check if the 'accept_incomplete_allocation' field is required.""" + order = self.context['order'] + + if not value and not order.is_fully_allocated(): + raise ValidationError(_('Order has incomplete allocations')) + + return value + + def get_context_data(self): + """Custom context information for this serializer.""" + order = self.context['order'] + + return {'is_complete': order.is_completed()} + + def validate(self, data): + """Custom validation for the serializer.""" + data = super().validate(data) + self.order.can_complete( + raise_error=True, + allow_incomplete_lines=str2bool( + data.get('accept_incomplete_allocation', False) + ), + ) + return data + + def save(self): + """Save the serializer to 'complete' the order.""" + request = self.context.get('request') + data = self.validated_data + user = request.user if request else None + + self.order.complete_order( + user=user, + allow_incomplete_lines=data.get('accept_incomplete_allocation', False), + ) + + +@register_importer() +class TransferOrderLineItemSerializer( + DataImportExportSerializerMixin, + AbstractLineItemSerializer, + InvenTreeModelSerializer, +): + """Serializer for a TransferOrderLineItem object.""" + + class Meta: + """Metaclass options.""" + + model = order.models.TransferOrderLineItem + fields = AbstractLineItemSerializer.line_fields([ + 'allocated', + 'overdue', + 'part', + 'part_detail', + 'transferred', + # Annotated fields for part stocking information + 'available_stock', + 'available_variant_stock', + 'building', + 'on_order', + # Filterable detail fields + ]) + + @staticmethod + def annotate_queryset(queryset): + """Add some extra annotations to this queryset. + + - "overdue" status (boolean field) + - "available_quantity" + - "building" + - "on_order" + """ + queryset = queryset.annotate( + overdue=Case( + When( + Q(order__status__in=TransferOrderStatusGroups.OPEN) + & order.models.TransferOrderLineItem.OVERDUE_FILTER, + then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + + # Annotate each line with the available stock quantity + # To do this, we need to look at the total stock and any allocations + queryset = queryset.alias( + total_stock=part_filters.annotate_total_stock(reference='part__'), + allocated_to_sales_orders=part_filters.annotate_sales_order_allocations( + reference='part__' + ), + allocated_to_build_orders=part_filters.annotate_build_order_allocations( + reference='part__' + ), + ) + + queryset = queryset.annotate( + available_stock=Greatest( + ExpressionWrapper( + F('total_stock') + - F('allocated_to_sales_orders') + - F('allocated_to_build_orders'), + output_field=models.DecimalField(), + ), + 0, + output_field=models.DecimalField(), + ) + ) + + # Add information about the quantity of parts currently on order + queryset = queryset.annotate( + on_order=part_filters.annotate_on_order_quantity(reference='part__') + ) + + # Add information about the quantity of parts currently in production + queryset = queryset.annotate( + building=part_filters.annotate_in_production_quantity(reference='part__') + ) + + # Annotate total 'allocated' stock quantity + queryset = queryset.annotate( + allocated=Coalesce( + SubquerySum('allocations__quantity'), + Decimal(0), + output_field=models.DecimalField(), + ) + ) + + return queryset + + order_detail = OptionalField( + serializer_class=TransferOrderSerializer, + serializer_kwargs={ + 'source': 'order', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + prefetch_fields=[ + 'order__created_by', + 'order__responsible', + 'order__project_code', + ], + ) + + part_detail = OptionalField( + serializer_class=PartBriefSerializer, + serializer_kwargs={ + 'source': 'part', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + prefetch_fields=['part__pricing_data'], + ) + + # Annotated fields + overdue = serializers.BooleanField(read_only=True, allow_null=True) + available_stock = serializers.FloatField(read_only=True) + available_variant_stock = serializers.FloatField(read_only=True) + on_order = serializers.FloatField(label=_('On Order'), read_only=True) + building = serializers.FloatField(label=_('In Production'), read_only=True) + + quantity = InvenTreeDecimalField() + + allocated = serializers.FloatField(read_only=True) + + transferred = InvenTreeDecimalField(read_only=True) + + +class TransferOrderAllocationItemSerializer(serializers.Serializer): + """A serializer for allocating a single stock-item against a TransferOrder line item.""" + + class Meta: + """Metaclass options.""" + + fields = ['line_item', 'stock_item', 'quantity'] + + line_item = serializers.PrimaryKeyRelatedField( + queryset=order.models.TransferOrderLineItem.objects.all(), + many=False, + allow_null=False, + required=True, + label=_('Stock Item'), + ) + + def validate_line_item(self, line_item): + """Custom validation for the 'line_item' field. + + - Ensure the line_item is associated with the particular TransferOrder + """ + order = self.context['order'] + + # Ensure that the line item points to the correct order + if line_item.order != order: + raise ValidationError(_('Line item is not associated with this order')) + + return line_item + + stock_item = serializers.PrimaryKeyRelatedField( + queryset=stock.models.StockItem.objects.all(), + many=False, + allow_null=False, + required=True, + label=_('Stock Item'), + ) + + quantity = serializers.DecimalField( + max_digits=15, decimal_places=5, min_value=Decimal(0), required=True + ) + + def validate_quantity(self, quantity): + """Custom validation for the 'quantity' field.""" + if quantity <= 0: + raise ValidationError(_('Quantity must be positive')) + + return quantity + + def validate(self, data): + """Custom validation for the serializer. + + - Ensure that the quantity is 1 for serialized stock + - Quantity cannot exceed the available amount + """ + data = super().validate(data) + + stock_item = data['stock_item'] + quantity = data['quantity'] + + if stock_item.serialized and quantity != 1: + raise ValidationError({ + 'quantity': _('Quantity must be 1 for serialized stock item') + }) + + q = normalize(stock_item.unallocated_quantity()) + + if quantity > q: + raise ValidationError({'quantity': _(f'Available quantity ({q}) exceeded')}) + + return data + + +class TransferOrderLineItemAllocationSerializer(serializers.Serializer): + """DRF serializer for allocation of stock items against a transfer order line item.""" + + class Meta: + """Metaclass options.""" + + fields = ['items'] + + items = TransferOrderAllocationItemSerializer(many=True) + + def validate(self, data): + """Serializer validation.""" + data = super().validate(data) + + # Extract TransferOrder from serializer context + # order = self.context['order'] + + items = data.get('items', []) + + if len(items) == 0: + raise ValidationError(_('Allocation items must be provided')) + + return data + + def save(self): + """Perform the allocation of items against this order.""" + data = self.validated_data + + items = data['items'] + + with transaction.atomic(): + for entry in items: + # Create a new TransferOrderAllocation + allocation = order.models.TransferOrderAllocation( + line=entry.get('line_item'), + item=entry.get('stock_item'), + quantity=entry.get('quantity'), + ) + + allocation.full_clean() + allocation.save() + + +class TransferOrderAllocationSerializer( + FilterableSerializerMixin, InvenTreeModelSerializer +): + """Serializer for the TransferOrderAllocation model. + + This includes some fields from the related model objects. + """ + + class Meta: + """Metaclass options.""" + + model = order.models.TransferOrderAllocation + fields = [ + 'pk', + 'item', + 'quantity', + # Annotated read-only fields + 'line', + 'part', + 'order', + 'serial', + 'location', + # Extra detail fields + 'item_detail', + 'part_detail', + 'order_detail', + 'location_detail', + ] + read_only_fields = ['line', ''] + + part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) + order = serializers.PrimaryKeyRelatedField( + source='line.order', many=False, read_only=True + ) + serial = serializers.CharField(source='get_serial', read_only=True, allow_null=True) + quantity = serializers.FloatField(read_only=False) + location = serializers.PrimaryKeyRelatedField( + source='item.location', many=False, read_only=True + ) + + # Extra detail fields + order_detail = OptionalField( + serializer_class=TransferOrderSerializer, + serializer_kwargs={ + 'source': 'line.order', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + ) + + part_detail = OptionalField( + serializer_class=PartBriefSerializer, + serializer_kwargs={ + 'source': 'item.part', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + ) + + item_detail = OptionalField( + serializer_class=stock.serializers.StockItemSerializer, + serializer_kwargs={ + 'source': 'item', + 'many': False, + 'read_only': True, + 'allow_null': True, + 'part_detail': False, + 'location_detail': False, + 'supplier_part_detail': False, + }, + ) + + location_detail = OptionalField( + serializer_class=stock.serializers.LocationBriefSerializer, + serializer_kwargs={ + 'source': 'item.location', + 'many': False, + 'read_only': True, + 'allow_null': True, + }, + ) + + +class TransferOrderSerialAllocationSerializer(serializers.Serializer): + """DRF serializer for allocation of serial numbers against a transfer order.""" + + class Meta: + """Metaclass options.""" + + fields = ['line_item', 'quantity', 'serial_numbers'] + + line_item = serializers.PrimaryKeyRelatedField( + queryset=order.models.TransferOrderLineItem.objects.all(), + many=False, + required=True, + allow_null=False, + label=_('Line Item'), + ) + + def validate_line_item(self, line_item): + """Ensure that the line_item is valid.""" + order = self.context['order'] + + # Ensure that the line item points to the correct order + if line_item.order != order: + raise ValidationError(_('Line item is not associated with this order')) + + return line_item + + quantity = serializers.IntegerField( + min_value=1, required=True, allow_null=False, label=_('Quantity') + ) + + serial_numbers = serializers.CharField( + label=_('Serial Numbers'), + help_text=_('Enter serial numbers to allocate'), + required=True, + allow_blank=False, + ) + + def validate(self, data): + """Validation for the serializer. + + - Ensure the serial_numbers and quantity fields match + - Check that all serial numbers exist + - Check that the serial numbers are not yet allocated + """ + data = super().validate(data) + + line_item = data['line_item'] + quantity = data['quantity'] + serial_numbers = data['serial_numbers'] + + part = line_item.part + + try: + data['serials'] = extract_serial_numbers( + serial_numbers, quantity, part.get_latest_serial_number(), part=part + ) + except DjangoValidationError as e: + raise ValidationError({'serial_numbers': e.messages}) + + serials_not_exist = set() + serials_unavailable = set() + stock_items_to_allocate = [] + + for serial in data['serials']: + serial = str(serial).strip() + + items = stock.models.StockItem.objects.filter( + part=part, serial=serial, quantity=1 + ) + + if not items.exists(): + serials_not_exist.add(str(serial)) + continue + + stock_item = items[0] + + if not stock_item.in_stock: + serials_unavailable.add(str(serial)) + continue + + if stock_item.unallocated_quantity() < 1: + serials_unavailable.add(str(serial)) + continue + + # At this point, the serial number is valid, and can be added to the list + stock_items_to_allocate.append(stock_item) + + if len(serials_not_exist) > 0: + error_msg = _('No match found for the following serial numbers') + error_msg += ': ' + error_msg += ','.join(sorted(serials_not_exist)) + + raise ValidationError({'serial_numbers': error_msg}) + + if len(serials_unavailable) > 0: + error_msg = _('The following serial numbers are unavailable') + error_msg += ': ' + error_msg += ','.join(sorted(serials_unavailable)) + + raise ValidationError({'serial_numbers': error_msg}) + + data['stock_items'] = stock_items_to_allocate + + return data + + def save(self): + """Allocate stock items against the transfer order.""" + data = self.validated_data + + line_item = data['line_item'] + stock_items = data['stock_items'] + + allocations = [] + + for stock_item in stock_items: + # Create a new TransferOrderAllocation + allocations.append( + order.models.TransferOrderAllocation( + line=line_item, item=stock_item, quantity=1 + ) + ) + + with transaction.atomic(): + order.models.TransferOrderAllocation.objects.bulk_create(allocations) diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py index 7ec5756b92..d8893a1fa3 100644 --- a/src/backend/InvenTree/order/status_codes.py +++ b/src/backend/InvenTree/order/status_codes.py @@ -115,3 +115,30 @@ class ReturnOrderLineStatus(StatusCode): # Item is rejected REJECT = 60, _('Reject'), ColorEnum.danger + + +class TransferOrderStatus(StatusCode): + """Defines a set of status codes for a TransferOrder.""" + + # Order status codes + PENDING = 10, _('Pending'), ColorEnum.secondary # Order is pending (not yet issued) + ISSUED = 20, _('Issued'), ColorEnum.primary # Order has been issued + ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Order is on hold + COMPLETE = 30, _('Complete'), ColorEnum.success # Order has been completed + CANCELLED = 40, _('Cancelled'), ColorEnum.danger # Order was cancelled + + +class TransferOrderStatusGroups: + """Groups for TransferOrderStatus codes.""" + + # Open orders + OPEN = [ + TransferOrderStatus.PENDING.value, + TransferOrderStatus.ON_HOLD.value, + TransferOrderStatus.ISSUED.value, + ] + + # Failed orders + FAILED = [TransferOrderStatus.CANCELLED.value] + + COMPLETE = [TransferOrderStatus.COMPLETE.value] diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index fae21e6e37..2da8cd6115 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -27,6 +27,8 @@ from order.status_codes import ( ReturnOrderStatus, SalesOrderStatus, SalesOrderStatusGroups, + TransferOrderStatus, + TransferOrderStatusGroups, ) from part.models import Part from stock.models import StockItem, StockLocation @@ -46,9 +48,10 @@ class OrderTest(InvenTreeAPITestCase): 'stock', 'order', 'sales_order', + 'transfer_order', ] - roles = ['purchase_order.change', 'sales_order.change'] + roles = ['purchase_order.change', 'sales_order.change', 'transfer_order.change'] def filter(self, filters, count): """Test API filters.""" @@ -2892,3 +2895,848 @@ class ReturnOrderLineItemTests(InvenTreeAPITestCase): line = models.ReturnOrderLineItem.objects.get(pk=1) self.assertEqual(float(line.price.amount), 15.75) + + +class TransferOrderTest(OrderTest): + """Tests for the TransferOrder API.""" + + LIST_URL = reverse('api-transfer-order-list') + + def test_transfer_order_list(self): + """Test the TransferOrder list API endpoint.""" + # all orders + self.filter({}, 5) + + # filter by outstanding + self.filter({'outstanding': True}, 3) + self.filter({'outstanding': False}, 2) + + # Filter by status + self.filter({'status': TransferOrderStatus.PENDING.value}, 1) + self.filter({'status': SalesOrderStatus.COMPLETE.value}, 1) + self.filter({'status': 99}, 0) # Invalid + + # Filter by "reference" + self.filter({'reference': 'TO-123'}, 1) + self.filter({'reference': 'TO-999'}, 0) + + # Filter by "assigned_to_me" + self.filter({'assigned_to_me': 1}, 0) + self.filter({'assigned_to_me': 0}, 5) + + def test_overdue(self): + """Test "overdue" status.""" + self.filter({'overdue': True}, 0) + self.filter({'overdue': False}, 5) + + # pick two orders that are still open (not cancelled or complete) + for pk in [1, 4]: + order = models.TransferOrder.objects.get(pk=pk) + order.target_date = datetime.now().date() - timedelta(days=10) + order.save() + + self.filter({'overdue': True}, 2) + self.filter({'overdue': False}, 3) + + def test_transfer_order_detail(self): + """Test the TransferOrder detail endpoint.""" + url = '/api/order/transfer-order/1/' + + response = self.get(url) + + data = response.data + + self.assertEqual(data['pk'], 1) + + def test_transfer_order_attachments(self): + """Test the list endpoint for the Transfer Order Attachments.""" + url = reverse('api-attachment-list') + + # Filter by 'transferorder' + self.get( + url, data={'model_type': 'transferorder', 'model_id': 1}, expected_code=200 + ) + + def test_transfer_order_operations(self): + """Test that we can create / edit and delete a TransferOrder via the API.""" + n = models.TransferOrder.objects.count() + + url = reverse('api-transfer-order-list') + + # Initially we do not have "add" permission for the TransferOrder model, + # so this POST request should return 403 (denied) + response = self.post( + url, + {'reference': 'TO-43245', 'description': 'Transfer order'}, + expected_code=403, + ) + + self.assignRole('transfer_order.add') + + # Now we should be able to create a TransferOrder via the API + response = self.post( + url, + {'reference': 'TO-12345', 'description': 'Transfer order'}, + expected_code=201, + ) + + # Check that the new order has been created + self.assertEqual(models.TransferOrder.objects.count(), n + 1) + + # Grab the PK for the newly created TransferOrder + pk = response.data['pk'] + + # Basic checks against the newly created TransferOrder + so = models.TransferOrder.objects.get(pk=pk) + self.assertEqual(so.reference, 'TO-12345') + self.assertEqual(so.created_by.username, 'testuser') + + # Try to create a TO with identical reference (should fail) + response = self.post( + url, + { + 'customer': 4, + 'reference': 'TO-12345', + 'description': 'Another transfer order', + }, + expected_code=400, + ) + + url = reverse('api-transfer-order-detail', kwargs={'pk': pk}) + + # Extract detail info for the TransferOrder + response = self.get(url) + self.assertEqual(response.data['reference'], 'TO-12345') + + # Try to alter (edit) the TransferOrder + # Initially try with an invalid reference field value + response = self.patch(url, {'reference': 'TO-12345-a'}, expected_code=400) + + response = self.patch(url, {'reference': 'TO-12346'}, expected_code=200) + + # Reference should have changed + self.assertEqual(response.data['reference'], 'TO-12346') + + # Now, let's try to delete this TransferOrder + # Initially, we do not have the required permission + response = self.delete(url, expected_code=403) + + self.assignRole('transfer_order.delete') + + response = self.delete(url, expected_code=204) + + # Check that the number of transfer orders has decreased + self.assertEqual(models.TransferOrder.objects.count(), n) + + # And the resource should no longer be available + response = self.get(url, expected_code=404) + + def test_transfer_order_create(self): + """Test that we can create a new TransferOrder via the API.""" + self.assignRole('transfer_order.add') + + url = reverse('api-transfer-order-list') + + # Will fail due to invalid reference field + response = self.post( + url, + {'reference': '1234566778', 'description': 'A test transfer order'}, + expected_code=400, + ) + + self.assertIn( + 'Reference must match required pattern', str(response.data['reference']) + ) + + self.post( + url, + {'reference': 'TO-12345', 'description': 'A better test transfer order'}, + expected_code=201, + ) + + def test_transfer_order_cancel(self): + """Test API endpoint for cancelling a TransferOrder.""" + to = models.TransferOrder.objects.get(pk=1) + + self.assertEqual(to.status, TransferOrderStatus.PENDING) + + url = reverse('api-transfer-order-cancel', kwargs={'pk': to.pk}) + + # Try to cancel, without permission + self.post(url, {}, expected_code=403) + + self.assignRole('transfer_order.add') + + self.post(url, {}, expected_code=201) + + to.refresh_from_db() + + self.assertEqual(to.status, TransferOrderStatus.CANCELLED) + + def test_transfer_order_hold(self): + """Test API endpoint for holdling a TransferOrder.""" + to = models.TransferOrder.objects.get(pk=1) + + self.assertEqual(to.status, TransferOrderStatus.PENDING) + + url = reverse('api-transfer-order-hold', kwargs={'pk': to.pk}) + + # Try to hold, without permission + self.post(url, {}, expected_code=403) + + self.assignRole('transfer_order.add') + + self.post(url, {}, expected_code=201) + + to.refresh_from_db() + + self.assertEqual(to.status, TransferOrderStatus.ON_HOLD) + + def test_transfer_order_calendar(self): + """Test the calendar export endpoint.""" + # Create required transfer orders + self.assignRole('transfer_order.add') + + for i in range(1, 9): + self.post( + reverse('api-transfer-order-list'), + { + 'reference': f'TO-1100000{i}', + 'description': f'Calendar SO {i}', + 'target_date': f'2024-12-{i:02d}', + }, + expected_code=201, + ) + + # Cancel a few orders - these will not show in incomplete view below + for to in models.TransferOrder.objects.filter(target_date__isnull=False): + if to.reference in [ + 'TO-11000006', + 'TO-11000007', + 'TO-11000008', + 'TO-11000009', + ]: + self.post( + reverse('api-transfer-order-cancel', kwargs={'pk': to.pk}), + expected_code=201, + ) + + url = reverse('api-po-so-calendar', kwargs={'ordertype': 'transfer-order'}) + + # Test without completed orders + response = self.get(url, expected_code=200, format=None) + + number_orders = len( + models.TransferOrder.objects.filter(target_date__isnull=False).filter( + status__lt=TransferOrderStatus.COMPLETE.value + ) + ) + + # Transform content to a Calendar object + calendar = Calendar.from_ical(response.content) + n_events = 0 + # Count number of events in calendar + for component in calendar.walk(): + if component.name == 'VEVENT': + n_events += 1 + + self.assertGreaterEqual(n_events, 1) + self.assertEqual(number_orders, n_events) + + # Test with completed orders + response = self.get( + url, data={'include_completed': 'True'}, expected_code=200, format=None + ) + + number_orders_incl_complete = len( + models.TransferOrder.objects.filter(target_date__isnull=False) + ) + self.assertGreater(number_orders_incl_complete, number_orders) + + # Transform content to a Calendar object + calendar = Calendar.from_ical(response.content) + n_events = 0 + # Count number of events in calendar + for component in calendar.walk(): + if component.name == 'VEVENT': + n_events += 1 + + self.assertGreaterEqual(n_events, 1) + self.assertEqual(number_orders_incl_complete, n_events) + + def test_export(self): + """Test we can export the TransferOrder list.""" + n = models.TransferOrder.objects.count() + + # Check there are some sales orders + self.assertGreater(n, 0) + + # Download file, check we get a 200 response + for fmt in ['csv', 'xlsx', 'tsv']: + self.export_data( + reverse('api-transfer-order-list'), + export_format=fmt, + decode=fmt == 'csv', + expected_code=200, + expected_fn=r'InvenTree_TransferOrder_.+', + ) + + def test_transfer_order_complete(self): + """Tests for marking a TransferOrder as complete.""" + self.assignRole('transfer_order.add') + destination = StockLocation.objects.first() + # Let's create a TransferOrder + to = models.TransferOrder.objects.create( + reference='TO-12345', description='Test TO' + ) + + self.assertEqual(to.status, TransferOrderStatus.PENDING.value) + + # Create a line item + part = Part.objects.exclude(virtual=True).first() + + line = models.TransferOrderLineItem.objects.create( + order=to, part=part, quantity=10 + ) + + # issue the order + url = reverse('api-transfer-order-issue', kwargs={'pk': to.pk}) + self.post(url, {}, expected_code=201) + to.refresh_from_db() + self.assertEqual(to.status, TransferOrderStatus.ISSUED.value) + + # Allocate some stock + item = StockItem.objects.create( + part=part, quantity=100, location=None, batch='transfer-order-test' + ) + short_allocation = models.TransferOrderAllocation.objects.create( + quantity=5, line=line, item=item + ) + + # attempt to complete the order, but fail because there are incomplete allocations + url = reverse('api-transfer-order-complete', kwargs={'pk': to.pk}) + response = self.post(url, {}, expected_code=400) + self.assertIn('has incomplete allocations', str(response.data)) + # allocate more stock + short_allocation.delete() + models.TransferOrderAllocation.objects.create(quantity=10, line=line, item=item) + + # attempt to complete the order, but fail because there is no destination yet + url = reverse('api-transfer-order-complete', kwargs={'pk': to.pk}) + response = self.post(url, {}, expected_code=400) + self.assertIn('until a destination location is set', str(response.data)) + # add destination + to.destination = destination + to.save() + + # Ok, now we should be able to "complete" the transfer via the API + url = reverse('api-transfer-order-complete', kwargs={'pk': to.pk}) + self.post(url, {}, expected_code=201) + + to.refresh_from_db() + self.assertEqual(to.status, TransferOrderStatus.COMPLETE.value) + self.assertIsNotNone(to.complete_date) + + # Now, let's try *again* (it should fail as the order is already complete) + response = self.post(url, {}, expected_code=400) + self.assertIn('Order is already complete', str(response.data)) + + # Now, we make sure the affected stock was transferred to the correct location + StockItem.objects.get( + part=part, quantity=10, batch='transfer-order-test', location=destination + ) + + def test_transfer_order_consume(self): + """Tests for marking a TransferOrder consume the stock it 'transfers'.""" + self.assignRole('transfer_order.add') + destination = StockLocation.objects.first() + # Let's create a TransferOrder + to = models.TransferOrder.objects.create( + reference='TO-12345', + description='Test TO', + consume=True, + destination=destination, + ) + + self.assertEqual(to.status, TransferOrderStatus.PENDING.value) + + # Create a line item + part = Part.objects.exclude(virtual=True).first() + + line = models.TransferOrderLineItem.objects.create( + order=to, part=part, quantity=10 + ) + + # issue the order + url = reverse('api-transfer-order-issue', kwargs={'pk': to.pk}) + self.post(url, {}, expected_code=201) + to.refresh_from_db() + self.assertEqual(to.status, TransferOrderStatus.ISSUED.value) + + # Allocate some stock + item = StockItem.objects.create( + part=part, quantity=100, location=None, batch='transfer-order-test' + ) + models.TransferOrderAllocation.objects.create(quantity=10, line=line, item=item) + + # Ok, now we should be able to "complete" the transfer via the API + url = reverse('api-transfer-order-complete', kwargs={'pk': to.pk}) + self.post(url, {}, expected_code=201) + + to.refresh_from_db() + self.assertEqual(to.status, TransferOrderStatus.COMPLETE.value) + self.assertIsNotNone(to.complete_date) + + # Now, we make sure the affected stock was 'consumed', reducing available quantity + item.refresh_from_db() + self.assertEqual(item.quantity, 90) + + # and that it wasn't transferred to the destination + with self.assertRaises(StockItem.DoesNotExist): + StockItem.objects.get( + part=part, + quantity=10, + batch='transfer-order-test', + location=destination, + ) + + def test_output_options(self): + """Test the output options for the TransferOrder detail endpoint.""" + self.run_output_test( + reverse('api-transfer-order-detail', kwargs={'pk': 1}), + ['take_from_detail', 'destination_detail'], + ) + + +class TransferOrderLineItemTest(OrderTest): + """Tests for the TransferOrderLineItem API.""" + + LIST_URL = reverse('api-transfer-order-line-list') + + # adjust counts in asserts based on those created in setUpTestData + # plus those in fixtures + NUM_LINE_ITEMS_IN_FIXTURES = 2 + + @classmethod + def setUpTestData(cls): + """Init routine for this unit test class.""" + super().setUpTestData() + + # List of 'transferrable' parts + parts = Part.objects.exclude(virtual=True) + + lines = [] + + # Create a bunch of TransferOrderLineItems for each order + for idx, to in enumerate(models.TransferOrder.objects.all()): + for part in parts: + lines.append( + models.TransferOrderLineItem( + order=to, + part=part, + quantity=(idx + 1) * 5, + reference=f'Order {to.reference} - line {idx}', + ) + ) + + # Bulk create + models.TransferOrderLineItem.objects.bulk_create(lines) + + cls.url = reverse('api-transfer-order-line-list') + + def test_transfer_order_line_list(self): + """Test list endpoint.""" + response = self.get(self.url, {}, expected_code=200) + + n = models.TransferOrderLineItem.objects.count() + + # We should have received *all* lines + self.assertEqual(len(response.data), n) + + # List *all* lines, but paginate + response = self.get(self.url, {'limit': 5}, expected_code=200) + + self.assertEqual(response.data['count'], n) + self.assertEqual(len(response.data['results']), 5) + + n_orders = models.TransferOrder.objects.count() + n_parts = Part.objects.exclude(virtual=True).count() + + # List by part + # fixures add line items, avoid those here with [:3] for predictable counts + for part in Part.objects.exclude(virtual=True)[:3]: + response = self.get(self.url, {'part': part.pk, 'limit': 10}) + self.assertEqual(response.data['count'], n_orders) + + # List by order + # fixures add line items, avoid those here with [:3] for predictable counts + for order in models.TransferOrder.objects.all()[:3]: + response = self.get(self.url, {'order': order.pk, 'limit': 10}) + # count of line items equal to number of parts because + # we created a line item per part on each order in setUpTestData + self.assertEqual(response.data['count'], n_parts) + + # Filter by 'completed' status + self.filter({'completed': 1}, 1) + self.filter({'completed': 0}, n - 1) + + # Filter by 'allocated' status + self.filter({'allocated': 'true'}, 2) + self.filter({'allocated': 'false'}, n - 2) + + def test_transfer_order_line_allocated_filters(self): + """Test filtering by allocation status for a TransferOrderLineItem.""" + self.assignRole('transfer_order.add') + + destination = StockLocation.objects.first() + assert destination + + response = self.post( + reverse('api-transfer-order-list'), + { + 'reference': 'TO-12345', + 'description': 'Test Transfer Order', + 'destination': destination.pk, + }, + ) + + order_id = response.data['pk'] + order = models.TransferOrder.objects.get(pk=order_id) + + transfer_order_line_url = reverse('api-transfer-order-line-list') + + # Initially, there should be no line items against this order + response = self.get(transfer_order_line_url, {'order': order_id}) + + self.assertEqual(len(response.data), 0) + + parts = [25, 50, 100] + + # Let's create some new line items + for part_id in parts: + self.post( + transfer_order_line_url, + {'order': order_id, 'part': part_id, 'quantity': 10}, + ) + + # Should be three items now + response = self.get(transfer_order_line_url, {'order': order_id}) + + self.assertEqual(len(response.data), 3) + + for item in response.data: + # Check that the line item has been created + self.assertEqual(item['order'], order_id) + + # Check that the line quantities are correct + self.assertEqual(item['quantity'], 10) + self.assertEqual(item['allocated'], 0) + self.assertEqual(item['transferred'], 0) + + # Initial API filters should return no results + self.filter({'order': order_id, 'allocated': 1}, 0) + self.filter({'order': order_id, 'completed': 1}, 0) + + # issue the order + order_issue_url = reverse('api-transfer-order-issue', kwargs={'pk': order.pk}) + self.post(order_issue_url, {}, expected_code=201) + + # Next, allocate stock against 2 line items + for item in parts[:2]: + p = Part.objects.get(pk=item) + s = StockItem.objects.create(part=p, quantity=100) + l = models.TransferOrderLineItem.objects.filter(order=order, part=p).first() + assert l + + # Allocate against the API + self.post( + reverse('api-transfer-order-allocate', kwargs={'pk': order.pk}), + {'items': [{'line_item': l.pk, 'stock_item': s.pk, 'quantity': 10}]}, + ) + + # Filter by 'fully allocated' status + self.filter({'order': order_id, 'allocated': 1}, 2) + self.filter({'order': order_id, 'allocated': 0}, 1) + + self.filter({'order': order_id, 'completed': 1}, 0) + self.filter({'order': order_id, 'completed': 0}, 3) + + # Finally, attempt to transfer this line item + # we have incomplete allocations, so must specify arg + self.post( + reverse('api-transfer-order-complete', kwargs={'pk': order.pk}), + {'accept_incomplete_allocation': 'true'}, + ) + + # Filter by 'completed' status + self.filter({'order': order_id, 'completed': 1}, 2) + self.filter({'order': order_id, 'completed': 0}, 1) + + def test_output_options(self): + """Test the various output options for the TransferOrderLineItem detail endpoint.""" + self.run_output_test( + reverse('api-transfer-order-line-detail', kwargs={'pk': 1}), + ['part_detail', 'order_detail'], + ) + + +class TransferOrderDownloadTest(OrderTest): + """Unit tests for downloading TransferOrder data via the API endpoint.""" + + def test_download_fail(self): + """Test that downloading without the 'export' option fails.""" + url = reverse('api-transfer-order-list') + + response = self.export_data(url, export_plugin='no-plugin', expected_code=400) + self.assertIn('is not a valid choice', str(response['export_plugin'])) + + def test_download_xlsx(self): + """Test xlsx file download.""" + url = reverse('api-transfer-order-list') + + # Download .xls file + with self.export_data( + url, export_format='xlsx', expected_code=200, decode=False + ) as file: + self.assertIsInstance(file, io.BytesIO) + + def test_download_csv(self): + """Test that the list of transfer orders can be downloaded as a .csv file.""" + url = reverse('api-transfer-order-list') + + required_cols = [ + 'Line Items', + 'Completed Lines', + 'ID', + 'Reference', + 'Order Status', + 'Description', + 'Project Code', + 'Responsible', + 'Consume Stock', + ] + + excluded_cols = ['metadata'] + + # Download .xls file + with self.export_data(url, export_format='csv') as file: + data = self.process_csv( + file, + required_cols=required_cols, + excluded_cols=excluded_cols, + required_rows=models.TransferOrder.objects.count(), + ) + + for line in data: + order = models.TransferOrder.objects.get(pk=line['ID']) + + self.assertEqual(line['Description'], order.description) + self.assertEqual(line['Order Status'], str(order.status)) + + # Download only outstanding transfer orders + with self.export_data(url, {'outstanding': True}, export_format='tsv') as file: + self.process_csv( + file, + required_cols=required_cols, + excluded_cols=excluded_cols, + required_rows=models.TransferOrder.objects.filter( + status__in=TransferOrderStatusGroups.OPEN + ).count(), + delimiter='\t', + ) + + +class TransferOrderAllocateTest(OrderTest): + """Unit tests for allocating stock items against a TransferOrder.""" + + @classmethod + def setUpTestData(cls): + """Init routine for this unit test class.""" + super().setUpTestData() + + def setUp(self): + """Init routines for this unit testing class.""" + super().setUp() + + self.assignRole('transfer_order.add') + + self.url = reverse('api-transfer-order-allocate', kwargs={'pk': 1}) + self.url_serialized = reverse( + 'api-transfer-order-allocate-serials', kwargs={'pk': 1} + ) + + self.order = models.TransferOrder.objects.get(pk=1) + + # Create some line items for this transfer order + parts = Part.objects.exclude(virtual=True) + + for part in parts: + # Create a new line item + models.TransferOrderLineItem.objects.create( + order=self.order, part=part, quantity=5 + ) + + # Ensure we have stock! + StockItem.objects.create(part=part, quantity=100) + + # Create a new shipment against this TransferOrder + # self.shipment = models.TransferOrderShipment.objects.create(order=self.order) + + def test_invalid(self): + """Test POST with invalid data.""" + # No data + response = self.post(self.url, {}, expected_code=400) + + self.assertIn('This field is required', str(response.data['items'])) + + # Test with a single line items + line = self.order.lines.first() + part = line.part + + # Valid stock_item, but quantity is invalid + data = { + 'items': [ + { + 'line_item': line.pk, + 'stock_item': part.stock_items.last().pk, + 'quantity': 0, + } + ] + } + + response = self.post(self.url, data, expected_code=400) + + self.assertIn('Quantity must be positive', str(response.data['items'])) + + # Valid stock item, too much quantity + data['items'][0]['quantity'] = 250 + + response = self.post(self.url, data, expected_code=400) + + self.assertIn('Available quantity (100) exceeded', str(response.data['items'])) + + def test_allocate(self): + """Test that the allocation endpoint acts as expected, when provided with valid data!""" + # First, check that there are no line items allocated against this TransferOrder + self.assertEqual(self.order.stock_allocations.count(), 0) + + data = {'items': []} + + for line in self.order.lines.all(): + for stock_item in line.part.stock_items.filter(quantity__gt=5): + # Find a non-serialized stock item to allocate + if not stock_item.serialized: + break + + # Fully-allocate each line + data['items'].append({ + 'line_item': line.pk, + 'stock_item': stock_item.pk, + 'quantity': 5, + }) + + self.post(self.url, data, expected_code=201) + + # There should have been 1 stock item allocated against each line item + n_lines = self.order.lines.count() + + self.assertEqual(self.order.stock_allocations.count(), n_lines) + + for line in self.order.lines.all(): + self.assertEqual(line.allocations.count(), 1) + + def test_allocate_serials(self): + """Test that the allocation endpoint acts as expected, when provided with serials.""" + self.assertEqual(self.order.stock_allocations.count(), 0) + + trackable_lines = self.order.lines.filter(part__trackable=True) + for line in trackable_lines: + stock_item = ( + line.part.stock_items + .exclude(serial=None) + .filter(StockItem.IN_STOCK_FILTER) + .first() + ) + + # Allocate this serialized item to the transfer order + data = { + 'line_item': line.pk, + 'quantity': 1, + 'serial_numbers': stock_item.serial, + } + + self.post(self.url_serialized, data, expected_code=201) + + # There should have been 1 stock item allocated against each line item + n_lines = trackable_lines.count() + self.assertEqual(self.order.stock_allocations.count(), n_lines) + + for line in trackable_lines.all(): + self.assertEqual(line.allocations.count(), 1) + + def test_allocate_variant(self): + """Test that the allocation endpoint acts as expected, when provided with variant.""" + # First, check that there are no line items allocated against this TransferOrder + self.assertEqual(self.order.stock_allocations.count(), 0) + + data = {'items': []} + + def check_template(line_item): + return line_item.part.is_template + + for line in filter(check_template, self.order.lines.all()): + stock_item: Optional[StockItem] = None + + stock_item = None + + # Allocate a matching variant + parts: list[Part] = ( + Part.objects + .exclude(virtual=True) + .exclude(is_template=True) + .filter(variant_of=line.part.pk) + ) + # if we don't have a matching variant, continue + if not parts.exists(): + continue + for part in parts: + # ensure we have the quantity necessary to allocate + if not part.stock_items.filter(quantity__gt=5).exists(): + continue + + stock_item = part.stock_items.last() + + for item in part.stock_items.filter(quantity__gt=5): + if item.serialized: + continue + + stock_item = item + break + + if stock_item is not None: + break + + if stock_item is None: + raise self.fail('No stock item found for part') # pragma: no cover + + # Fully-allocate each line + data['items'].append({ + 'line_item': line.pk, + 'stock_item': stock_item.pk, + 'quantity': 5, + }) + + self.post(self.url, data, expected_code=201) + + # At least one item should be allocated, and all should be variants + self.assertGreater(self.order.stock_allocations.count(), 0) + for allocation in self.order.stock_allocations.all(): + self.assertNotEqual(allocation.item.part.pk, allocation.line.part.pk) + + def test_output_options(self): + """Test the various output options for the SalesOrderAllocation detail endpoint.""" + self.run_output_test( + reverse('api-transfer-order-allocation-list'), + ['part_detail', 'item_detail', 'order_detail', 'location_detail'], + assert_subset=True, + ) diff --git a/src/backend/InvenTree/order/validators.py b/src/backend/InvenTree/order/validators.py index a4873679bd..ce2004de76 100644 --- a/src/backend/InvenTree/order/validators.py +++ b/src/backend/InvenTree/order/validators.py @@ -22,6 +22,13 @@ def generate_next_return_order_reference(): return ReturnOrder.generate_reference() +def generate_next_transfer_order_reference(): + """Generate the next available TransferOrder reference.""" + from order.models import TransferOrder + + return TransferOrder.generate_reference() + + def validate_sales_order_reference_pattern(pattern): """Validate the SalesOrder reference 'pattern' setting.""" from order.models import SalesOrder @@ -62,3 +69,17 @@ def validate_return_order_reference(value): from order.models import ReturnOrder ReturnOrder.validate_reference_field(value) + + +def validate_transfer_order_reference_pattern(pattern): + """Validate the TransferOrder reference 'pattern' setting.""" + from order.models import TransferOrder + + TransferOrder.validate_reference_pattern(pattern) + + +def validate_transfer_order_reference(value): + """Validate that the ReturnOrder reference field matches the required pattern.""" + from order.models import TransferOrder + + TransferOrder.validate_reference_field(value) diff --git a/src/backend/InvenTree/part/filters.py b/src/backend/InvenTree/part/filters.py index d6610260d5..2c832c2e9e 100644 --- a/src/backend/InvenTree/part/filters.py +++ b/src/backend/InvenTree/part/filters.py @@ -37,7 +37,11 @@ from sql_util.utils import SubquerySum import part.models import stock.models from build.status_codes import BuildStatusGroups -from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups +from order.status_codes import ( + PurchaseOrderStatusGroups, + SalesOrderStatusGroups, + TransferOrderStatusGroups, +) def annotate_in_production_quantity(reference: str = '') -> QuerySet: @@ -274,6 +278,40 @@ def annotate_sales_order_allocations(reference: str = '', location=None) -> Quer ) +def annotate_transfer_order_allocations(reference: str = '', location=None) -> QuerySet: + """Annotate the total quantity of each part allocated to transfer orders. + + - This function calculates the total part quantity allocated to open transfer orders" + - Finds all transfer order allocations for each part (using the provided filter) + - Aggregates the 'allocated quantity' for each relevant transfer order allocation item + + Arguments: + reference: The relationship reference of the part from the current model + location: If provided, only allocated stock items from this location are considered + """ + # Order filter only returns open orders + order_filter = Q(line__order__status__in=TransferOrderStatusGroups.OPEN) + + if location is not None: + # Filter by location (including any child locations) + + order_filter &= Q( + item__location__tree_id=location.tree_id, + item__location__lft__gte=location.lft, + item__location__rght__lte=location.rght, + item__location__level__gte=location.level, + ) + + return Coalesce( + SubquerySum( + f'{reference}stock_items__transfer_order_allocations__quantity', + filter=order_filter, + ), + Decimal(0), + output_field=models.DecimalField(), + ) + + def variant_stock_query(reference: str = '', filter: Optional[Q] = None) -> QuerySet: """Create a queryset to retrieve all stock items for variant parts under the specified part. diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index a38870df5e..0548ffbef3 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -60,6 +60,7 @@ from order.status_codes import ( PurchaseOrderStatus, PurchaseOrderStatusGroups, SalesOrderStatusGroups, + TransferOrderStatusGroups, ) from stock import models as StockModels @@ -1768,8 +1769,50 @@ class Part( return query['total'] + def transfer_order_allocations(self, **kwargs): + """Return all transfer-order-allocation objects which allocate this part to a TransferOrder.""" + include_variants = kwargs.get('include_variants', True) + + queryset = OrderModels.TransferOrderAllocation.objects.all() + + if include_variants: + # Include allocations for all variants + variants = self.get_descendants(include_self=True) + queryset = queryset.filter(item__part__in=variants) + else: + # Only look at this part + queryset = queryset.filter(item__part=self) + + # Default behaviour is to only return *pending* allocations + pending = kwargs.get('pending', True) + + if pending is True: + # Look only for 'open' orders + queryset = queryset.filter( + line__order__status__in=TransferOrderStatusGroups.OPEN + ) + elif pending is False: + # Look only for 'closed' orders + queryset = queryset.exclude( + line__order__status__in=TransferOrderStatusGroups.OPEN + ) + + return queryset + + def transfer_order_allocation_count(self, **kwargs): + """Return the total quantity of this part allocated to transfer orders.""" + query = self.transfer_order_allocations(**kwargs).aggregate( + total=Coalesce( + Sum('quantity', output_field=models.DecimalField()), + 0, + output_field=models.DecimalField(), + ) + ) + + return query['total'] + def allocation_count(self, **kwargs): - """Return the total quantity of stock allocated for this part, against both build orders and sales orders.""" + """Return the total quantity of stock allocated for this part, against build orders, sales orders, and transfer orders.""" if self.id is None: # If this instance has not been saved, foreign-key lookups will fail return 0 @@ -1777,6 +1820,8 @@ class Part( return sum([ self.build_order_allocation_count(**kwargs), self.sales_order_allocation_count(**kwargs), + # For now, stock allocated to a transfer order will not impact its availability + # self.transfer_order_allocation_count(**kwargs), ]) def stock_entries( diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index f67f802a26..ccbce76492 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -735,6 +735,8 @@ class PartSerializer( ordering=part_filters.annotate_on_order_quantity(), in_stock=part_filters.annotate_total_stock(), allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(), + # NOTE: for now, decided that allocations to Transfer Orders don't reduce available stock + # allocated_to_transfer_orders=part_filters.annotate_transfer_order_allocations(), allocated_to_build_orders=part_filters.annotate_build_order_allocations(), ) @@ -759,6 +761,8 @@ class PartSerializer( ExpressionWrapper( F('total_in_stock') - F('allocated_to_sales_orders') + # NOTE: for now, decided that allocations to Transfer Orders don't reduce available stock + # - F('allocated_to_transfer_orders'), - F('allocated_to_build_orders'), output_field=models.DecimalField(), ), @@ -768,6 +772,8 @@ class PartSerializer( ) # Annotate with the total 'required for builds' quantity + # NOTE: for now, we don't consider transfer orders for required quantities + # and they are assumed to operate on stock that already exists. queryset = queryset.annotate( required_for_build_orders=part_filters.annotate_build_order_requirements(), required_for_sales_orders=part_filters.annotate_sales_order_requirements(), @@ -1907,6 +1913,7 @@ class BomItemSerializer( 'sub_part__stock_items', 'sub_part__stock_items__allocations', 'sub_part__stock_items__sales_order_allocations', + 'sub_part__stock_items__transfer_order_allocations', ) # Annotate with the 'total pricing' information based on unit pricing and quantity diff --git a/src/backend/InvenTree/report/apps.py b/src/backend/InvenTree/report/apps.py index 40de211e32..e72ea19145 100644 --- a/src/backend/InvenTree/report/apps.py +++ b/src/backend/InvenTree/report/apps.py @@ -229,6 +229,13 @@ class ReportConfig(AppConfig): 'model_type': 'returnorder', 'filename_pattern': 'ReturnOrder-{{ reference }}.pdf', }, + { + 'file': 'inventree_transfer_order_report.html', + 'name': 'InvenTree Transfer Order', + 'description': 'Sample transfer order report', + 'model_type': 'transferorder', + 'filename_pattern': 'TransferOrder-{{ reference }}.pdf', + }, { 'file': 'inventree_test_report.html', 'name': 'InvenTree Test Report', diff --git a/src/backend/InvenTree/report/templates/report/inventree_transfer_order_report.html b/src/backend/InvenTree/report/templates/report/inventree_transfer_order_report.html new file mode 100644 index 0000000000..1b88d0275f --- /dev/null +++ b/src/backend/InvenTree/report/templates/report/inventree_transfer_order_report.html @@ -0,0 +1,52 @@ +{% extends "report/inventree_order_report_base.html" %} + +{% load i18n %} +{% load report %} +{% load barcode %} +{% load inventree_extras %} +{% load markdownify %} + +{% block header_content %} + +

+

{% trans "Transfer Order" %} {{ prefix }}{{ reference }}

+ {{ order.take_from.pathstring }} → {{ order.destination.pathstring }} +
+ +{% endblock header_content %} + +{% block page_content %} + +

{% trans "Line Items" %}

+ + + + + + + + + + + + + {% for line in lines.all %} + + + + + + + + {% endfor %} + +
{% trans "Part" %}{% trans "Reference" %}{% trans "Quantity" %}{% trans "Transferred" %}{% trans "Note" %}
+
+ {% trans "Part image" %} +
+
+ {{ line.part.full_name }} +
+
{{ line.reference }}{% decimal line.quantity %}{% decimal line.transferred %}{{ line.notes }}
+ +{% endblock page_content %} diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 981bf6696d..b93b8edf2c 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -53,11 +53,12 @@ from InvenTree.mixins import ( RetrieveUpdateDestroyAPI, SerializerContextMixin, ) -from order.models import PurchaseOrder, ReturnOrder, SalesOrder +from order.models import PurchaseOrder, ReturnOrder, SalesOrder, TransferOrder from order.serializers import ( PurchaseOrderSerializer, ReturnOrderSerializer, SalesOrderSerializer, + TransferOrderSerializer, ) from part.models import BomItem, Part, PartCategory from part.serializers import PartBriefSerializer @@ -672,13 +673,17 @@ class StockFilter(FilterSet): def filter_allocated(self, queryset, name, value): """Filter by whether or not the stock item is 'allocated'.""" if str2bool(value): - # Filter StockItem with either build allocations or sales order allocations + # Filter StockItem with either build allocations or transfer order allocations or sales order allocations return queryset.filter( - Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False) + Q(sales_order_allocations__isnull=False) + | Q(transfer_order_allocations__isnull=False) + | Q(allocations__isnull=False) ).distinct() - # Filter StockItem without build allocations or sales order allocations + # Filter StockItem without build allocations or transfer order allocations or sales order allocations return queryset.filter( - Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True) + Q(sales_order_allocations__isnull=True) + & Q(transfer_order_allocations__isnull=True) + & Q(allocations__isnull=True) ) expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired') @@ -1584,6 +1589,7 @@ class StockTrackingList( 'purchaseorder': (PurchaseOrder, PurchaseOrderSerializer), 'salesorder': (SalesOrder, SalesOrderSerializer), 'returnorder': (ReturnOrder, ReturnOrderSerializer), + 'transferorder': (TransferOrder, TransferOrderSerializer), 'buildorder': (Build, BuildSerializer), 'item': (StockItem, StockSerializers.StockItemSerializer), 'stockitem': (StockItem, StockSerializers.StockItemSerializer), diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 51daa040ce..040daee86e 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -47,6 +47,7 @@ from InvenTree.status_codes import ( StockStatus, StockStatusGroups, ) +from order.status_codes import TransferOrderStatusGroups from part import models as PartModels from plugin.events import trigger_event from stock.events import StockEvents @@ -1535,7 +1536,7 @@ class StockItem( item.save(add_note=False) def is_allocated(self): - """Return True if this StockItem is allocated to a SalesOrder or a Build.""" + """Return True if this StockItem is allocated to a SalesOrder, TransferOrder, or a Build.""" return self.allocation_count() > 0 def build_allocation_count(self, **kwargs): @@ -1595,12 +1596,48 @@ class StockItem( return total + def get_transfer_order_allocations(self, active=True, **kwargs): + """Return a queryset for TransferOrderAllocations against this StockItem, with optional filters. + + Arguments: + active: Filter by 'active' status of the allocation + """ + query = self.transfer_order_allocations.all() + + if filter_allocations := kwargs.get('filter_allocations'): + query = query.filter(**filter_allocations) + + if exclude_allocations := kwargs.get('exclude_allocations'): + query = query.exclude(**exclude_allocations) + + if active is True: + query = query.filter(line__order__status__in=TransferOrderStatusGroups.OPEN) + elif active is False: + query = query.exclude( + line__order__status__in=TransferOrderStatusGroups.OPEN + ) + + return query + + def transfer_order_allocation_count(self, active=True, **kwargs): + """Return the total quantity allocated to TransferOrders.""" + query = self.get_transfer_order_allocations(active=active, **kwargs) + query = query.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) + + total = query['q'] + + if total is None: + total = Decimal(0) + + return total + def allocation_count(self): """Return the total quantity allocated to builds or orders.""" bo = self.build_allocation_count() so = self.sales_order_allocation_count() + to = self.transfer_order_allocation_count() - return bo + so + return bo + so + to def unallocated_quantity(self): """Return the quantity of this StockItem which is *not* allocated.""" @@ -2330,6 +2367,10 @@ class StockItem( deltas = {'stockitem': self.pk} + transferorder = kwargs.pop('transferorder', None) + if transferorder: + deltas['transferorder'] = transferorder.pk + # Optional fields which can be supplied in a 'move' call for field in StockItem.optional_transfer_fields(): if field in kwargs: @@ -2478,6 +2519,10 @@ class StockItem( ) tracking_info['old_status_logical'] = old_status_logical + transferorder = kwargs.pop('transferorder', None) + if transferorder: + tracking_info['transferorder'] = transferorder.pk + # Optional fields which can be supplied in a 'move' call for field in StockItem.optional_transfer_fields(): if field in kwargs: @@ -2717,6 +2762,10 @@ class StockItem( setattr(self, field, kwargs[field]) deltas[field] = kwargs[field] + transferorder = kwargs.pop('transferorder', None) + if transferorder: + deltas['transferorder'] = transferorder.pk + self.save(add_note=False) self.add_tracking_entry( diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index a87de263dc..570b4f7e51 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -520,6 +520,8 @@ class StockItemSerializer( allocated=Coalesce( SubquerySum('sales_order_allocations__quantity'), Decimal(0) ) + # For now, stock allocated to a transfer order will not impact its availability + # + Coalesce(SubquerySum('transfer_order_allocations__quantity'), Decimal(0)) + Coalesce(SubquerySum('allocations__quantity'), Decimal(0)) ) @@ -1390,6 +1392,10 @@ class StockAssignmentItemSerializer(serializers.Serializer): if item.sales_order_allocations.count() > 0: raise ValidationError(_('Item is allocated to a sales order')) + # The item must not be allocated to a transfer order + if item.transfer_order_allocations.count() > 0: + raise ValidationError(_('Item is allocated to a transfer order')) + # The item must not be allocated to a build order if item.allocations.count() > 0: raise ValidationError(_('Item is allocated to a build order')) diff --git a/src/backend/InvenTree/users/oauth2_scopes.py b/src/backend/InvenTree/users/oauth2_scopes.py index 123cb8d528..52656a33fa 100644 --- a/src/backend/InvenTree/users/oauth2_scopes.py +++ b/src/backend/InvenTree/users/oauth2_scopes.py @@ -20,6 +20,7 @@ _roles = { 'purchase_order': 'Role Purchase Orders', 'sales_order': 'Role Sales Orders', 'return_order': 'Role Return Orders', + 'transfer_order': 'Role Transfer Orders', } _methods = {'view': 'GET', 'add': 'POST', 'change': 'PUT / PATCH', 'delete': 'DELETE'} diff --git a/src/backend/InvenTree/users/ruleset.py b/src/backend/InvenTree/users/ruleset.py index 272a30d452..3787eb89b9 100644 --- a/src/backend/InvenTree/users/ruleset.py +++ b/src/backend/InvenTree/users/ruleset.py @@ -19,6 +19,7 @@ class RuleSetEnum(StringEnum): PURCHASE_ORDER = 'purchase_order' SALES_ORDER = 'sales_order' RETURN_ORDER = 'return_order' + TRANSFER_ORDER = 'transfer_order' # This is a list of all the ruleset choices available in the system. @@ -34,6 +35,7 @@ RULESET_CHOICES = [ (RuleSetEnum.PURCHASE_ORDER, _('Purchase Orders')), (RuleSetEnum.SALES_ORDER, _('Sales Orders')), (RuleSetEnum.RETURN_ORDER, _('Return Orders')), + (RuleSetEnum.TRANSFER_ORDER, _('Transfer Orders')), ] # Ruleset names available in the system. @@ -161,6 +163,11 @@ def get_ruleset_models() -> dict: 'order_returnorderlineitem', 'order_returnorderextraline', ], + RuleSetEnum.TRANSFER_ORDER: [ + 'order_transferorder', + 'order_transferorderallocation', + 'order_transferorderlineitem', + ], } if settings.SITE_MULTI: diff --git a/src/frontend/lib/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx index bc06ace42c..cfb6e5e498 100644 --- a/src/frontend/lib/enums/ApiEndpoints.tsx +++ b/src/frontend/lib/enums/ApiEndpoints.tsx @@ -199,6 +199,17 @@ export enum ApiEndpoints { return_order_line_list = 'order/ro-line/', return_order_extra_line_list = 'order/ro-extra-line/', + transfer_order_list = 'order/transfer-order/', + transfer_order_issue = 'order/transfer-order/:id/issue/', + transfer_order_hold = 'order/transfer-order/:id/hold/', + transfer_order_cancel = 'order/transfer-order/:id/cancel/', + transfer_order_complete = 'order/transfer-order/:id/complete/', + transfer_order_allocate = 'order/transfer-order/:id/allocate/', + transfer_order_allocate_serials = 'order/transfer-order/:id/allocate-serials/', + + transfer_order_line_list = 'order/transfer-order-line/', + transfer_order_allocation_list = 'order/transfer-order-allocation/', + // Template API endpoints label_list = 'label/template/', label_print = 'label/print/', diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx index c9853d9b29..63aeb6dff5 100644 --- a/src/frontend/lib/enums/ModelInformation.tsx +++ b/src/frontend/lib/enums/ModelInformation.tsx @@ -207,6 +207,22 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.return_order_line_list, icon: 'return_orders' }, + transferorder: { + label: () => t`Transfer Order`, + label_multiple: () => t`Transfer Orders`, + url_overview: '/stock/location/index/transfer-orders', + url_detail: '/stock/transfer-order/:pk/', + api_endpoint: ApiEndpoints.transfer_order_list, + admin_url: '/order/transferorder/', + supports_barcode: true, + icon: 'transfer_orders' + }, + transferorderlineitem: { + label: () => t`Transfer Order Line Item`, + label_multiple: () => t`Transfer Order Line Items`, + api_endpoint: ApiEndpoints.transfer_order_line_list, + icon: 'transfer-orders' + }, address: { label: () => t`Address`, label_multiple: () => t`Addresses`, diff --git a/src/frontend/lib/enums/ModelType.tsx b/src/frontend/lib/enums/ModelType.tsx index 191d04545e..80c436b1b1 100644 --- a/src/frontend/lib/enums/ModelType.tsx +++ b/src/frontend/lib/enums/ModelType.tsx @@ -24,6 +24,8 @@ export enum ModelType { salesordershipment = 'salesordershipment', returnorder = 'returnorder', returnorderlineitem = 'returnorderlineitem', + transferorder = 'transferorder', + transferorderlineitem = 'transferorderlineitem', importsession = 'importsession', address = 'address', contact = 'contact', diff --git a/src/frontend/lib/enums/Roles.tsx b/src/frontend/lib/enums/Roles.tsx index 0f5aedd94c..3d7ff9c78b 100644 --- a/src/frontend/lib/enums/Roles.tsx +++ b/src/frontend/lib/enums/Roles.tsx @@ -11,6 +11,7 @@ export enum UserRoles { part_category = 'part_category', purchase_order = 'purchase_order', return_order = 'return_order', + transfer_order = 'transfer_order', sales_order = 'sales_order', stock = 'stock', stock_location = 'stock_location' @@ -40,6 +41,8 @@ export function userRoleLabel(role: UserRoles): string { return t`Purchase Orders`; case UserRoles.return_order: return t`Return Orders`; + case UserRoles.transfer_order: + return t`Transfer Orders`; case UserRoles.sales_order: return t`Sales Orders`; case UserRoles.stock: diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 329eb4dd32..191c56f2a2 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -51,7 +51,9 @@ import { RenderReturnOrder, RenderReturnOrderLineItem, RenderSalesOrder, - RenderSalesOrderShipment + RenderSalesOrderShipment, + RenderTransferOrder, + RenderTransferOrderLineItem } from './Order'; import { RenderPart, RenderPartCategory, RenderPartTestTemplate } from './Part'; import { RenderPlugin } from './Plugin'; @@ -87,6 +89,8 @@ export const RendererLookup: ModelRendererDict = { [ModelType.returnorderlineitem]: RenderReturnOrderLineItem, [ModelType.salesorder]: RenderSalesOrder, [ModelType.salesordershipment]: RenderSalesOrderShipment, + [ModelType.transferorder]: RenderTransferOrder, + [ModelType.transferorderlineitem]: RenderTransferOrderLineItem, [ModelType.stocklocation]: RenderStockLocation, [ModelType.stocklocationtype]: RenderStockLocationType, [ModelType.stockitem]: RenderStockItem, diff --git a/src/frontend/src/components/render/Order.tsx b/src/frontend/src/components/render/Order.tsx index e74915db18..c9475d7dea 100644 --- a/src/frontend/src/components/render/Order.tsx +++ b/src/frontend/src/components/render/Order.tsx @@ -123,3 +123,46 @@ export function RenderSalesOrderShipment({ /> ); } + +/** + * Inline rendering of a single TransferOrder instance + */ +export function RenderTransferOrder( + props: Readonly +): ReactNode { + const { instance } = props; + + return ( + + ); +} + +export function RenderTransferOrderLineItem( + props: Readonly +): ReactNode { + const { instance } = props; + + return ( + + ); +} diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx index 4754d9abb9..058ad80dbf 100644 --- a/src/frontend/src/defaults/backendMappings.tsx +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -11,6 +11,8 @@ export const statusCodeList: Record = { PurchaseOrderStatus: ModelType.purchaseorder, ReturnOrderStatus: ModelType.returnorder, ReturnOrderLineStatus: ModelType.returnorderlineitem, + TransferOrderStatus: ModelType.transferorder, + TransferOrderLineStatus: ModelType.transferorderlineitem, SalesOrderStatus: ModelType.salesorder, StockHistoryCode: ModelType.stockhistory, StockStatus: ModelType.stockitem, diff --git a/src/frontend/src/forms/TransferOrderForms.tsx b/src/frontend/src/forms/TransferOrderForms.tsx new file mode 100644 index 0000000000..a7e3c8be3f --- /dev/null +++ b/src/frontend/src/forms/TransferOrderForms.tsx @@ -0,0 +1,308 @@ +import { ApiEndpoints, ModelType, ProgressBar, apiUrl } from '@lib/index'; +import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms'; +import { t } from '@lingui/core/macro'; +import { Table } from '@mantine/core'; +import { IconCalendar, IconUsers } from '@tabler/icons-react'; +import { useMemo, useState } from 'react'; +import RemoveRowButton from '../components/buttons/RemoveRowButton'; +import { StandaloneField } from '../components/forms/StandaloneField'; +import type { TableFieldRowProps } from '../components/forms/fields/TableField'; +import { useCreateApiFormModal } from '../hooks/UseForm'; +import { useGlobalSettingsState } from '../states/SettingsStates'; +import { RenderPartColumn } from '../tables/ColumnRenderers'; + +export function useTransferOrderFields({ + duplicateOrderId +}: { + duplicateOrderId?: number; +}): ApiFormFieldSet { + const globalSettings = useGlobalSettingsState(); + + return useMemo(() => { + const fields: ApiFormFieldSet = { + reference: {}, + description: {}, + project_code: {}, + start_date: { + icon: + }, + target_date: { + icon: + }, + take_from: {}, + destination: { + filters: { + structural: false + } + }, + consume: {}, + link: {}, + responsible: { + filters: { + is_active: true + }, + icon: + } + }; + + // Order duplication fields + if (!!duplicateOrderId) { + fields.duplicate = { + children: { + order_id: { + hidden: true, + value: duplicateOrderId + }, + copy_lines: {}, + // Transfer Orders don't have extra lines for now... + copy_extra_lines: { hidden: true, value: false } + } + }; + } + + if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) { + delete fields.project_code; + } + + return fields; + }, [duplicateOrderId, globalSettings]); +} + +export function useTransferOrderLineItemFields({ + orderId, + create +}: { + orderId?: number; + create?: boolean; +}): ApiFormFieldSet { + return useMemo(() => { + const fields: ApiFormFieldSet = { + order: { + filters: {}, + disabled: true + }, + part: { + filters: { + active: true, + virtual: false + } + }, + reference: {}, + quantity: {}, + project_code: { + description: t`Select project code for this line item` + }, + target_date: {}, + notes: {}, + link: {} + }; + + return fields; + }, [orderId, create]); +} + +function TransferOrderAllocateLineRow({ + props, + record, + sourceLocation +}: Readonly<{ + props: TableFieldRowProps; + record: any; + sourceLocation?: number | null; +}>) { + // Statically defined field for selecting the stock item + const stockItemField: ApiFormFieldType = useMemo(() => { + return { + field_type: 'related field', + api_url: apiUrl(ApiEndpoints.stock_item_list), + model: ModelType.stockitem, + autoFill: true, + filters: { + available: true, + part_detail: true, + location_detail: true, + location: sourceLocation, + cascade: sourceLocation ? true : undefined, + part: record.part + }, + value: props.item.stock_item, + name: 'stock_item', + onValueChange: (value: any, instance: any) => { + props.changeFn(props.idx, 'stock_item', value); + + // Update the allocated quantity based on the selected stock item + if (instance) { + const available = instance.quantity - instance.allocated; + const required = record.quantity - record.allocated; + + let quantity = props.item?.quantity ?? 0; + + quantity = Math.max(quantity, required); + quantity = Math.min(quantity, available); + + if (quantity != props.item.quantity) { + props.changeFn(props.idx, 'quantity', quantity); + } + } + } + }; + }, [sourceLocation, record, props]); + + // Statically defined field for selecting the allocation quantity + const quantityField: ApiFormFieldType = useMemo(() => { + return { + field_type: 'number', + name: 'quantity', + required: true, + value: props.item.quantity, + onValueChange: (value: any) => { + props.changeFn(props.idx, 'quantity', value); + } + }; + }, [props]); + + return ( + + + + + + + + + + + + + + + props.removeFn(props.idx)} /> + + + ); +} + +export function useAllocateToTransferOrderForm({ + orderId, + sourceLocationId, + lineItems, + onFormSuccess +}: { + orderId: number; + sourceLocationId?: number; + lineItems: any[]; + onFormSuccess: (response: any) => void; +}) { + const [sourceLocation, setSourceLocation] = useState( + sourceLocationId || null + ); + + const fields: ApiFormFieldSet = useMemo(() => { + return { + // Non-submitted field to select the source location + source_location: { + exclude: true, + required: false, + value: sourceLocationId, + field_type: 'related field', + api_url: apiUrl(ApiEndpoints.stock_location_list), + model: ModelType.stocklocation, + label: t`Source Location`, + description: t`Select the source location for the stock allocation`, + onValueChange: (value: any) => { + setSourceLocation(value); + } + }, + items: { + field_type: 'table', + value: [], + headers: [ + { title: t`Part`, style: { minWidth: '200px' } }, + { title: t`Allocated`, style: { minWidth: '200px' } }, + { title: t`Stock Item`, style: { width: '100%' } }, + { title: t`Quantity`, style: { minWidth: '200px' } }, + { title: '', style: { width: '50px' } } + ], + modelRenderer: (row: TableFieldRowProps) => { + const record = + lineItems.find((item) => item.pk == row.item.line_item) ?? {}; + + return ( + + ); + } + } + }; + }, [orderId, lineItems, sourceLocation]); + + return useCreateApiFormModal({ + title: t`Allocate Stock`, + url: ApiEndpoints.transfer_order_allocate, + pk: orderId, + fields: fields, + onFormSuccess: onFormSuccess, + successMessage: t`Stock items allocated`, + size: '80%', + initialData: { + items: lineItems.map((item) => { + return { + line_item: item.pk, + quantity: 0, + stock_item: null + }; + }) + } + }); +} + +export function useTransferOrderAllocationFields({ + orderId +}: { + orderId?: number; +}): ApiFormFieldSet { + return useMemo(() => { + return { + item: { + // Cannot change item, but display for reference + disabled: true + }, + quantity: {} + }; + }, [orderId]); +} + +export function useTransferOrderAllocateSerialsFields({ + itemId, + orderId +}: { + itemId: number; + orderId: number; +}): ApiFormFieldSet { + return useMemo(() => { + return { + line_item: { + value: itemId, + hidden: true + }, + quantity: {}, + serial_numbers: {} + }; + }, [itemId, orderId]); +} diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 00f9726f80..e169f89e79 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -21,6 +21,7 @@ import { IconCancel, IconCheck, IconCircleCheck, + IconCircleDashedCheck, IconCircleMinus, IconCirclePlus, IconCircleX, @@ -148,11 +149,13 @@ const icons: InvenTreeIconType = { build_order: IconTools, builds: IconTools, used_in: IconStack2, + consume: IconCircleDashedCheck, manufacturers: IconBuildingFactory2, suppliers: IconBuilding, customers: IconBuildingStore, purchase_orders: IconShoppingCart, return_orders: IconTruckReturn, + transfer_orders: IconTransfer, sales_orders: IconTruckDelivery, scheduling: IconCalendarStats, scrap: IconCircleX, diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index bd5c368b8f..409110efee 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -13,6 +13,7 @@ import { IconQrcode, IconServerCog, IconShoppingCart, + IconTransfer, IconTruckDelivery } from '@tabler/icons-react'; import { useMemo } from 'react'; @@ -363,6 +364,20 @@ export default function SystemSettings() { ) }, + { + name: 'transferorders', + label: t`Transfer Orders`, + icon: , + content: ( + + ) + }, { name: 'plugins', label: t`Plugins`, diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 209711b9b2..a42157c7d1 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -30,6 +30,7 @@ import { IconStack2, IconTestPipe, IconTools, + IconTransfer, IconTruckDelivery, IconTruckReturn, IconVersions @@ -101,6 +102,7 @@ import { RelatedPartTable } from '../../tables/part/RelatedPartTable'; import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable'; import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; +import { TransferOrderTable } from '../../tables/stock/TransferOrderTable'; import PartAllocationPanel from './PartAllocationPanel'; import PartPricingPanel from './PartPricingPanel'; import PartStockHistoryDetail from './PartStockHistoryDetail'; @@ -771,6 +773,20 @@ export default function PartDetail() { hidden: !part.assembly || !user.hasViewRole(UserRoles.build), content: part.pk ? : }, + { + name: 'transfer_orders', + label: t`Transfer Orders`, + icon: , + hidden: + part.virtual || + !globalSettings.isSet('TRANSFERORDER_ENABLED') || + !user.hasViewRole(UserRoles.transfer_order), + content: part.pk ? ( + + ) : ( + + ) + }, { name: 'stocktake', label: t`Stock History`, diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 629f95d83d..5a3f0e50c7 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -8,11 +8,13 @@ import type { PanelType } from '@lib/types/Panel'; import { t } from '@lingui/core/macro'; import { Group, Skeleton, Stack } from '@mantine/core'; import { + IconCalendar, IconInfoCircle, IconListDetails, IconPackages, IconSitemap, - IconTable + IconTable, + IconTransfer } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -20,6 +22,7 @@ import { api } from '../../App'; import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialog'; import AdminButton from '../../components/buttons/AdminButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; +import OrderCalendar from '../../components/calendar/OrderCalendar'; import { type DetailsField, DetailsTable @@ -48,11 +51,14 @@ import { import { useInstance } from '../../hooks/UseInstance'; import { useStockAdjustActions } from '../../hooks/UseStockAdjustActions'; import { useUserSettingsState } from '../../states/SettingsStates'; +import { useGlobalSettingsState } from '../../states/SettingsStates'; import { useUserState } from '../../states/UserState'; import { PartListTable } from '../../tables/part/PartTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import StockLocationParametricTable from '../../tables/stock/StockLocationParametricTable'; import { StockLocationTable } from '../../tables/stock/StockLocationTable'; +import TransferOrderParametricTable from '../../tables/stock/TransferOrderParametricTable'; +import { TransferOrderTable } from '../../tables/stock/TransferOrderTable'; export default function Stock() { const { id: _id } = useParams(); @@ -65,6 +71,7 @@ export default function Stock() { const navigate = useNavigate(); const user = useUserState(); const settings = useUserSettingsState(); + const globalSettings = useGlobalSettingsState(); const [treeOpen, setTreeOpen] = useState(false); @@ -169,6 +176,7 @@ export default function Stock() { }, [location, instanceQuery]); const [sublocationView, setSublocationView] = useState('table'); + const [transferOrderView, setTransferOrderView] = useState('table'); const locationPanels: PanelType[] = useMemo(() => { return [ @@ -219,6 +227,42 @@ export default function Stock() { /> ) }, + SegmentedControlPanel({ + name: 'transfer-orders', + label: t`Transfer Orders`, + icon: , + hidden: + !user.hasViewRole(UserRoles.transfer_order) || + !globalSettings.isSet('TRANSFERORDER_ENABLED'), + selection: transferOrderView, + onChange: setTransferOrderView, + options: [ + { + value: 'table', + label: t`Table View`, + icon: , + content: + }, + { + value: 'calendar', + label: t`Calendar View`, + icon: , + content: ( + + ) + }, + { + value: 'parametric', + label: t`Parametric View`, + icon: , + content: + } + ] + }), { name: 'default_parts', label: t`Default Parts`, @@ -240,7 +284,7 @@ export default function Stock() { hidden: !location.pk }) ]; - }, [sublocationView, location, id]); + }, [sublocationView, transferOrderView, location, id]); const editLocation = useEditApiFormModal({ url: ApiEndpoints.stock_location_list, diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 88fe30945f..da674823b2 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -88,6 +88,7 @@ import InstalledItemsTable from '../../tables/stock/InstalledItemsTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable'; import { StockTrackingTable } from '../../tables/stock/StockTrackingTable'; +import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable'; export default function StockDetail() { const { id } = useParams(); @@ -476,6 +477,13 @@ export default function StockDetail() { return stockitem?.part_detail?.salable; }, [stockitem]); + const showTransferAllocations: boolean = useMemo(() => { + return ( + !stockitem?.part_detail?.virtual && + globalSettings.isSet('TRANSFERORDER_ENABLED') + ); + }, [stockitem]); + // API query to determine if this stock item has trackable BOM items const trackedBomItemQuery = useQuery({ queryKey: ['tracked-bom-item', stockitem.pk, stockitem.part], @@ -544,11 +552,17 @@ export default function StockDetail() { icon: , hidden: !stockitem.in_stock || - (!showSalesAllocations && !showBuildAllocations), + (!showSalesAllocations && + !showBuildAllocations && + !showTransferAllocations), content: ( {showBuildAllocations && ( @@ -580,6 +594,24 @@ export default function StockDetail() { )} + {showTransferAllocations && ( + + + {t`Transfer Order Allocations`} + + + + + + )} ) }, diff --git a/src/frontend/src/pages/stock/TransferOrderDetail.tsx b/src/frontend/src/pages/stock/TransferOrderDetail.tsx new file mode 100644 index 0000000000..5d06fa2732 --- /dev/null +++ b/src/frontend/src/pages/stock/TransferOrderDetail.tsx @@ -0,0 +1,552 @@ +import { t } from '@lingui/core/macro'; +import { Grid, Skeleton, Stack } from '@mantine/core'; +import { type ReactNode, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; +import { UserRoles } from '@lib/enums/Roles'; +import { type PanelType, apiUrl } from '@lib/index'; +import { + IconBookmark, + IconInfoCircle, + IconList, + IconListCheck +} from '@tabler/icons-react'; +import AdminButton from '../../components/buttons/AdminButton'; +import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; +import { PrintingActions } from '../../components/buttons/PrintingActions'; +import { + type DetailsField, + DetailsTable +} from '../../components/details/Details'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; +import { + BarcodeActionDropdown, + CancelItemAction, + DuplicateItemAction, + EditItemAction, + HoldItemAction, + OptionsActionDropdown +} from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; +import { PageDetail } from '../../components/nav/PageDetail'; +import AttachmentPanel from '../../components/panels/AttachmentPanel'; +import NotesPanel from '../../components/panels/NotesPanel'; +import { PanelGroup } from '../../components/panels/PanelGroup'; +import ParametersPanel from '../../components/panels/ParametersPanel'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; +import { useTransferOrderFields } from '../../forms/TransferOrderForms'; +import { + useCreateApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; +import { useInstance } from '../../hooks/UseInstance'; +import useStatusCodes from '../../hooks/UseStatusCodes'; +import { useGlobalSettingsState } from '../../states/SettingsStates'; +import { useUserState } from '../../states/UserState'; +import TransferOrderAllocationTable from '../../tables/stock/TransferOrderAllocationTable'; +import TransferOrderLineItemTable from '../../tables/stock/TransferOrderLineItemTable'; + +export default function TransferOrderDetail() { + const { id } = useParams(); + + const user = useUserState(); + + const globalSettings = useGlobalSettingsState(); + + const { + instance: order, + instanceQuery, + refreshInstance + } = useInstance({ + endpoint: ApiEndpoints.transfer_order_list, + pk: id, + params: {} + }); + + const toStatus = useStatusCodes({ modelType: ModelType.transferorder }); + + const lineItemsEditable: boolean = useMemo(() => { + const orderOpen: boolean = + order.status != toStatus.COMPLETE && order.status != toStatus.CANCELLED; + + return orderOpen; + // TODO: does this setting make any sense for Transfer Orders??? + // if (orderOpen) { + // return true; + // } else { + // return globalSettings.isSet('TRANSFERORDER_EDIT_COMPLETED_ORDERS'); + // } + }, [globalSettings, order.status, toStatus]); + + // for now, only permit editing allocations when line items can be edited + const allocationsEditable = lineItemsEditable; + + const orderOpen = useMemo(() => { + return ( + order.status == toStatus.PENDING || + order.status == toStatus.ISSUED || + order.status == toStatus.ON_HOLD + ); + }, [order, toStatus]); + + const detailsPanel = useMemo(() => { + if (instanceQuery.isFetching) { + return ; + } + + const tl: DetailsField[] = [ + { + type: 'text', + name: 'reference', + label: t`Reference`, + copy: true + }, + { + type: 'link', + name: 'take_from', + icon: 'location', + label: t`Source Location`, + model: ModelType.stocklocation + }, + { + type: 'link', + name: 'destination', + icon: 'location', + label: t`Destination Location`, + model: ModelType.stocklocation + }, + { + type: 'text', + name: 'description', + label: t`Description`, + copy: true + }, + { + type: 'status', + name: 'status', + label: t`Status`, + model: ModelType.transferorder + }, + { + type: 'status', + name: 'status_custom_key', + label: t`Custom Status`, + model: ModelType.transferorder, + icon: 'status', + hidden: + !order.status_custom_key || order.status_custom_key == order.status + } + ]; + + const tr: DetailsField[] = [ + { + type: 'boolean', + name: 'consume', + icon: 'consume', + label: t`Consume Stock` + }, + { + type: 'text', + name: 'line_items', + label: t`Line Items`, + icon: 'list' + }, + { + type: 'progressbar', + name: 'completed', + icon: 'progress', + label: t`Completed Line Items`, + total: order.line_items, + progress: order.completed_lines + } + ]; + + const bl: DetailsField[] = [ + { + type: 'link', + external: true, + name: 'link', + label: t`Link`, + copy: true, + hidden: !order.link + }, + { + type: 'text', + name: 'project_code_label', + label: t`Project Code`, + icon: 'reference', + copy: true, + hidden: !order.project_code + }, + { + type: 'text', + name: 'responsible', + label: t`Responsible`, + badge: 'owner', + hidden: !order.responsible + } + ]; + + const br: DetailsField[] = [ + { + type: 'date', + name: 'creation_date', + label: t`Creation Date`, + icon: 'calendar', + copy: true, + hidden: !order.creation_date + }, + { + type: 'date', + name: 'issue_date', + label: t`Issue Date`, + icon: 'calendar', + copy: true, + hidden: !order.issue_date + }, + { + type: 'date', + name: 'start_date', + label: t`Start Date`, + icon: 'calendar', + copy: true, + hidden: !order.start_date + }, + { + type: 'date', + name: 'target_date', + label: t`Target Date`, + copy: true, + hidden: !order.target_date + }, + { + type: 'date', + name: 'complete_date', + icon: 'calendar_check', + label: t`Completion Date`, + copy: true, + hidden: !order.complete_date + } + ]; + + return ( + + + {/* TODO: what image do we show for a Transfer Order? */} + {/* */} + + + + + + + + + ); + }, [order, instanceQuery]); + + const orderPanels: PanelType[] = useMemo(() => { + return [ + { + name: 'detail', + label: t`Order Details`, + icon: , + content: detailsPanel + }, + { + name: 'line-items', + label: t`Line Items`, + icon: , + content: ( + + // TODO: add back the accordion if we need extra lines + // + // + // + // {t`Line Items`} + // + // + // + // + // + // {/* + // + // {t`Extra Line Items`} + // + // + // + // + // */} + // + ) + }, + { + name: 'allocations', + label: + order.status != toStatus.COMPLETE + ? t`Allocated Stock` + : t`Transferred Stock`, + icon: + order.status != toStatus.COMPLETE ? ( + + ) : ( + + ), + content: ( + + ) + }, + ParametersPanel({ + model_type: ModelType.transferorder, + model_id: order.pk + }), + AttachmentPanel({ + model_type: ModelType.transferorder, + model_id: order.pk + }), + NotesPanel({ + model_type: ModelType.transferorder, + model_id: order.pk + }) + ]; + }, [order, id, user]); + + const orderBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + + ]; + }, [order, instanceQuery]); + + const transferOrderFields = useTransferOrderFields({}); + + const duplicateTransferOrderFields = useTransferOrderFields({ + duplicateOrderId: order.pk + }); + + const editTransferOrder = useEditApiFormModal({ + url: ApiEndpoints.transfer_order_list, + pk: order.pk, + title: t`Edit Transfer Order`, + fields: transferOrderFields, + onFormSuccess: () => { + refreshInstance(); + } + }); + + const duplicateTransferOrderInitialData = useMemo(() => { + const data = { ...order }; + // if we set the reference to null/undefined, it will be left blank in the form + // if we omit the reference altogether, it will be auto-generated via reference pattern + // from the OPTIONS response + delete data.reference; + return data; + }, [order]); + + const duplicateTransferOrder = useCreateApiFormModal({ + url: ApiEndpoints.transfer_order_list, + title: t`Add Transfer Order`, + fields: duplicateTransferOrderFields, + initialData: duplicateTransferOrderInitialData, + modelType: ModelType.transferorder, + follow: true + }); + + const issueOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.transfer_order_issue, order.pk), + title: t`Issue Transfer Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Issue this order`, + successMessage: t`Order issued` + }); + + const cancelOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.transfer_order_cancel, order.pk), + title: t`Cancel Transfer Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Cancel this order`, + successMessage: t`Order cancelled` + }); + + const holdOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.transfer_order_hold, order.pk), + title: t`Hold Transfer Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Place this order on hold`, + successMessage: t`Order placed on hold` + }); + + const completeOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.transfer_order_complete, order.pk), + title: t`Complete Transfer Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Mark this order as complete`, + successMessage: t`Order completed`, + fields: { + accept_incomplete_allocation: {} + } + }); + + const orderActions = useMemo(() => { + const canEdit: boolean = user.hasChangeRole(UserRoles.transfer_order); + + const canIssue: boolean = + canEdit && + (order.status == toStatus.PENDING || order.status == toStatus.ON_HOLD); + + const canHold: boolean = + canEdit && + (order.status == toStatus.PENDING || order.status == toStatus.ISSUED); + + const canCancel: boolean = + canEdit && + (order.status == toStatus.PENDING || order.status == toStatus.ON_HOLD); + + const canComplete: boolean = canEdit && order.status == toStatus.ISSUED; + + return [ +