From 189f73b7c23dcb10cb252ab4f4a11d7709433663 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 11 Jun 2025 09:10:41 -0500 Subject: [PATCH] Initial commit. --- __init__.py | 37 +++ __pycache__/__init__.cpython-313.pyc | Bin 0 -> 1761 bytes __pycache__/app.cpython-313.pyc | Bin 0 -> 306 bytes __pycache__/routes.cpython-313.pyc | Bin 0 -> 18377 bytes __pycache__/utils.cpython-313.pyc | Bin 0 -> 2035 bytes app.py | 6 + models/__init__.py | 11 + models/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 547 bytes models/__pycache__/areas.cpython-313.pyc | Bin 0 -> 1453 bytes models/__pycache__/brands.cpython-313.pyc | Bin 0 -> 1474 bytes models/__pycache__/inventory.cpython-313.pyc | Bin 0 -> 5028 bytes models/__pycache__/items.cpython-313.pyc | Bin 0 -> 1689 bytes .../room_functions.cpython-313.pyc | Bin 0 -> 1536 bytes models/__pycache__/rooms.cpython-313.pyc | Bin 0 -> 2669 bytes models/__pycache__/users.cpython-313.pyc | Bin 0 -> 3228 bytes models/__pycache__/work_log.cpython-313.pyc | Bin 0 -> 3198 bytes models/areas.py | 19 ++ models/brands.py | 19 ++ models/inventory.py | 73 ++++++ models/items.py | 20 ++ models/room_functions.py | 19 ++ models/rooms.py | 34 +++ models/users.py | 36 +++ models/work_log.py | 35 +++ requirements.txt | 3 + routes.py | 236 ++++++++++++++++++ templates/_table_fragment.html | 75 ++++++ templates/index.html | 21 ++ templates/inventory.html | 127 ++++++++++ templates/layout.html | 53 ++++ templates/table.html | 20 ++ templates/user.html | 130 ++++++++++ templates/worklog.html | 60 +++++ utils.py | 30 +++ 34 files changed, 1064 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/app.cpython-313.pyc create mode 100644 __pycache__/routes.cpython-313.pyc create mode 100644 __pycache__/utils.cpython-313.pyc create mode 100644 app.py create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-313.pyc create mode 100644 models/__pycache__/areas.cpython-313.pyc create mode 100644 models/__pycache__/brands.cpython-313.pyc create mode 100644 models/__pycache__/inventory.cpython-313.pyc create mode 100644 models/__pycache__/items.cpython-313.pyc create mode 100644 models/__pycache__/room_functions.cpython-313.pyc create mode 100644 models/__pycache__/rooms.cpython-313.pyc create mode 100644 models/__pycache__/users.cpython-313.pyc create mode 100644 models/__pycache__/work_log.cpython-313.pyc create mode 100644 models/areas.py create mode 100644 models/brands.py create mode 100644 models/inventory.py create mode 100644 models/items.py create mode 100644 models/room_functions.py create mode 100644 models/rooms.py create mode 100644 models/users.py create mode 100644 models/work_log.py create mode 100644 requirements.txt create mode 100644 routes.py create mode 100644 templates/_table_fragment.html create mode 100644 templates/index.html create mode 100644 templates/inventory.html create mode 100644 templates/layout.html create mode 100644 templates/table.html create mode 100644 templates/user.html create mode 100644 templates/worklog.html create mode 100644 utils.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..199ca67 --- /dev/null +++ b/__init__.py @@ -0,0 +1,37 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from urllib.parse import quote_plus +import logging + +db = SQLAlchemy() + +logger = logging.getLogger('sqlalchemy.engine') +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) +logger.addHandler(handler) + +def create_app(): + app = Flask(__name__) + + params = quote_plus( + "DRIVER=ODBC Driver 17 for SQL Server;" + "SERVER=NDVASQLCR01;" + "DATABASE=conradTest;" + "Trusted_Connection=yes;" + ) + + app.config['SQLALCHEMY_DATABASE_URI'] = f"mssql+pyodbc:///?odbc_connect={params}" + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + + from .routes import main + app.register_blueprint(main) + + @app.route("/") + def index(): + return "Hello, you've reached the terrible but functional root of the site." + + + return app diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c48d94dc32fd2921478e3ddd0eb6c04812e9194a GIT binary patch literal 1761 zcmey&%ge>Uz`(HnfMdpWW(J1GAPx+(LK&Y`7#SF*G6XXOGkP<4F%~f@fY?mlOkT`I z%wRUNH;Wf*5i6L@;>}jX7Q?K>U<%b<#2&*IBmm_gQA!NKtfq_*fg;Wr&LB|;8<~t@ z3lc--L)b7~3Jfu9K@t!#WD?14-Wc8>S!6i~JBBSt4#Gkvl^BBAU}nQ;1%?>TAVp+V z5OxeNLPapU6ho0CzRZ!K)O)V}? zOi$G;P6k;6!^{i}3?TfO2^>w)&>&@Ch-QK*fjKpgAsQB`48e?WQO01VP)0rmW2R7s zAU2pD1Rcz*&cMJB%z{If6(k$Y0aG8u4PznbPzIP?3Jk$)!R+2_UhH{H3JjqPL4pWX zFjgoNLUkxZkT6UfK?ifdR0MNM!4p~@b0~ufgC>_>RkBNvXP9e{t-p(tvw}-eW?5>H zf}y!WT7Hp2aG;MuaB5LmYLRuYYY<40pG%lyaG;NKkb$AKi(`nRlVh-}ZE}8IQDRC+ zYH^8mNKt8VNoq>GbADc4YH~?tex7Y*YO!^dTyAl3VUBh|WqwLhvX#ERzCDN$pA1oE zdrKS?^*+uXuD+4+FmvNWgFM}C$sx&v1UWi;$NT!bc)EEyJBE1r`vt!Q#h@nREtcHG z%)DES`Y5ptk^_avXBGxfrc7tlWb&(W_DIdi$=6Y+%r8|hOI0XJO-#;6O;ISxNL46F zEh@@P%1Kp7DlJh+E6oFYATdXwC_leMAwLbQrZ}@CRc|Fj5ibJ+!)H(^-_mimiU}=F zEh>(21_y+mUusTqeqKywURi2hNq$jfOniK1US>&ryk0@&E!NDul+=n_lF3D>i6yD= zi3J6EHaYppi8;k~dSEF91_p*=P|9myc)%Z0#K*wEpvhGPO0>5);^RRHJU+e%B!7!7 zIX^EgGyN7*O42P3unma?1-ICW@=Hrni;F;I)h(f-)bz~alGLL3q@2>!f}+g4l3T1` z1)40kn8C^m5{nXZAp(?A6XB!~pS?07_RQ3UeNEuPY%oSe)gy@JG|;#5fJ zvFGHcr)TD+7m0wp$(f#7;*+1Ao?3K^4J46Tbc>@nwZsQhsN7=l^mFsS#T#5wl$x09 zk(if~lUj6(Jp;<%DNZeM%P-1JEGbDXy2XiLawVpuKvjc-4xDDdj)nv+hfQvNN@-52 zU6CdO0|O|r7MCzEFnnNUWMsU{AbyuY{Vs#@Z3e!Fto+R}4NMO>1+H^SUE-9wAgyUz`)?*XrGbFz`*br#DQTJDC4si0|UcUhG2$ZMsEf$#v(=qh8RXA22-eX z5mO9vFq2gT14E(!149f`5GPaui3(;8X0c>cU`S_8XVqkT$;iOKpvicPE4e5&u_QG< zv7q1&2#FaQAhSV-Ie literal 0 HcmV?d00001 diff --git a/__pycache__/routes.cpython-313.pyc b/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5802dc833a1b734ab906a4353eaa42a5bb0ae280 GIT binary patch literal 18377 zcmey&%ge>Uz`*do)idLVCIiD`5C?|&pp4I_m>3wQG6XXOGkP<4F%~g;F%>a+F&8m= zu@tc=Fa$FNGkddou@$j-u@|v>aTIZQaTalUaTRfSaTjrW@f7iR@fPtafb_F?^A+(0 zGX%4G^Lq&t34q1eyam04iiEs`i-f&IibTNXv3rYpi4}=~*&N>DUJ^wTUXn$UUQ$I; zUeZO<3JfufN({lAF*2r15H}Xd26Gk31#=h4>oWxN1Tq#W=raWK1~L{Y2J=ZV1oKNV z1Pe$p1Pe+r1Pe(q1PeB1dB;A1dB^C1WQOU1WQUW1WW1h6e$HuOECn? zNHGMNihV=OECm1NHGK}>ai6m2P;W21S?B11gl6f1glCh1gl9g1glFi1Zzk! z1Z(QC7pcfG1Um+6#i*Jv1#3$&WU*v17OBC+f^}llgLPvxg7sq5gY{#CVubY>V?<&! zSQ!`?Oqf7qutAKv2~V&gRHtT)Y)~k~pU7mektrjDA8aheU!FEy%S&VT18sgAl z57*&C8SP*`cQ+~Rb~DNQXX%FHXd#b1<~my%i(Uy_}Q>Qt1NmvW26vm`b57N=)kS!!NMeo^Huo}m2v zT({D^QN*u(RSvVHQ?Z?S+R{WKYGab@La=B1|O^!3Zj%k|2Q_413-^^FV+4D^f3(v=j-GgC@3Y?TbnloT>jGt)ClAndfv zoE%%FG&H8j?;uz=b5k9+aA!UzVD3i^CFt7(S;kfD2~EP=+9o zI2zV~@)Knk7_h4d0+|9;1*3wQU>V1NQH3Fx0Yt83_PfPWl3Gzx#i1IhtD38;nxe^c zi?bv%H?_DVF}L6rM{!9}8i>1+@fKrdG03qBx3rwCVn7+7IL0|YuP8A^&o4EnI6p52 zo=#(m@=Hrni}eaBZ*kbgm!sXQq ztFNnDUsSi=;dNQv{i0mrg_wkka*3ZoE=LI;P^AF!*=ISt;RE&=lV6cC0|NsnQ)Z^5 z=9Pe=qo_y?wxFKyp;&nB%i)v;UWzDJL3bx|ZqRhk`+_r;4s~8#rW(y3LTP(D= zD5XOcyV;Wxi<0wG@OT}RCSaC>fZBU|i0=kbODNLY@oa zx);QKc9b0`*^zfa+y{@R2q$ZZvlwr&=H{oQ;)zUfI)_Fk)F~ImHSkzXukd8a1XZlu zDXGQDMVSSl`VM!BR0oAB$R`a9I}G=m?Khj@w!m?@+d{YNDrOf|%q~cqUyu$x!FC}a z1matFA;YhYNQxLjkQ z#s;?w!6Da!qpt@?AMw5V|&h07COZ~x+kx5O2ce{lqZt1<=UUtCU!MGDUODX9v|MVt%_ z47XT)LFHlu?`ingnocHW+vHFA45Qh&m1aEOV!`w@04bPdASX=^0m!L3* zG?UO87zUvD3Ih?KxS`ZWuC&Y|f>s)WY|{e~7*>LECpZNU-m;;{9n^ecxy4mnT98^) zmRX!%1WEKnWPKx0%@GL6`h`W;3u~_z)JS7>oM6E zW3n&0gBMEjCN&Xu_AOASV6WnVD+7FRGLeaAB}FjTPxgTl?eiro=xmpw{q8v+?u zlhv=t1LSW{5aA6X0zfH(tXR1!{V=pC0z_lx@ebtQ8MKN1A~+s(}jw<4-5>BOs=3jT*2rD zqNLogawcaMHzQIMNJymwFG*evUqx)ph6hbFhs&= zV>7Saw!a1u5qXQmGdr;w~hl zTnJCSAeD;8tMsZVIFj>o3vyCRh$>fYL2(DIEF716E%e&Kb|F0CdU)dX@WeCRm!wOt zq?cThE*%WsO3Tm5$uBQ0Af~ziRhq?6Zv`%oS{SuM>_SB3^@ybF5lLsHFUgc$$tb-f zQ#Kg9m6(^9Q(2r@OpLcc{V%AuRw%C5TB)@o?Luta_1N_5vFT^BFDX`E$*sDiSPk}; zCi^XRXGjh4ixV`qt^jJrS8)Y{vb91;W^QT~hihI6gvIF$O*8htxZJ?utPomY|BFAc zG&4C{!4c|2`&%ppiRr1y$axN4*lRI@Mp?1vJS8|g#zN`u}2hviVe8!ObiTp9HA_s ztf6e79HETCEPAXe44`Tq)bYK=1{zH*sVoBdPm}c)E2yR^zQvJ|SR9|1T2TUF6%?hG z-Qq6EFGbzzU z(E{obLfrs%WKk=q>;Q#4Xr=*-!M(Iw!nujrsUR~l^Abx^Q{qz+OA?DFFfcGUfix@t zR}>$(7qoP~> zJ|};t*aVRoj1whhL|hb5z09e0pOdFkxLfoCCj%?j7aj%y;R&YQRWl+k@~bv@+~60U zkl0z#;BiApe7g2T?a8_g-ZzBhE(&X|aJ(q2yFufkkX3{C13|GHA~GN(u5?2{sO$vi@$>$^B`YjqXNDh{YU<_q91<5dgQbjbB1+L$igFu-CtO`NEQdKAi zB=-n2gmD%#=`$2F2{U+zGce@O#5Oh*%BsW=$_Y!spi!a#CSOSg25trghGM2X&QPXM zj$js$b)g)=tYDfElsbdiz-;DFmSA>EZWV@NCW!hp22BpXDy|aHgp3|&7!I5+Rx&{o zSaNDkPH_=Dm4Fk5Cf6-CNWk4N~ntUWkkf(c0K6obxT z79^%a(D6xDN zLrYM>jG&bTmP{%Pnk;@*T$y<(sTJTTsuDzu^7tm^f#ydw8Nsoj$p{YJl?>oy4ha=j zu*RZ=pd1Rq0^mS+!Ywt!=7PS{MQ-N{EY4u%$)HjT<}y&>4=PYUgQlXUGK4Z95_%}3 z38Mi+D3c4L8DlUbXqZxy$*&01f6-(t0{I3yxC>4Lx7f-Ob4pW-i$PvgP*8xli6tj9 zFS}?70|NsnN{Yq7PP!o?u|#}@#6?A$4Ph4*Z7y=#USP2WyR3>!AKsWp2|G|K02%z* z4qQb=!(7jRZQ24`CIp3XG{~=Dox#kO%y|sapn?y~j|Q0!W(DzpDHH-*RS^veU9c)x z7KPD4!eC((0--uc1VsYOL6QX}4X_x3K#~PD&=C?~7Q7&0jFtuqFfcHLGKMk)vqa0l z_`$4F48d%{?7?#bJoPOYH0$g3KWV*$klA4xSnp0Bb0Lp`)q=3=C2Knf70wmSK z48__2fE9JZ3}KA$B94oJAzx65ArxML2Qc~azzZ@)coD^z#;}snPm}4EAUqAkLyDi` zqQwjh3@e$6KocOhn9}l!K`9uT#=x_cw-jt80 zV7Q`XeO=4xqL$MYEtkuZu9vi27C6mNnh`ip^`e%`B}vzxmn2<3g9@+dg4!(5J|?R1 zw~(eVK^5srcC1AkqErJHi~L2P1bB-ju_(Rx7IQkNBG1e#DFVfL5hz*QVlPfDF3!x) zgBS*iilTN-!)=A*7Uv7x(igZb8eH!1=&dPQU$wI8y1xBIefuMvm-W3a^LSt9 z3AxAc3K$)kK0>H33=GapKA;}5GgB}~5JY`uVBig5YH+>7 ztq3Yjc?ECqN!}4qyvgr+Ls0F8h{{KP4p7<20j;d7BoLV&+*{YT$;>OU%1p6CNsyp2 z1Y8vOv4C2uR4ED=qv17w5WFaWr*wuOc$*R-77c12fVr@YpT`s}2o?ZkK_-|e*5UxJ z7h7=<$^dGu!u5mv3o{lSF#)p>1S~y(T9#p4#mokfwk1CULn#vjL&2<2&R|w+Mh1oy zCI$vyUIvDUS@BH1f{-FOk2{Yuk1Lb|(r(P-31x;4Z3{3k1ha=S2Z3fx5SD>id<@1+ zdA$Bi!5n$K(V%G+uxK!6#N`OrP_|GWP%kNzJCsM5A&f7KznB@?+BIch$bW}5&2tBV zM(V+)AP6LX@uO5H|CJa*`C##iQl0PzbLlY!F!`E8d{@kz$DhX+%nfe|Gv{&Tai+mq z#r#kfSXCN>CXe4O9?)PRXk{3vlLoFMi~Yh^Pe-bsz%N!KxBQ zG}VGqlk!G5JAh&M<5t~5- zs0J$90%C!xlOk~K1FD*Cv883^l%y7c+wiwo!Sih3xy7O`kYZ5~0V*GhKm}D1s8?MC zD&8P=g3Iyv%oIq6U_VGLD73)6Yj6t|T(fHmfDJE#I38Y!$3yzGpr9z)1=6}3MC<|i zkQe4dh?0FEF~;m$jAgeZAg)bKiH9kH213zZkZw@F;}&yHetOXwP<;q$#eq8(NCdd$ zTC^Ud8bow})Lmu-Ezx=+EH&L`qRn+-<%`0~3;cFC9&x@Ptb9S(qrvBnn8fs~iCNdh z)GmstEvdXLX4v3=hg)|={$*~v>)fsvxm}NB9?vSbP)>%1Blc{MKcYBjjs;n7^91R5v0u4Q#m%W99wWi5xx zJdW3STrcvtUgmLca3w;8h}d+miC))5R4$6BED66XqI+G$;|l|WLIBexLywgrD+EtCeqdk- zWAq1A^uCP#Op93-FfL-bXy|c)Mdl+|;SD~81>zU^G#lKXv2))LQeBX;Jbz*S1y!>P z0_L5p4+Qi&Sf6l9U69t^V0uGIV@K!>Wvv@}4mUJRZfHAyWMXh(@@D$Nz@X{F^oaq? zxlmKTooyrAg^--44IK;A83+LrO0?mR->-{|M3W39S1g zFFTLa2L^Ulsh^((K_2+|Nru6Rv7GVeXRthY>D@7I9uG0LW72FMVr<7nlspvWj++U3 z$f%uQ=k!pNJ0ZjBp(t0yrVq}8*gGXB(3{4f<|(2W1G5IUQ|YMfbu(- zkEeS8F8XeS`EsapwLF?49MhwIs+ip z#h4udaJ~o4hBPp&(YdZ=dr`~wikAIlNry{X_K1Ff{Uu3%05{fp)A7i zLFixx*lvwHRx;cOs&v7o!(5Zc77dCAuvjP?Ocbvh*%6}wWVir6asaanMhD4*9f3k1 z#ue%v~@#j{~Uz-5AUUZ$NW~at5=5 zns9ksL^q|m!+0>7(tAR=gE>%|(tD7a(!6=xc|6hjU`H}AFpv@Qpq@ThJIqHg8f(bI zRR@8_1;F|c1Zv3V@garGBczbw59No3j6gJKhyiQ>fldalkm1A`GJ@ccF$WvYz`zj9 z70L?lW*9Iqgt7)%z~m4#Tm?S^LojzJYY=Ew9w7~8fjT1og1BXad5oF;8H0HvLL#a{ zc|*B`8N!6Zgy9_$a|VVyp-`b9&`dGdJOmNS8w8rGLkNIbuu?#nAxs$FCE;dZ$P*3~ zR$>Shg2h(=6R4vF9jq4)<^y+1Ea9CJ;XI*G&QQisfl$Fv0bzzP5i~c6go|7!Dh-6hKma42|?MYq9SPwngV{exDtztQ%gWS zlvG4#3f#~~X-|L#bU+=z&!82oc8tml3JjpJXki8q3DD>vwoM-)OjZ%lF0BY?%(IMP zDyss6uM{MV(;3W}(-^@m2k?RrP3BundIq-`OTj%2=pc0ixT$}O(KgG zRAzh$8b$(7v1oD^g@da5NDu*I$SE>|oGXZ&5bL)!;?Mpfm{Smx_W@OA*+A;La&G zDEy0|9aC_afkN*VH@IgG6$W)si$IfxMR!1M<%9=W(J_z}$3cQp@Lnoh7T&Nu3Q`3Q zK5&qNIv3#K0C11B=p-m5f&wZ5smlsyf)jKR$W34Z)Wt6bZEF3|!0<#+V!F&knd^dz z7X=j;=Hd4tOx;xsBO?mpL`zg31>Kl&=e@UldSZ!+%-8;xeb@2X+QlkuO{f zGWsj(PdHtc_P@X)aYI05LHI=h-3HG)#xB>5y)GJiT`~6g!oZ~D!gR^lXQjys!wUg{ z9~hXT7@e6uGB5=)Ix{U+S)jN`<)X3A1s1uFV3l9k7(`9C1Z|Jn7QaL^7xIf|l*})mS$@Ga{<>@0Mc1?| zt{ImNGcUPj>?qo(v>{}h#zohRi-wt3B(gp-fc1Z3V32TPx*=;i6;0700B@ddZg@lXe)bEZU&FuKc27(nY=GD`F`Z z3(7BvR@{)*y&?5RfT3E=3SL?JYr;m`*875XAc8N2*tUf|YRUG=zQ6iMu2vl~1YNXF6hIPljiUYi+ z$R|G?#rFfzw=V*h!=Mf?jxN0%xNi)NETjq=x|#9;s8j@X6&e`U=wH`zxTxiDMa${3 zr1K>$Cq$>->5`=L&r6cdkWRfIqEinw_ZECu5xh(UU;iFlo1oQUpw1;^at`bpjQ%|j zvLA|GF)%P(2RWOF9=^_+p!HEJqps`PUevYSlYUv(4KnhFm{^mzBOrg1-}#21$_)`^ z^d7z-ER|#L4Mnhg>79Iw)I?yE)T5H38RBR1IpkgG=e~=#y0Ja zIyZuC5+2qV&toLRrJ$i-uv1_z%ws|tBSXyJf$CbYIs^f?mzfNEK}{L3QE+=%kh=Hq zHETim>}5rC??DS2LK#5apa|hm)?j8!(8@Fxzbe=~26$?riVK$06_hnuAk98dL!t=W zgaM7nYw|%FNZ67zsL2Is2ps}7O~AugAlHGLYLHeL$W!3)d`&ipN|5QdxInEXXsZZh z(=9I0WI0p-WNR^~tbsIMAT1Zrfdxe%;wdO+K-QT(;X-X9xL?+Gxy#lcJM7W@J`j@B2cb_ zH2Oi+4I~&ri%*Kc;R`NHKxP&>g4&CG#f3SEImsERxs`hPMY%s%xRiW<<6Y>^a*1&YU8tfeKHImJcbxB|xzxGfDTx^8jAgSThJ#}|QymXH>%f=bd` zf}Vcpt!Z#W8a!?ZF5JLVMxX_Ox0s7dif*x_WG0t@%RbO7RcT2oco`CS;SYH60c1J_ zG^hz~mx2pFa6A`*;usu;-~@1s!zMRBr8FniuIMKN0|TfeDrRM3VEDky$jEq~!R|7H z-F=4O>kPq{8G>&!aNlK6z0aU_kwNV)gTQwt2F5U^&kP{)A-li?u^AGV*_9etKCrOx zvovsj;9?LIZs7gI!Ya(v!2gMlMU?3RgWyL#2GJzOn?go6h0SjY=-d?2zsc)!lUL)W zVCYT${F}VBHwB_@3L1Qr?ih1_onq}~)Py(v_7Q@r{kKL^O#4^j+#0w3AM7}*|h@qT3$V`Tf_zz7l& z6bB1^U=?R%`@qJ)!~cOD#NuG!6Z*gjVsSADiGJV)v3MAGMJFUr2%X`wKxC%Z1yQw& zJnA2KK{9*{{6b&(K`bK%Cbo8|MyU?xX8DgyjEpXfpP3n$1V4b8pV$}~?LM%ErV&Bz8)BJ}~p`XB|ByuhP&nNj^ClLVs_NLJ$mi1on)D%;_4nNbw3RSaFL%wUz`&5^;+`>!nStRkhy%k+P{wBm1_p+y48aV+jNS}hj75wJ48csn%-&31 z%tg#zEJZ9{tVOI|Y(;De48bf?3`Ok0tWpd`9Kmc-3`Ly5>{1LxTAQfPCGz*wwU|4b5hv?)|CR`ncE zMMxA(eGuGygk&^0BtiTj9uSF&VLDBrp{l~LlF?6-{T6$EK}lwQUU3n~#3EJ(28LT) z#ia$QMP-@A`9-%la`KZCK?;gMuD``wl%Jm)pH`Ze3=&((c#E~LG_|O*7~~CwTbj;R zF`$qvj&aV^wgqwP$a~c7N-`)7p3Na z?J3U4EGRA#VqjnZ1zfQW0|P??!wqhMe%nslI~+n&Os3mUw7<-u3}M?&w7txsc%4J% zB8SeJkn6hk7j^9~b2xlvW#(b~z`)GP_8AlkDDejhRghml%P@jc)xg9b*7%0UAIQxR z42w0a>hZ)f*7U&#F_VEINC3t{&@h`!LBYi=+is%WWez1!O0c{xta?#c^)iRrbq>Re9EMv|t{b^rG;+Dj;rf}A6_gfO zS=ow&85kI_r-erYn3u7}Ehs1ukppJIVhF2xJZS+vM%j^K6clH-SQ3j;6TvZeixVC_ zw^%{hvKS*Skuxs1FhI??vJ4Cipv+tBiXM$7(=8`jUgnSmMVARYy1=~Yh=??qZZ*;B zGKU<#SOYm4oP(PNFxC*|C?ub-B84ibh`Gg{oS#>cm|Sv;vpl~jJ06@*&@+YtQpNzO z%E?bh%NB|Z3=C4B?4*kxp5{o|!u-0h%0*$7%N(kRk`J8P{WQ66@f8>5B<3V%q~=!Y zC%v?=q;~XE3_VVDyQFg^}$O4;v%fCjmA_w$B1AjF1cn E03~QnU;qFB literal 0 HcmV?d00001 diff --git a/app.py b/app.py new file mode 100644 index 0000000..9b63deb --- /dev/null +++ b/app.py @@ -0,0 +1,6 @@ +from . import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..774536f --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from .. import db # If you're in an actual module +from .areas import Area +from .brands import Brand +from .items import Item +from .inventory import Inventory +from .room_functions import RoomFunction +from .users import User +from .work_log import WorkLog +from .rooms import Room \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37b766d8e4ee260358bd56ac4ad1385264bd0259 GIT binary patch literal 547 zcmey&%ge>Uz`(E}#v#Ljk%8echy%k+P{!vP1_p+y48aV+jNS}hj75wJ48csn%-&31 z%tg!!48bhHtllhMtVOI~Hk&t_7kd#qn9c6Z;l)|R31)M6b9r$Waf8{M-aKBsMZ91( zmp7jme-S^J&FwAVC0HZ~X7hLpc?lN@D=?(#ofNQ+7hpV%4W|pMp78h}X*qoU#8;d|;e2cFrKR-7<4Qgv~ z5jRK?YiV(6QE?Fuh|N)+Uz8o6lb>G13*xeZ)E5`=F)%Q!WcUmUfLq4SRxu!_7RNZ} z=M^QU==r7Q6zAu~z-@}j%}+_qDUOMc&&Uz`(FD+aaTyiGkrUhy%l{P{!v83=9lY8G;#t8NC_27>gLan2MOZn2VSd z7-E<;po$XN7#M{1ND95MVs3Q&WQC{tz#r${iJQxPv*=85kJAY8hg9lo*0JO+l__U|h zjNyyn2dM_(817(hunqwwhG3px-e5jUrXs-@&KRL!9*{f;$8ZPpgVY2INHG)%2MZR7 zqzh>Z-(qnrN=a`jb8-@sa#C-xv@#i>QbG0ypUMTseTeyKUd`FSy!d1a}2CHX~_F}e9E zsX4_lAfFcN6;$5hh>r&u79W3$7i6U#j4Q;zz)-Bhz`)SJa6?dPhRJ;MndUPz7nrXx z*}$@$b0g<5^4^PDKTs1(xR(xEm%VutcW*^6BAmsvs{ zD(S5#JCJr!$+y9^!)$`egtCb`7rEsxv-o~xVPv&00=ofZy`QGgEw+-%g3P@1BCzv8 z{wf0Hiz2X>z=@TsxG*O%CpjZEw-ORe-~a69xtbP|_+kU|?YQz|6?Vc%Q-GB7?y_W`Vmb($5(TFESY3 eXRy1-VE2?k;0p^UqkV@_zeT6TR|ZZ-h>rlDsbfR{ literal 0 HcmV?d00001 diff --git a/models/__pycache__/brands.cpython-313.pyc b/models/__pycache__/brands.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00aabd632d5629396b2fa054c519511bf2d1f2a4 GIT binary patch literal 1474 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%0Ay%Ug8VqcFADB2;Rc1JUX5e5|`OM6~#`hT{ z0M5JMgr%u=i>;)xATuw$2vi&wfzv8DLx4)NTWsLQYcaS%eG6Q77eiXItfj@NMa7U7 zE(fgr3TfH07Uk#X7DF1!T*ZYsi8;v`skxO!;G6?$`V@ilA1F10b3G)33xiU*2#5gZ zSWphTC5h0jmy(&7lbT#ote0C{T$ob?&gv4NOvHzzD8DEdTs}Yw4sfmv2dQBwk^xby z;9dmS-$kgI>=uU&q};MA%3xq%02R2!AQc~&85tSxGnigxFulhtaF<2)E{il6Dc)s~ x{?5X{=)v@z0YTnpFuTuScag#FHUs}t27xaumW(PBn5J_~
    9$*2Mjb^rsKf#(1K literal 0 HcmV?d00001 diff --git a/models/__pycache__/items.cpython-313.pyc b/models/__pycache__/items.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9fb40c6335476dc06378e6b91201cafc7bf2a47 GIT binary patch literal 1689 zcmey&%ge>Uz`#(D?vP=_%)sy%#DQT}DC6@41_p+y48aV+jNS}hj75xIOhrsy%tg!! z3^B|aP(_Js3=F|c!OY$)UaUo|3Lw=i-fUj%MeJT2MI2t7MVw$UR&OpZ?jmk4o+2JE z-XdNFkRCQ~J}>?vegy^z1|^1Ib}5Enju`$R1*pMDlqoZWQzV$qsmb*c#L;BF#p07$ zTyl%Uzn~;DKQA%o7H>#ofNQ+7hpV%=H22cO3g_G z*-@O4S)j>yizy{38RP^Q=7cgnUjYYK3_}rPFhdbjFk=z3Dnl?+5lalK5<@U^3|kDl z5<@UcFiSA2B~vh4FuNs75l0MX3>PT8W4MDk)EO8Uz-l>Tc$64|xlBP`U|?VfW((%F zWG>>3;f&#n;RmS(;TY~<9=Hx(tU3fRbnt<72r4lI^9KtA3tBQ23B_>62nQpZDg;s! zEG)%PBoZuAB$_U&DRzs+vm`b5B`C?=a3=9mnSTajeb2S-n@g*fDXU7-h7nJ5CmZTPgtcFH% zGBnJzENz+b~rAQp?*`m~f zq9SPq28JS8NN}+fr{<&;gM6rP%fQ(xCbT%Us5r(sKd&e;Mb9rar#L?^CKHz2V{-FT zQge!9KtWclS5SG2BR(EvVto89UXWY#U|eAa28LoA1_p)(h8u!XGfd{2&orN*xxj9V z%7&8b=I$5G-7gz?Trl)JA$=t@@>&4`7qCg@aT(bF%2FaX%ix5 zBu|XJ$ftanCFY^F`3~+2AyF5#qZ>Rs$|j`Du)Zj!evx0}GE4MV7Dm=wMu zy2VyfS&*5R4)G%>1d2cfL=o8E;C#bXT$q!Xlbn&7TL}qJaL|H$c8d=orI%ln3l3#S z@Poqc7DEvqD9l)WL5ZmdWFcBW-Quu;#IaqGIRgU&C~+1WF)%QEU}j`wyw6~8k-^{| rv%p;zmFEnG7a0uiGuT~ZuzSiN@P&noF_*E!sNbT~;wuA)4fYcN+JUGA literal 0 HcmV?d00001 diff --git a/models/__pycache__/room_functions.cpython-313.pyc b/models/__pycache__/room_functions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7b816fd70bf5ac36b41fbb9d7adbb519cec6650 GIT binary patch literal 1536 zcmey&%ge>Uz`(F3+aco!69dCz5C?`?p^VQH7#J9)G6XXOGkP<4F%~g;F%>a+F&8l_ zFvKuxKoupjF)#!(1v7iIc(E3-Du7h8c(Zx27qNSB6mfWQ7IA{bSiQNtxQn>Gc#3$u zc#C)yKzi7``Mmgx_!Sr=7?c=-*`*kQIb!&O6rctpQKrlgPLW_brzY1+5J!{w7K=}2 zamg(X|ALat{Jg}RTf8BW0j}}R9)knJ$c31xg<1P-Yfh9bsbh9ah5#v*1_hG3>5mKatghG6CxwitFLhG3Rp zmS9#(reL;Uc1xBbju_4uE>KX%a0hd!GcYiK)pEx0C@}yQV48gp?e8K#dOhtk*oH0VdydZfHj^Pd#0I3NUlwv3n4i+jB zNf*`>xy1tt4!6?0WRS03g3>@0A4o_6CRTil$oDTO4_%IXQ_*IjOg}Q&NkQi!#Ac zRK(7}z;KJVC_g_pJ`L&uO~zY%Nr}nX@dfz>r8$Wusl~TgK?;hKp#cC6V32D-N#L^| zIGBSOLm7g=5~u_pLoib`NF7)>lp%-(%s?Sv0n5h_%xui6!l23GS0!iz3IUiWG%{0c ztN3&jkX&wC#iVJc$y_7`@&HRwYC%zv6vU@2#i==I#UQsT+_H4GiU}=FEh>(2&d)1K zOwsd8%_+{$i^dIgoYIO5|$rpCwL5=C~39!yw> zfq|hIl;av0ZU`zZFj;QC(0qaB3cEcjJ4&wG`(L#8zibt7!7}hdVDJV0kP93kpFy5p z$>66c3Nkw{F*h|n{uWn!d~SY9X%2|Z6CYn#nwSHX;g63`%PcA`$;r%1&C8FEzr`CL zUjm9ihSrQ0+2H{6i(Z6=etVFDivKxOSLLP?=CRQRgDJ z{AHG~uPltLp^QafSAZPgrzv!ct)#LbGcUae>;aI^ia;5y2<$0v+U6=Q%t_2i&PdIz zgai~g$Ux4%#fOm6%P-0WhaMzYK>>4%p@@G6cJ!KI1!otZI$~eJjy2V6` KuM8kI*iQh_-H4n3 literal 0 HcmV?d00001 diff --git a/models/__pycache__/rooms.cpython-313.pyc b/models/__pycache__/rooms.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffd96590ce1a0d4b34130206847668439218bdaf GIT binary patch literal 2669 zcmey&%ge>Uz`#)S$}xk9je+4Yhy%l{P{!vS3=9lY8G;#t8NC_27>gLan2MOZn2VSd z7-E<-po$VD7#MWrlEyB+_{_d0&D!n#{L2{0mAl^YapOZt;dh2DrvMd$>A#d-}QG zV)4l=E_unwz`&r%c#FlcC^b=&@fJ@|etxc7XI@7F zV6_Y}JW33~oTeb}GB7X%vjuZmG8gg2aK`Y(@Pkx?a13`aH(Uo#3|owV5<@UAObbg*2JOuD?L!Yvk1`gqC9z`$^e6~rvQ#pLNy#0E-4Ot)Bz zOA?DpZgFPjB^RaUrskD|Xfod7E-1>(O)RR6&rYqp#gv)y@;^v1<1LQ7(wv;cq@2`S zEP08!sYUDz3=CBqpk!FA=jn2bJ+UY?F+MZp76&XfRtbVERDf~8s<_jjNfo4$wJ1M7 zw^)<$7GF|ga&~+{enDwYVo7T8Ef$bjMI0bU^McgG!*msa651_}ocv^PY6Q6)n%av% z<-;x3(&E&j;$&!|0fz=i2PmI?Zi18!jG+ucUT8l(;^9Lf;H0%o8Puq49A z5X>CP5CjTD6nO>)K89eHP=+9o%Q0nGjX6~qG}-*BIBh@yqmh|nTg9%U0P>)16_<_z z$kXv4K|UP?n1{i9CQUm{)*@+8aIh4m78Di9fr>(T5TOW(80Q$KT?LkI&6dDa}cZkH5teA75CSm;;sJkB?8wEGjO^$;?a5%LgTj`1le~ zzK6&c34lBWDo~1eKrB$QEaC@wh&eN_&;S`ny-D*{DqkuJz=j)J26g4Cjt$|6u1cS|%rz8GXh za(rS*Nl|7}X-O)`E_09>Am4&RxJU%V1rZ?E77H;lFtjt=5Rq>1eJG~9z-5K{MvDWA zCkihb2VRIzz9^Q`;MBp^QQXOWnI+|>lKuv^o$Lpgck`Smxu_J<;L@QsL1KpTMA?g6 z@|RgcZmJq>Fxz2%(Cb9iWtXVSs?iPZ9bpqZW&}_4y~wL{nI-zBrrCz71Lh~aE`(=Y z_Q}4inbY7sL2L%c0^ymw7X{TWv*bKfHQb@G?fT@*IH%#w9e+k6N2g^;+5+VKsZ9c2?zW>{YoQ@_ZsahWCl zrjp)@vIAKcl>!@FJIp4iOemYEbCFyAGE3km7Dm=`#t&=^Y zi31!`ewvcE*h(r3GV{{GQB?#g0m1QbixpHf6@yFsTYR8m1708(gG*^hlz~cGNR)xg zVy@!CoWz{ujMUspNQ8rv3n(hUY2p?iLS8SwC>NZDAc+f{9t=T|Cj^RnR$owg1$JW* zY7F1vuz@6EyCM$;1_n_1S8U0^!0>^Yk&*E}gTX}xgL}*ZcUh$Gvgq7pQMt<^{hYz} jGK1}X2D^(4c25}uzOaZe>U9|PTXb4{We{Q11BV6xU`2n& literal 0 HcmV?d00001 diff --git a/models/__pycache__/users.cpython-313.pyc b/models/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08122928256a9c5037d39b1272620d705822f779 GIT binary patch literal 3228 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 literal 0 HcmV?d00001 diff --git a/models/areas.py b/models/areas.py new file mode 100644 index 0000000..a3ebfe9 --- /dev/null +++ b/models/areas.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .rooms import Room + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Area(db.Model): + __tablename__ = 'Areas' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Area", Unicode(255), nullable=True) + + rooms: Mapped[List['Room']] = relationship('Room', back_populates='area') + + def __repr__(self): + return f"" diff --git a/models/brands.py b/models/brands.py new file mode 100644 index 0000000..168894e --- /dev/null +++ b/models/brands.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Brand(db.Model): + __tablename__ = 'Brands' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Brand", Unicode(255), nullable=True) + + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='brand') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/inventory.py b/models/inventory.py new file mode 100644 index 0000000..ffca1d5 --- /dev/null +++ b/models/inventory.py @@ -0,0 +1,73 @@ +from typing import Any, List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .brands import Brand + from .items import Item + from .users import User + from .work_log import WorkLog + from .rooms import Room + +from sqlalchemy import Boolean, ForeignKeyConstraint, ForeignKey, Identity, Index, Integer, PrimaryKeyConstraint, String, Unicode, text +from sqlalchemy.dialects.mssql import DATETIME2, MONEY +from sqlalchemy.orm import Mapped, mapped_column, relationship +import datetime + +from . import db + +class Inventory(db.Model): + __tablename__ = 'Inventory' + __table_args__ = ( + Index('Inventory$Bar Code', 'Bar Code'), + ) + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + timestamp: Mapped[datetime.datetime] = mapped_column('Date Entered', DATETIME2) + condition: Mapped[str] = mapped_column('Working Condition', Unicode(255)) + needed: Mapped[str] = mapped_column("Needed", Unicode(255)) + type_id: Mapped[int] = mapped_column('Item Type', Integer, ForeignKey("Items.ID")) + inventory_name: Mapped[Optional[str]] = mapped_column('Inventory #', Unicode(255)) + serial: Mapped[Optional[str]] = mapped_column('Serial #', Unicode(255)) + model: Mapped[Optional[str]] = mapped_column('Model #', Unicode(255)) + notes: Mapped[Optional[str]] = mapped_column('Notes', Unicode(255)) + owner_id = mapped_column('Owner', Integer, ForeignKey('Users.ID')) + brand_id: Mapped[Optional[int]] = mapped_column("Brand", Integer, ForeignKey("Brands.ID")) + # Photo: Mapped[Optional[str]] = mapped_column(String(8000)) Will be replacing with something that actually works. + location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("Rooms.ID")) + barcode: Mapped[Optional[str]] = mapped_column('Bar Code', Unicode(255)) + shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) + + location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory') + owner = relationship('User', back_populates='inventory') + brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory') + item: Mapped['Item'] = relationship('Item', back_populates='inventory') + work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='work_item') + + def __repr__(self): + parts = [f"id={self.id}"] + + if self.inventory_name: + parts.append(f"name={repr(self.inventory_name)}") + + if self.item: + parts.append(f"item={repr(self.item.description)}") + + if self.notes: + parts.append(f"notes={repr(self.notes)}") + + if self.owner: + parts.append(f"owner={repr(self.owner.full_name)}") + + if self.location: + parts.append(f"location={repr(self.location.full_name)}") + + return f"" + + @property + def identifier(self) -> str: + if self.inventory_name: + return f"Name: {self.inventory_name}" + elif self.barcode: + return f"Bar: {self.barcode}" + elif self.serial: + return f"Serial: {self.serial}" + else: + return f"ID: {self.id}" diff --git a/models/items.py b/models/items.py new file mode 100644 index 0000000..0e0ae0e --- /dev/null +++ b/models/items.py @@ -0,0 +1,20 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Item(db.Model): + __tablename__ = 'Items' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + description: Mapped[Optional[str]] = mapped_column("Description", Unicode(255), nullable=True) + category: Mapped[Optional[str]] = mapped_column("Category", Unicode(255), nullable=True) + + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='item') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/room_functions.py b/models/room_functions.py new file mode 100644 index 0000000..8683174 --- /dev/null +++ b/models/room_functions.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .rooms import Room + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class RoomFunction(db.Model): + __tablename__ = 'Room Functions' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + description: Mapped[Optional[str]] = mapped_column("Function", Unicode(255), nullable=True) + + rooms: Mapped[List['Room']] = relationship('Room', back_populates='room_function') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/rooms.py b/models/rooms.py new file mode 100644 index 0000000..9323535 --- /dev/null +++ b/models/rooms.py @@ -0,0 +1,34 @@ +from typing import Optional, TYPE_CHECKING, List +if TYPE_CHECKING: + from .areas import Area + from .room_functions import RoomFunction + from .inventory import Inventory + from .users import User + +from sqlalchemy import ForeignKey, Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Room(db.Model): + __tablename__ = 'Rooms' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Room", Unicode(255), nullable=True) + area_id: Mapped[Optional[int]] = mapped_column("Area", Integer, ForeignKey("Areas.ID")) + function_id: Mapped[Optional[int]] = mapped_column("Function", Integer, ForeignKey("Room Functions.ID")) + + area: Mapped[Optional['Area']] = relationship('Area', back_populates='rooms') + room_function: Mapped[Optional['RoomFunction']] = relationship('RoomFunction', back_populates='rooms') + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location') + users: Mapped[List['User']] = relationship('User', back_populates='location') + + def __repr__(self): + return f"" + + @property + def full_name(self): + name = self.name or "" + func = self.room_function.description if self.room_function else "" + return f"{name} - {func}".strip(" -") + diff --git a/models/users.py b/models/users.py new file mode 100644 index 0000000..7df3c17 --- /dev/null +++ b/models/users.py @@ -0,0 +1,36 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + from .rooms import Room + from .work_log import WorkLog + +from sqlalchemy import Boolean, ForeignKey, Identity, Integer, Unicode, text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class User(db.Model): + __tablename__ = 'Users' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + staff: Mapped[Optional[bool]] = mapped_column("Staff", Boolean, server_default=text('((0))')) + active: Mapped[Optional[bool]] = mapped_column("Active", Boolean, server_default=text('((0))')) + last_name: Mapped[Optional[str]] = mapped_column("Last", Unicode(255), nullable=True) + first_name: Mapped[Optional[str]] = mapped_column("First", Unicode(255), nullable=True) + location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("Rooms.ID")) + supervisor_id: Mapped[Optional[int]] = mapped_column("Supervisor", Integer, ForeignKey("Users.ID")) + + supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates') + subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor') + + work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='contact') + location: Mapped[Optional['Room']] = relationship('Room', back_populates='users') + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner') + + @property + def full_name(self) -> str: + return f"{self.first_name or ''} {self.last_name or ''}".strip() + + def __repr__(self): + return f"" diff --git a/models/work_log.py b/models/work_log.py new file mode 100644 index 0000000..fbea696 --- /dev/null +++ b/models/work_log.py @@ -0,0 +1,35 @@ +from typing import Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + from .inventory import User + +from sqlalchemy import Boolean, ForeignKeyConstraint, Identity, Integer, ForeignKey, Unicode, text +from sqlalchemy.dialects.mssql import DATETIME2 +from sqlalchemy.orm import Mapped, mapped_column, relationship +import datetime + +from . import db + +class WorkLog(db.Model): + __tablename__ = 'Work Log' + __table_args__ = ( + ForeignKeyConstraint(['Work Item'], ['Inventory.ID'], name='Work Log$InventoryWork Log'), + ) + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + start_time: Mapped[Optional[datetime.datetime]] = mapped_column('Start Timestamp', DATETIME2) + end_time: Mapped[Optional[datetime.datetime]] = mapped_column('End Timestamp', DATETIME2) + notes: Mapped[Optional[str]] = mapped_column('Description & Notes', Unicode()) + complete: Mapped[Optional[bool]] = mapped_column("Complete", Boolean, server_default=text('((0))')) + followup: Mapped[Optional[bool]] = mapped_column('Needs Follow-Up', Boolean, server_default=text('((0))')) + contact_id: Mapped[Optional[int]] = mapped_column('Point of Contact', Integer, ForeignKey("Users.ID")) + analysis: Mapped[Optional[bool]] = mapped_column('Needs Quick Analysis', Boolean, server_default=text('((0))')) + work_item_id: Mapped[Optional[int]] = mapped_column("Work Item", Integer, ForeignKey("Inventory.ID")) + + work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs') + contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') + + def __repr__(self): + return f"" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8d2de2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +flask_sqlalchemy +pyodbc diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..f181b50 --- /dev/null +++ b/routes.py @@ -0,0 +1,236 @@ +from flask import Blueprint, render_template, url_for, request +from .models import Area, Brand, Item, Inventory, RoomFunction, User, WorkLog, Room +import html +from sqlalchemy.orm import joinedload +from typing import Callable, Any, List +from . import db +from .utils import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_room_relationships, eager_load_worklog_relationships + +main = Blueprint('main', __name__) + +checked_box = ''' + + + +''' +unchecked_box = '' + +ACTIVE_STATUSES = [ + "Working", + "Deployed", + "Partially Inoperable", + "Unverified" +] + +INACTIVE_STATUSES = [ + "Inoperable", + "Removed", + "Disposed" +] + +inventory_headers = { + "Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None}, + "Identifier": lambda i: {"text": i.identifier}, + "Inventory #": lambda i: {"text": i.inventory_name}, + "Serial #": lambda i: {"text": i.serial}, + "Bar Code #": lambda i: {"text": i.barcode}, + "Brand": lambda i: {"text": i.brand.name} if i.brand else {"text": None}, + "Model": lambda i: {"text": i.model}, + "Item Type": lambda i: {"text": i.item.description} if i.item else {"text": None}, + "Shared?": lambda i: {"text": i.shared, "type": "bool", "html": checked_box if i.shared else unchecked_box}, + "Owner": lambda i: {"text": i.owner.full_name, "url": url_for("main.user", id=i.owner.id)} if i.owner else {"text": None}, + "Location": lambda i: {"text": i.location.full_name} if i.location else {"Text": None}, + "Condition": lambda i: {"text": i.condition}, + # "Notes": lambda i: {"text": i.notes} +} + +user_headers = { + "Last Name": lambda i: {"text": i.last_name}, + "First Name": lambda i: {"text": i.first_name}, + "Supervisor": lambda i: {"text": i.supervisor.full_name, "url": url_for("main.user", id=i.supervisor.id)} if i.supervisor else {"text": None}, + "Location": lambda i: {"text": i.location.full_name} if i.location else {"text": None}, + "Staff?": lambda i: {"text": i.staff, "type": "bool", "html": checked_box if i.staff else unchecked_box}, + "Active?": lambda i: {"text": i.active, "type": "bool", "html": checked_box if i.active else unchecked_box} +} + +worklog_headers = { + "Contact": lambda i: {"text": i.contact.full_name, "url": url_for("main.user", id=i.contact.id)} if i.contact else {"Text": None}, + "Work Item": lambda i: {"text": i.work_item.identifier, "url": url_for('main.inventory_item',id=i.work_item.id)} if i.work_item else {"text": None}, + "Start Time": lambda i: {"text": i.start_time.strftime("%Y-%m-%d")}, + "End Time": lambda i: {"text": i.end_time.strftime("%Y-%m-%d")} if i.end_time else {"text": None}, + "Complete?": lambda i: {"text": i.complete, "type": "bool", "html": checked_box if i.complete else unchecked_box}, + "Follow Up?": lambda i: {"text": i.followup, "type": "bool", "html": checked_box if i.followup else unchecked_box}, + "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box}, +} + +def make_paginated_data( + query, + page: int, + per_page=15 +): + model = query.column_descriptions[0]['entity'] + items = ( + query.order_by(model.id) + .limit(per_page) + .offset((page - 1) * per_page) + .all() + ) + has_next = len(items) == per_page + has_prev = page > 1 + total_items = query.count() + total_pages = (total_items + per_page - 1) // per_page + return { + "items": items, + "has_next": has_next, + "has_prev": has_prev, + "total_pages": total_pages, + "page": page + } + +def render_paginated_table( + query, + page: int, + title: str, + headers: dict, + entry_route: str, + row_fn: Callable[[Any], List[dict]], + endpoint: str, + per_page=15 +): + data = make_paginated_data(query, page, per_page) + return render_template( + "table.html", + header=headers.keys(), + rows=[{"id": item.id, "cells": row_fn(item)} for item in data['items']], + title=title, + has_next=data['has_next'], + has_prev=data['has_prev'], + page=page, + endpoint=endpoint, + total_pages=data['total_pages'], + headers=headers, + entry_route=entry_route + ) + +@main.route("/") +def index(): + return render_template("index.html", title="Inventory Manager") + +def link(text, endpoint, **values): + return {"text": text, "url": url_for(endpoint, **values)} + +@main.route("/inventory") +def list_inventory(): + page = request.args.get('page', default=1, type=int) + query = eager_load_inventory_relationships(db.session.query(Inventory)).order_by(Inventory.inventory_name, Inventory.barcode, Inventory.serial) + return render_paginated_table( + query=query, + page=page, + title="Inventory", + headers=inventory_headers, + row_fn=lambda i: [fn(i) for fn in inventory_headers.values()], + endpoint="main.list_inventory", + entry_route="inventory_item" + ) + +@main.route("/inventory_item/") +def inventory_item(id): + worklog_page = request.args.get("worklog_page", default=1, type=int) + inventory_query = db.session.query(Inventory) + item = eager_load_inventory_relationships(inventory_query).filter(Inventory.id == id).first() + brands = db.session.query(Brand).all() + users = eager_load_user_relationships(db.session.query(User)).all() + rooms = eager_load_room_relationships(db.session.query(Room)).all() + worklog_query = db.session.query(WorkLog).filter(WorkLog.work_item_id == id) + worklog_pagination = make_paginated_data(worklog_query, worklog_page, 5) + filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Work Item', 'Contact', 'Follow Up?', 'Quick Analysis?']} + worklog = worklog_pagination['items'] + + if item: + title = f"Inventory Record - {item.identifier}" + else: + title = "Inventory Record - Not Found" + + return render_template("inventory.html", title=title, item=item, + brands=brands, users=users, rooms=rooms, + worklog=worklog, + worklog_pagination=worklog_pagination, + worklog_page=worklog_page, + worklog_headers=filtered_worklog_headers, + worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog], + ) + +@main.route("/users") +def list_users(): + page = request.args.get('page', default=1, type=int) + query = eager_load_user_relationships(db.session.query(User)) + return render_paginated_table( + query=query, + page=page, + title="Users", + headers=user_headers, + row_fn=lambda i: [fn(i) for fn in user_headers.values()], + endpoint="main.list_users", + entry_route="user" + ) + +@main.route("/user/") +def user(id): + asset_page = request.args.get("asset_page", default=1, type=int) + worklog_page = request.args.get("worklog_page", default=1, type=int) + users_query = db.session.query(User) + users = eager_load_user_relationships(users_query).all() + user = next((u for u in users if u.id == id), None) + rooms_query = db.session.query(Room) + rooms = eager_load_room_relationships(rooms_query).all() + inventory_query = ( + eager_load_inventory_relationships(db.session.query(Inventory)) + .filter(Inventory.owner_id == id) + .filter(Inventory.condition.in_(ACTIVE_STATUSES)) + ) + inventory_pagination = make_paginated_data(inventory_query, asset_page, 10) + inventory = inventory_pagination['items'] + filtered_inventory_headers = {k: v for k, v in inventory_headers.items() if k not in ['Date Entered', 'Inventory #', 'Serial #', + 'Bar Code #', 'Condition', 'Owner', 'Notes', + 'Brand', 'Model', 'Shared?', 'Location']} + worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.contact_id == id) + worklog_pagination = make_paginated_data(worklog_query, worklog_page, 10) + worklog = worklog_pagination['items'] + filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Contact', 'Follow Up?', 'Quick Analysis?']} + return render_template( + "user.html", + title=(f"User Record - {user.full_name}" if user.active else f"User Record - {user.full_name} (Inactive)") if user else "User Record - Record Not Found", + user=user, users=users, rooms=rooms, assets=inventory, + inventory_headers=filtered_inventory_headers, + inventory_rows=[{"id": item.id, "cells": [fn(item) for fn in filtered_inventory_headers.values()]} for item in inventory], + inventory_pagination=inventory_pagination, + asset_page=asset_page, + worklog=worklog, + worklog_headers=filtered_worklog_headers, + worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog], + worklog_pagination=worklog_pagination, + worklog_page=worklog_page + ) + +@main.route("/worklog") +def list_worklog(page=1): + page = request.args.get('page', default=1, type=int) + query = eager_load_worklog_relationships(db.session.query(WorkLog)) + return render_paginated_table( + query=query, + page=page, + title="Work Log", + headers=worklog_headers, + row_fn=lambda i: [fn(i) for fn in worklog_headers.values()], + endpoint="main.list_worklog", + entry_route="worklog_entry" + ) + +@main.route("/worklog/") +def worklog_entry(id): + log = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.id == id).first() + user_query = db.session.query(User) + users = eager_load_user_relationships(user_query).all() + item_query = db.session.query(Inventory) + items = eager_load_inventory_relationships(item_query).all() + return render_template("worklog.html", title=f"Work Log #{id}", log=log, users=users, items=items) \ No newline at end of file diff --git a/templates/_table_fragment.html b/templates/_table_fragment.html new file mode 100644 index 0000000..7b400a8 --- /dev/null +++ b/templates/_table_fragment.html @@ -0,0 +1,75 @@ +{% macro render_table(headers, rows, entry_route=None, title=None) %} +
    + + {% if title %} + + {% endif %} + + + {% for h in headers %} + + {% endfor %} + + + + {% for row in rows %} + + {% for cell in row.cells %} + + {% endfor %} + + {% endfor %} + +
    {{ title }}
    {{ h }}
    + {% if cell.type == 'bool' %} + {{ cell.html | safe }} + {% elif cell.url %} + {{ cell.text }} + {% else %} + {{ cell.text or '-' }} + {% endif %} +
    +
    +{% endmacro %} + +{% macro render_pagination(endpoint, page, has_prev, has_next, total_pages, page_variable='page', extra_args={}) %} +{% set prev_args = extra_args.copy() %} +{% set next_args = extra_args.copy() %} +{% set first_args = extra_args.copy() %} +{% set last_args = extra_args.copy() %} +{% set _ = prev_args.update({page_variable: page - 1}) %} +{% set _ = next_args.update({page_variable: page + 1}) %} +{% set _ = first_args.update({page_variable: 1}) %} +{% set _ = last_args.update({page_variable: total_pages}) %} + +
    + + +
    +
    Page {{ page }} of {{ total_pages }}
    +
    + {% for number in range(page - 2, page + 3) if number > 0 and number <= total_pages %} {% set + args=extra_args.copy() %} {% set _=args.update({page_variable: number}) %} + {{ number }} + + {% endfor %} +
    +
    + +
    + Next > + Last » +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9f22500 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,21 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +

    Table of Contents

    +
    +
    + Inventory +
    +
    + Users +
    +
    + Work Log +
    +
    +{% endblock %} diff --git a/templates/inventory.html b/templates/inventory.html new file mode 100644 index 0000000..ce13946 --- /dev/null +++ b/templates/inventory.html @@ -0,0 +1,127 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + + + + {% for brand in brands %} + + {% endfor %} + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + + + + {% for user in users %} + + {% endfor %} + +
    +
    + + + + + {% for room in rooms %} + + {% endfor %} + +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    +
    + + +
    + {% if worklog %} +
    + {{ tables.render_table(worklog_headers, worklog_rows, 'worklog_entry', 'Work Log') }} + {% if worklog_pagination['total_pages'] > 1 %} + {{ tables.render_pagination( + page=worklog_pagination['page'], + has_prev=worklog_pagination['has_prev'], + has_next=worklog_pagination['has_next'], + total_pages=worklog_pagination['total_pages'], + endpoint='main.inventory_item', + page_variable='worklog_page', + extra_args={'id': item.id, 'worklog_page': worklog_page} + ) }} + {% endif %} +
    + {% endif %} +
    +
    +
    +{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..981c2b0 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,53 @@ + + + + + + + + {% block title %}Inventory{% endblock %} + + + + + +
    +

    {{ title }}

    +
    +
    {% block content %}{% endblock %}
    + + + + + + \ No newline at end of file diff --git a/templates/table.html b/templates/table.html new file mode 100644 index 0000000..3774e75 --- /dev/null +++ b/templates/table.html @@ -0,0 +1,20 @@ + +{% extends "layout.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + + +{% import "_table_fragment.html" as tables %} +{{ tables.render_table(header, rows, entry_route) }} +{{ tables.render_pagination(endpoint, page, has_prev, has_next, total_pages) }} +{% endblock %} \ No newline at end of file diff --git a/templates/user.html b/templates/user.html new file mode 100644 index 0000000..f6c5ebb --- /dev/null +++ b/templates/user.html @@ -0,0 +1,130 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + +
    +
    +
    +
    + + +
    + +
    + + +
    +
    + +
    +
    + + + + + {% for supervisor in users %} + + {% endfor %} + +
    + +
    + + + + + {% for location in rooms %} + + {% endfor %} + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + {% if inventory_rows %} +
    +
    + {{ tables.render_table(inventory_headers, inventory_rows, 'inventory_item', title='Assets') }} +
    +
    + {% endif %} + {% if worklog_rows %} +
    +
    + {{ tables.render_table(worklog_headers, worklog_rows, 'worklog_entry', title='Work Done') }} +
    +
    + {% endif %} +
    +
    + {% if inventory_pagination['total_pages'] > 1 %} +
    + {{ tables.render_pagination( + page=inventory_pagination['page'], + has_prev=inventory_pagination['has_prev'], + has_next=inventory_pagination['has_next'], + total_pages=inventory_pagination['total_pages'], + endpoint='main.user', + page_variable='asset_page', + extra_args={'id': user.id, 'worklog_page': worklog_page} + ) }} +
    + {% endif %} + {% if worklog_pagination['total_pages'] > 1 %} +
    + {{ tables.render_pagination( + page=worklog_pagination['page'], + has_prev=worklog_pagination['has_prev'], + has_next=worklog_pagination['has_next'], + total_pages=worklog_pagination['total_pages'], + endpoint='main.user', + page_variable='worklog_page', + extra_args={'id': user.id, 'worklog_page': worklog_page} + ) }} +
    + {% endif %} +
    +
    +{% endblock %} + +{% block script %} +document.getElementById('supervisor').addEventListener('input', function() { + const input = this.value; + const options = document.querySelectorAll('#supervisorList option'); + let foundId = ''; + options.forEach(option => { + if (option.value === input) { + foundId = option.dataset.id; + } + }) + document.getElementById('supervisorId').value = foundId; +}); +{% endblock %} \ No newline at end of file diff --git a/templates/worklog.html b/templates/worklog.html new file mode 100644 index 0000000..e8376f5 --- /dev/null +++ b/templates/worklog.html @@ -0,0 +1,60 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..91f5720 --- /dev/null +++ b/utils.py @@ -0,0 +1,30 @@ +from sqlalchemy.orm import joinedload +from .models import User, Room, Inventory, WorkLog + +def eager_load_user_relationships(query): + return query.options( + joinedload(User.supervisor), + joinedload(User.location).joinedload(Room.room_function) + ) + +def eager_load_inventory_relationships(query): + return query.options( + joinedload(Inventory.owner), + joinedload(Inventory.brand), + joinedload(Inventory.item), + joinedload(Inventory.location).joinedload(Room.room_function) + ) + +def eager_load_room_relationships(query): + return query.options( + joinedload(Room.area), + joinedload(Room.room_function), + joinedload(Room.inventory), + joinedload(Room.users) + ) + +def eager_load_worklog_relationships(query): + return query.options( + joinedload(WorkLog.contact), + joinedload(WorkLog.work_item) + )