Merge branch 'master' into pui-add-startpage-to-admin

This commit is contained in:
Matthias Mair 2024-08-26 23:23:17 +02:00 committed by GitHub
commit 4e7fca7009
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 3976 additions and 1926 deletions

View File

@ -13,10 +13,11 @@ permissions:
contents: read
jobs:
build:
synchronize-with-crowdin:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -39,16 +40,29 @@ jobs:
apt-dependency: gettext
- name: Make Translations
run: invoke translate
- name: Commit files
- name: Remove compiled static files
run: rm -rf src/backend/InvenTree/static
- name: Remove all local changes that are not *.po files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git checkout -b l10_local
git add "*.po"
git commit -m "updated translation base"
- name: Push changes
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
git add src/backend/InvenTree/locale/en/LC_MESSAGES/django.po src/frontend/src/locales/en/messages.po
git commit -m "add translations"
git reset --hard
git reset HEAD~
- name: crowdin action
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # pin@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: l10
force: true
upload_sources: true
upload_translations: false
download_translations: true
localization_branch_name: l10_crowdin
create_pull_request: true
pull_request_title: 'New Crowdin updates'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'l10'
pull_request_labels: 'translations'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@ -17,6 +17,7 @@ gunicorn>=22.0.0
# LDAP required packages
django-auth-ldap # Django integration for ldap auth
python-ldap # LDAP auth support
django<5.0 # Force lower to match main project
# Upgraded python package installer
uv

View File

@ -7,14 +7,16 @@ asgiref==3.8.1 \
django==4.2.15 \
--hash=sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30 \
--hash=sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a
# via django-auth-ldap
# via
# -r contrib/container/requirements.in
# django-auth-ldap
django-auth-ldap==4.8.0 \
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
--hash=sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738
# via -r contrib/container/requirements.in
gunicorn==22.0.0 \
--hash=sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9 \
--hash=sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via -r contrib/container/requirements.in
invoke==2.2.0 \
--hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 \
@ -44,86 +46,74 @@ mysqlclient==2.2.4 \
--hash=sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54 \
--hash=sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab
# via -r contrib/container/requirements.in
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via
# gunicorn
# mariadb
psycopg[binary, pool]==3.1.18 \
--hash=sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b \
--hash=sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e
psycopg[binary, pool]==3.2.1 \
--hash=sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7 \
--hash=sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175
# via -r contrib/container/requirements.in
psycopg-binary==3.1.18 \
--hash=sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7 \
--hash=sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c \
--hash=sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31 \
--hash=sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5 \
--hash=sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e \
--hash=sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4 \
--hash=sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404 \
--hash=sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7 \
--hash=sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46 \
--hash=sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57 \
--hash=sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee \
--hash=sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834 \
--hash=sha256:3c7afcd6f1d55992f26d9ff7b0bd4ee6b475eb43aa3f054d67d32e09f18b0065 \
--hash=sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686 \
--hash=sha256:3e7ce4d988112ca6c75765c7f24c83bdc476a6a5ce00878df6c140ca32c3e16d \
--hash=sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804 \
--hash=sha256:4575da95fc441244a0e2ebaf33a2b2f74164603341d2046b5cde0a9aa86aa7e2 \
--hash=sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143 \
--hash=sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722 \
--hash=sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82 \
--hash=sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad \
--hash=sha256:59701118c7d8842e451f1e562d08e8708b3f5d14974eefbce9374badd723c4ae \
--hash=sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a \
--hash=sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f \
--hash=sha256:639dd78ac09b144b0119076783cb64e1128cc8612243e9701d1503c816750b2e \
--hash=sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83 \
--hash=sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd \
--hash=sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09 \
--hash=sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73 \
--hash=sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d \
--hash=sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68 \
--hash=sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773 \
--hash=sha256:812726266ab96de681f2c7dbd6b734d327f493a78357fcc16b2ac86ff4f4e080 \
--hash=sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299 \
--hash=sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1 \
--hash=sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c \
--hash=sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d \
--hash=sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5 \
--hash=sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c \
--hash=sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5 \
--hash=sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe \
--hash=sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921 \
--hash=sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1 \
--hash=sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a \
--hash=sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d \
--hash=sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d \
--hash=sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741 \
--hash=sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c \
--hash=sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69 \
--hash=sha256:c76659ae29a84f2c14f56aad305dd00eb685bd88f8c0a3281a9a4bc6bd7d2aa7 \
--hash=sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38 \
--hash=sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c \
--hash=sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970 \
--hash=sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679 \
--hash=sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85 \
--hash=sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30 \
--hash=sha256:dea4a59da7850192fdead9da888e6b96166e90608cf39e17b503f45826b16f84 \
--hash=sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008 \
--hash=sha256:e1cf59e0bb12e031a48bb628aae32df3d0c98fd6c759cb89f464b1047f0ca9c8 \
--hash=sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e \
--hash=sha256:e262398e5d51563093edf30612cd1e20fedd932ad0994697d7781ca4880cdc3d \
--hash=sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5 \
--hash=sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414 \
--hash=sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c \
--hash=sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54
psycopg-binary==3.2.1 \
--hash=sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51 \
--hash=sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea \
--hash=sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9 \
--hash=sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1 \
--hash=sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938 \
--hash=sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3 \
--hash=sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9 \
--hash=sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788 \
--hash=sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2 \
--hash=sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db \
--hash=sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7 \
--hash=sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46 \
--hash=sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674 \
--hash=sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e \
--hash=sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f \
--hash=sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805 \
--hash=sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489 \
--hash=sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879 \
--hash=sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42 \
--hash=sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22 \
--hash=sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5 \
--hash=sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a \
--hash=sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f \
--hash=sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f \
--hash=sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040 \
--hash=sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960 \
--hash=sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60 \
--hash=sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c \
--hash=sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707 \
--hash=sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f \
--hash=sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef \
--hash=sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1 \
--hash=sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb \
--hash=sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d \
--hash=sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1 \
--hash=sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073 \
--hash=sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f \
--hash=sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291 \
--hash=sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b \
--hash=sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7 \
--hash=sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801 \
--hash=sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35 \
--hash=sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b \
--hash=sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e \
--hash=sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a \
--hash=sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d \
--hash=sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68 \
--hash=sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2 \
--hash=sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935 \
--hash=sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555 \
--hash=sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd \
--hash=sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7 \
--hash=sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7
# via psycopg
psycopg-pool==3.2.1 \
--hash=sha256:060b551d1b97a8d358c668be58b637780b884de14d861f4f5ecc48b7563aafb7 \
--hash=sha256:6509a75c073590952915eddbba7ce8b8332a440a31e77bba69561483492829ad
psycopg-pool==3.2.2 \
--hash=sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153 \
--hash=sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c
# via psycopg
pyasn1==0.6.0 \
--hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \
@ -140,93 +130,96 @@ python-ldap==3.4.4 \
# via
# -r contrib/container/requirements.in
# django-auth-ldap
pyyaml==6.0.1 \
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
--hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
--hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
--hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
--hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
--hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
--hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
--hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
--hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via -r contrib/container/requirements.in
setuptools==70.3.0 \
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via -r contrib/container/requirements.in
sqlparse==0.5.0 \
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
sqlparse==0.5.1 \
--hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \
--hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e
# via django
typing-extensions==4.11.0 \
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# psycopg
# psycopg-pool
uv==0.1.38 \
--hash=sha256:03242a734a572733f2b9a5dbb94517e918fe26fc01114b7c51d12296dfbb8f8b \
--hash=sha256:067af2d986329db4fa3c7373017d49f0e16ddff23e483b7e5bc3a5a18ce08ea6 \
--hash=sha256:0937ad16ae0e0b6bb6dd3c386f8fb33141ad08d1762eaacffb4d2b27fb466a17 \
--hash=sha256:0e1d64ac437b0a14fbcec55b1c3f162fa24860711e0d855fcd9c672b149a122a \
--hash=sha256:1be7aa46936c0351ccb1400ea95e5381b3f05fef772fa3b9f23af728cc175dea \
--hash=sha256:309e73a3ec3a5a536a3efaf434270fc94b483069f1425765165c1c9d786c27fd \
--hash=sha256:4251f9771d392d7badc1e5fb934b397b12ca00fef9d955207ade169cc1f7e872 \
--hash=sha256:43772e7589f70e954b1ae29230e575ef9e4d8d769138a94dfa5ae7eaf1e26ac5 \
--hash=sha256:4a6024256d38b77151e32876be9fcb99cf75df7a86b26e0161cc202bed558adf \
--hash=sha256:5a98d6aacd4b57b7e00daf154919e7c9206fefdf40bd28cfb13efe0e0324d491 \
--hash=sha256:8de6dbd8f348ee90af044f4cc7b6650521d25ba2d20a813c1e157a3f90069dd9 \
--hash=sha256:9133e24db9bdd4f412eab69586d03294419825432a9a27ee1b510a4c01eb7b0b \
--hash=sha256:92f65b6e4e5c8126501785af3629dc537d7c82caa56ac9336a86929c73d0e138 \
--hash=sha256:afd85029923e712b6b2c45ddc1680c785392220876c766521e45778db3f71f8e \
--hash=sha256:b0b15e51a0f8240969bc412ed0dd60cfe3f664b30173139ef263d71c596d631f \
--hash=sha256:ea44c07605d1359a7d82bf42706dd86d341f15f4ca2e1f36e51626a7111c2ad5 \
--hash=sha256:f87c9711493c53d32012a96b49c4d53aabdf7ed666cbf2c3fb55dd402a6b31a8
uv==0.3.0 \
--hash=sha256:084551ee0743339aa5d0d4c76a94c9f9df16c33030b850f0cd98f316db7b42cc \
--hash=sha256:0da4f060d583325846cde0727a8cc0cb4e8c63b30ac9373dae213a7315056d90 \
--hash=sha256:160a1f3b01298942d6cfe21f95a9b7daa3eb73231ba1fc4689157eb9f23b3438 \
--hash=sha256:21ebc6ca30df7ff57a8e17e3abeeba8a9d1d4ac79c1adf842fa42d48a5c7f372 \
--hash=sha256:24a1388f5e285058f97576b7dfee79bb5007a712a9e368f3fcdcfeb2dfd9ce92 \
--hash=sha256:2f937ebdf9976ec1ffe7228fd608ef3e6ce2a61ed68cf7b157ae6900a9c80f41 \
--hash=sha256:39a4276afe0808ca6c033e0cd6cb73249f934b4a0c9d7b18a944f3f8ea635e27 \
--hash=sha256:3b62e44f61a154303fc9f4aa87ae54891957d49769d21dcf2be9c22e640c3e92 \
--hash=sha256:4303364d717b1def58e82b11271259d2ee3bb03da0ca6111819ee254f65b38f4 \
--hash=sha256:503fc619238550be222b41422b415677c9b8045c92a9815f80ff5d7477671fe6 \
--hash=sha256:52b3a6110705ff27462ddc68657fedf8a296ed545619a90fa73354f130ad632e \
--hash=sha256:5c826d9daace67d67790503b0c1152093b3cecd35a91de10f5bb9e26afea9de9 \
--hash=sha256:6d1025349cbaeba9a974d413795d0ce8d37de5ad7fb7654c0519968b2c083ba1 \
--hash=sha256:a15b2321444f3668bc95863d2b13ce44ea54053189427ea48d112ecd8b3d2f89 \
--hash=sha256:a71b7080ee6d7658b22f93aa750cbfd19111cd6c8ac643a73d6778598dd06559 \
--hash=sha256:b44ebf501de5eef33e4f3cf4b6ea9a458d1f1b3cf26737c25ac507ab7914076a \
--hash=sha256:d3da56b87ec5aa4f2ae572127c754655bad3820dd41a4d37ed4d5e2f67035990 \
--hash=sha256:d87ff76da5128036c05db0291db7510a85cb8efb86538e8f49adc8074bb292f0
# via -r contrib/container/requirements.in
wheel==0.43.0 \
--hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \
--hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via -r contrib/container/requirements.in

View File

@ -104,9 +104,9 @@ jc==1.25.3 \
--hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \
--hash=sha256:fa3140ceda6cba1210d1362f363cd79a0514741e8a1dd6167db2b2e2d5f24f7b
# via -r contrib/dev_reqs/requirements.in
pygments==2.17.2 \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
pygments==2.18.0 \
--hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \
--hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a
# via jc
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \

View File

@ -1,8 +1,14 @@
# Configuration file for Crowdin project integration
# See: https://crowdin.com/project/inventree
"commit_message": "Fix: New translations %original_file_name% from Crowdin"
"append_commit_message": false
"preserve_hierarchy": true
files:
- source: /src/backend/InvenTree/locale/en/LC_MESSAGES/django.po
dest: /%original_path%/%original_file_name%
translation: /src/backend/InvenTree/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%
- source: /src/frontend/src/locales/en/messages.po
dest: /%original_path%/%original_file_name%
translation: /src/frontend/src/locales/%two_letters_code%/%original_file_name%

View File

@ -0,0 +1,15 @@
---
title: Custom States
---
## Custom States
Several models within InvenTree support the use of custom states. The custom states are display only - the business logic is not affected by the state.
States can be added in the Admin Center under the "Custom States" section. Each state has a name, label and a color that are used to display the state in the user interface. Changes to these settings will only be reflected in the user interface after a full reload of the interface.
States need to be assigned to a model, state (for example status on a StockItem) and a logical key - that will be used for business logic. These 3 values combined need to be unique throughout the system.
Custom states can be used in the following models:
- StockItem
- Orders (PurchaseOrder, SalesOrder, ReturnOrder, ReturnOrderLine)

View File

@ -47,6 +47,8 @@ If you want to create your own machine type, please also take a look at the alre
```py
from django.utils.translation import gettext_lazy as _
from generic.states import ColorEnum
from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
class ABCBaseDriver(BaseDriver):
@ -72,9 +74,9 @@ class ABCMachine(BaseMachineType):
base_driver = ABCBaseDriver
class ABCStatus(MachineStatus):
CONNECTED = 100, _('Connected'), 'success'
STANDBY = 101, _('Standby'), 'success'
PRINTING = 110, _('Printing'), 'primary'
CONNECTED = 100, _('Connected'), ColorEnum.success
STANDBY = 101, _('Standby'), ColorEnum.success
PRINTING = 110, _('Printing'), ColorEnum.primary
MACHINE_STATUS = ABCStatus
default_machine_status = ABCStatus.DISCONNECTED

View File

@ -38,6 +38,8 @@ Refer to the source code for the Purchase Order status codes:
show_source: True
members: []
Purchase Order Status supports [custom states](../concepts/custom_states.md).
### Purchase Order Currency
The currency code can be specified for an individual purchase order. If not specified, the default currency specified against the [supplier](./company.md#suppliers) will be used.

View File

@ -61,6 +61,8 @@ Refer to the source code for the Return Order status codes:
show_source: True
members: []
Return Order Status supports [custom states](../concepts/custom_states.md).
## Create a Return Order
From the Return Order index, click on <span class='badge inventree add'><span class='fas fa-plus-circle'></span> New Return Order</span> which opens the "Create Return Order" form.

View File

@ -39,6 +39,8 @@ Refer to the source code for the Sales Order status codes:
show_source: True
members: []
Sales Order Status supports [custom states](../concepts/custom_states.md).
### Sales Order Currency
The currency code can be specified for an individual sales order. If not specified, the default currency specified against the [customer](./company.md#customers) will be used.

View File

@ -10,7 +10,7 @@ Certain stock item status codes will restrict the availability of the stock item
Below is the list of available stock status codes and their meaning:
| Status | Description | Available |
| Status | Description | Available |
| ----------- | ----------- | --- |
| <span class='badge inventree success'>OK</span> | Stock item is healthy, nothing wrong to report | <span class='badge inventree success'>Yes</span> |
| <span class='badge inventree warning'>Attention needed</span> | Stock item hasn't been checked or tested yet | <span class='badge inventree success'>Yes</span> |
@ -38,6 +38,8 @@ Refer to the source code for the Stock status codes:
show_source: True
members: []
Stock Status supports [custom states](../concepts/custom_states.md).
### Default Status Code
The default status code for any newly created Stock Item is <span class='badge inventree success'>OK</span>

View File

@ -77,6 +77,7 @@ nav:
- Core Concepts:
- Terminology: concepts/terminology.md
- Physical Units: concepts/units.md
- Custom States: concepts/custom_states.md
- Development:
- Contributing: develop/contributing.md
- Devcontainer: develop/devcontainer.md

View File

@ -1,13 +1,27 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 245
INVENTREE_API_VERSION = 249
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v249 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
- Sort status enums
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
- Small adjustments to labels for new custom status fields
v247 - 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
- Adjust "attachment" field on StockItemTestResult serializer
- Allow null values for attachment
v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
- Adds custom status fields to various serializers
- Adds endpoints to admin custom status fields
v245 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
- Documented pagination fields (no functional changes)

View File

@ -3,9 +3,9 @@
"""Provides extra global data to all templates."""
import InvenTree.email
import InvenTree.ready
import InvenTree.status
from generic.states import StatusCode
from InvenTree.helpers import inheritors
from generic.states.custom import get_custom_classes
from users.models import RuleSet, check_user_role
@ -53,7 +53,10 @@ def status_codes(request):
return {}
request._inventree_status_codes = True
return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
get_custom = InvenTree.ready.isRebuildingData() is False
return {
cls.__name__: cls.template_context() for cls in get_custom_classes(get_custom)
}
def user_roles(request):

View File

@ -953,8 +953,15 @@ def get_objectreference(
Inheritors_T = TypeVar('Inheritors_T')
def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
"""Return all classes that are subclasses from the supplied cls."""
def inheritors(
cls: type[Inheritors_T], subclasses: bool = True
) -> set[type[Inheritors_T]]:
"""Return all classes that are subclasses from the supplied cls.
Args:
cls: The class to search for subclasses
subclasses: Include subclasses of subclasses (default = True)
"""
subcls = set()
work = [cls]
@ -963,7 +970,8 @@ def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
for child in parent.__subclasses__():
if child not in subcls:
subcls.add(child)
work.append(child)
if subclasses:
work.append(child)
return subcls

View File

@ -363,7 +363,7 @@ class InvenTreeMetadata(SimpleMetadata):
field_info['type'] = 'related field'
field_info['model'] = model._meta.model_name
# Special case for 'user' model
# Special case for special models
if field_info['model'] == 'user':
field_info['api_url'] = '/api/user/'
elif field_info['model'] == 'contenttype':
@ -381,6 +381,14 @@ class InvenTreeMetadata(SimpleMetadata):
if field_info['type'] == 'dependent field':
field_info['depends_on'] = field.depends_on
# Extend field info if the field has a get_field_info method
if (
not field_info.get('read_only')
and hasattr(field, 'get_field_info')
and callable(field.get_field_info)
):
field_info = field.get_field_info(field, field_info)
return field_info

View File

@ -0,0 +1,39 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
import django.core.validators
from django.db import migrations
import generic.states.fields
import InvenTree.status_codes
class Migration(migrations.Migration):
dependencies = [
("build", "0051_delete_buildorderattachment"),
]
operations = [
migrations.AddField(
model_name="build",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AlterField(
model_name="build",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.BuildStatus.items(),
default=10,
help_text="Build status code",
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Build Status",
),
),
]

View File

@ -43,6 +43,7 @@ import part.models
import report.mixins
import stock.models
import users.models
import generic.states
logger = logging.getLogger('inventree')
@ -315,7 +316,7 @@ class Build(
help_text=_('Number of stock items which have been completed')
)
status = models.PositiveIntegerField(
status = generic.states.fields.InvenTreeCustomStatusModelField(
verbose_name=_('Build Status'),
default=BuildStatus.PENDING.value,
choices=BuildStatus.items(),

View File

@ -2,45 +2,53 @@
from decimal import Decimal
from django.db import transaction
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.db.models import ExpressionWrapper, F, FloatField
from django.db.models import Case, Sum, When, Value
from django.db.models import BooleanField, Q
from django.db import models, transaction
from django.db.models import (
BooleanField,
Case,
ExpressionWrapper,
F,
FloatField,
Q,
Sum,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer
import InvenTree.helpers
import InvenTree.tasks
from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin
from stock.status_codes import StockStatus
from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationBriefSerializer
import build.tasks
import common.models
from common.serializers import ProjectCodeSerializer
from common.settings import get_global_setting
from importer.mixins import DataImportExportSerializerMixin
import company.serializers
import InvenTree.helpers
import InvenTree.tasks
import part.filters
import part.serializers as part_serializers
from common.serializers import ProjectCodeSerializer
from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import (
InvenTreeDecimalField,
InvenTreeModelSerializer,
NotesFieldMixin,
UserSerializer,
)
from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation
from stock.serializers import LocationBriefSerializer, StockItemSerializerBrief
from stock.status_codes import StockStatus
from users.serializers import OwnerSerializer
from .models import Build, BuildLine, BuildItem
from .models import Build, BuildItem, BuildLine
from .status_codes import BuildStatus
class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer):
class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTreeCustomStatusSerializerMixin, InvenTreeModelSerializer):
"""Serializes a Build object."""
class Meta:
@ -69,6 +77,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
'quantity',
'status',
'status_text',
'status_custom_key',
'target_date',
'take_from',
'notes',
@ -882,8 +891,8 @@ class BuildUnallocationSerializer(serializers.Serializer):
data = self.validated_data
build.deallocate_stock(
build_line=data['build_line'],
output=data['output']
build_line=data.get('build_line', None),
output=data.get('output', None),
)

View File

@ -2,17 +2,17 @@
from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode
from generic.states import ColorEnum, StatusCode
class BuildStatus(StatusCode):
"""Build status codes."""
PENDING = 10, _('Pending'), 'secondary' # Build is pending / active
PRODUCTION = 20, _('Production'), 'primary' # Build is in production
ON_HOLD = 25, _('On Hold'), 'warning' # Build is on hold
CANCELLED = 30, _('Cancelled'), 'danger' # Build was cancelled
COMPLETE = 40, _('Complete'), 'success' # Build is complete
PENDING = 10, _('Pending'), ColorEnum.secondary # Build is pending / active
PRODUCTION = 20, _('Production'), ColorEnum.primary # Build is in production
ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Build is on hold
CANCELLED = 30, _('Cancelled'), ColorEnum.danger # Build was cancelled
COMPLETE = 40, _('Complete'), ColorEnum.success # Build is complete
class BuildStatusGroups:

View File

@ -158,7 +158,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>
{% status_label 'build' build.status %}
{% display_status_label 'build' build.status_custom_key build.status %}
</td>
</tr>
{% if build.target_date %}
@ -225,7 +225,7 @@ src="{% static 'img/blank_image.png' %}"
{% block page_data %}
<h3>
{% status_label 'build' build.status large=True %}
{% display_status_label 'build' build.status_custom_key build.status large=True %}
{% if build.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}

View File

@ -60,7 +60,7 @@
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% status_label 'build' build.status %}</td>
<td>{% display_status_label 'build' build.status_custom_key build.status %}</td>
</tr>
<tr>
<td><span class='fas fa-check-circle'></span></td>

View File

@ -29,7 +29,7 @@ import common.models
import common.serializers
from common.icons import get_icon_packs
from common.settings import get_global_setting
from generic.states.api import AllStatusViews, StatusView
from generic.states.api import urlpattern as generic_states_api_urls
from importer.mixins import DataExportViewMixin
from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.config import CONFIG_LOOKUPS
@ -655,6 +655,8 @@ class ContentTypeList(ListAPI):
queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['app_label', 'model']
class ContentTypeDetail(RetrieveAPI):
@ -965,16 +967,7 @@ common_api_urls = [
]),
),
# Status
path(
'generic/status/',
include([
path(
f'<str:{StatusView.MODEL_REF}>/',
include([path('', StatusView.as_view(), name='api-status')]),
),
path('', AllStatusViews.as_view(), name='api-status-all'),
]),
),
path('generic/status/', include(generic_states_api_urls)),
# Contenttype
path(
'contenttype/',

View File

@ -0,0 +1,97 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
import django.db.models.deletion
from django.db import migrations, models
from common.models import state_color_mappings
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("common", "0028_colortheme_user_obj"),
]
operations = [
migrations.CreateModel(
name="InvenTreeCustomUserStateModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"key",
models.IntegerField(
help_text="Value that will be saved in the models database",
verbose_name="Key",
),
),
(
"name",
models.CharField(
help_text="Name of the state",
max_length=250,
verbose_name="Name",
),
),
(
"label",
models.CharField(
help_text="Label that will be displayed in the frontend",
max_length=250,
verbose_name="Label",
),
),
(
"color",
models.CharField(
choices=state_color_mappings(),
default="secondary",
help_text="Color that will be displayed in the frontend",
max_length=10,
verbose_name="Color",
),
),
(
"logical_key",
models.IntegerField(
help_text="State logical key that is equal to this custom state in business logic",
verbose_name="Logical Key",
),
),
(
"reference_status",
models.CharField(
help_text="Status set that is extended with this custom state",
max_length=250,
verbose_name="Reference Status Set",
),
),
(
"model",
models.ForeignKey(
blank=True,
help_text="Model this state is associated with",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="contenttypes.contenttype",
verbose_name="Model",
),
),
],
options={
"verbose_name": "Custom State",
"verbose_name_plural": "Custom States",
"unique_together": {
("model", "reference_status", "key", "logical_key")
},
},
),
]

View File

@ -53,6 +53,8 @@ import order.validators
import plugin.base.barcodes.helper
import report.helpers
import users.models
from generic.states import ColorEnum
from generic.states.custom import get_custom_classes, state_color_mappings
from InvenTree.sanitizer import sanitize_svg
from plugin import registry
@ -3339,3 +3341,109 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
raise ValidationError(_('Invalid model type specified for attachment'))
return model_class.check_attachment_permission(permission, user)
class InvenTreeCustomUserStateModel(models.Model):
"""Custom model to extends any registered state with extra custom, user defined states."""
key = models.IntegerField(
verbose_name=_('Key'),
help_text=_('Value that will be saved in the models database'),
)
name = models.CharField(
max_length=250, verbose_name=_('Name'), help_text=_('Name of the state')
)
label = models.CharField(
max_length=250,
verbose_name=_('Label'),
help_text=_('Label that will be displayed in the frontend'),
)
color = models.CharField(
max_length=10,
choices=state_color_mappings(),
default=ColorEnum.secondary.value,
verbose_name=_('Color'),
help_text=_('Color that will be displayed in the frontend'),
)
logical_key = models.IntegerField(
verbose_name=_('Logical Key'),
help_text=_(
'State logical key that is equal to this custom state in business logic'
),
)
model = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Model'),
help_text=_('Model this state is associated with'),
)
reference_status = models.CharField(
max_length=250,
verbose_name=_('Reference Status Set'),
help_text=_('Status set that is extended with this custom state'),
)
class Meta:
"""Metaclass options for this mixin."""
verbose_name = _('Custom State')
verbose_name_plural = _('Custom States')
unique_together = [['model', 'reference_status', 'key', 'logical_key']]
def __str__(self) -> str:
"""Return string representation of the custom state."""
return f'{self.model.name} ({self.reference_status}): {self.name} | {self.key} ({self.logical_key})'
def save(self, *args, **kwargs) -> None:
"""Ensure that the custom state is valid before saving."""
self.clean()
return super().save(*args, **kwargs)
def clean(self) -> None:
"""Validate custom state data."""
if self.model is None:
raise ValidationError({'model': _('Model must be selected')})
if self.key is None:
raise ValidationError({'key': _('Key must be selected')})
if self.logical_key is None:
raise ValidationError({'logical_key': _('Logical key must be selected')})
# Ensure that the key is not the same as the logical key
if self.key == self.logical_key:
raise ValidationError({'key': _('Key must be different from logical key')})
if self.reference_status is None or self.reference_status == '':
raise ValidationError({
'reference_status': _('Reference status must be selected')
})
# Ensure that the key is not in the range of the logical keys of the reference status
ref_set = list(
filter(
lambda x: x.__name__ == self.reference_status,
get_custom_classes(include_custom=False),
)
)
if len(ref_set) == 0:
raise ValidationError({
'reference_status': _('Reference status set not found')
})
ref_set = ref_set[0]
if self.key in ref_set.keys():
raise ValidationError({
'key': _(
'Key must be different from the logical keys of the reference status'
)
})
if self.logical_key not in ref_set.keys():
raise ValidationError({
'logical_key': _(
'Logical key must be in the logical keys of the reference status'
)
})
return super().clean()

View File

@ -14,6 +14,7 @@ from taggit.serializers import TagListSerializerField
import common.models as common_models
import common.validators
import generic.states.custom
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.helpers import get_objectreference
@ -308,6 +309,32 @@ class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSeria
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
@register_importer()
class CustomStateSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
"""Serializer for the custom state model."""
class Meta:
"""Meta options for CustomStateSerializer."""
model = common_models.InvenTreeCustomUserStateModel
fields = [
'pk',
'key',
'name',
'label',
'color',
'logical_key',
'model',
'model_name',
'reference_status',
]
model_name = serializers.CharField(read_only=True, source='model.name')
reference_status = serializers.ChoiceField(
choices=generic.states.custom.state_reference_mappings()
)
class FlagSerializer(serializers.Serializer):
"""Serializer for feature flags."""

View File

@ -33,6 +33,7 @@ from .models import (
Attachment,
ColorTheme,
CustomUnit,
InvenTreeCustomUserStateModel,
InvenTreeSetting,
InvenTreeUserSetting,
NotesImage,
@ -1586,3 +1587,93 @@ class ValidatorsTest(TestCase):
common.validators.validate_icon('ti:package:non-existing-variant')
common.validators.validate_icon('ti:package:outline')
class CustomStatusTest(TestCase):
"""Unit tests for the custom status model."""
def setUp(self):
"""Setup for all tests."""
self.data = {
'key': 11,
'name': 'OK - advanced',
'label': 'OK - adv.',
'color': 'secondary',
'logical_key': 10,
'model': ContentType.objects.get(model='stockitem'),
'reference_status': 'StockStatus',
}
def test_validation_model(self):
"""Test that model is present."""
data = self.data
data.pop('model')
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_key(self):
"""Tests Model must have a key."""
data = self.data
data.pop('key')
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_logicalkey(self):
"""Tests Logical key must be present."""
data = self.data
data.pop('logical_key')
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_reference(self):
"""Tests Reference status must be present."""
data = self.data
data.pop('reference_status')
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_logical_unique(self):
"""Tests Logical key must be unique."""
data = self.data
data['logical_key'] = data['key']
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_reference_exsists(self):
"""Tests Reference status set not found."""
data = self.data
data['reference_status'] = 'abcd'
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_key_unique(self):
"""Tests Key must be different from the logical keys of the reference."""
data = self.data
data['key'] = 50
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation_logical_key_exsists(self):
"""Tests Logical key must be in the logical keys of the reference status."""
data = self.data
data['logical_key'] = 12
with self.assertRaises(ValidationError):
InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 0)
def test_validation(self):
"""Tests Valid run."""
data = self.data
instance = InvenTreeCustomUserStateModel.objects.create(**data)
self.assertEqual(data['key'], instance.key)
self.assertEqual(InvenTreeCustomUserStateModel.objects.count(), 1)
self.assertEqual(
instance.__str__(), 'Stock Item (StockStatus): OK - advanced | 11 (10)'
)

View File

@ -6,7 +6,13 @@ There is a rendered state for each state value. The rendered state is used for d
States can be extended with custom options for each InvenTree instance - those options are stored in the database and need to link back to state values.
"""
from .states import StatusCode
from .states import ColorEnum, StatusCode
from .transition import StateTransitionMixin, TransitionMethod, storage
__all__ = ['StatusCode', 'storage', 'TransitionMethod', 'StateTransitionMixin']
__all__ = [
'ColorEnum',
'StatusCode',
'storage',
'TransitionMethod',
'StateTransitionMixin',
]

View File

@ -2,12 +2,22 @@
import inspect
from django.urls import include, path
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions, serializers
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
import common.models
import common.serializers
from generic.states.custom import get_status_api_response
from importer.mixins import DataExportViewMixin
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.permissions import IsStaffOrReadOnly
from InvenTree.serializers import EmptySerializer
from machine.machine_type import MachineStatus
from .states import StatusCode
@ -73,18 +83,52 @@ class AllStatusViews(StatusView):
def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes."""
data = {}
def discover_status_codes(parent_status_class, prefix=None):
"""Recursively discover status classes."""
for status_class in parent_status_class.__subclasses__():
name = '__'.join([*(prefix or []), status_class.__name__])
data[name] = {
'class': status_class.__name__,
'values': status_class.dict(),
}
discover_status_codes(status_class, [name])
discover_status_codes(StatusCode)
data = get_status_api_response()
# Extend with MachineStatus classes
data.update(get_status_api_response(MachineStatus, prefix=['MachineStatus']))
return Response(data)
# Custom states
class CustomStateList(DataExportViewMixin, ListCreateAPI):
"""List view for all custom states."""
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['key']
search_fields = ['key', 'name', 'label', 'reference_status']
class CustomStateDetail(RetrieveUpdateDestroyAPI):
"""Detail view for a particular custom states."""
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
urlpattern = [
# Custom state
path(
'custom/',
include([
path(
'<int:pk>/', CustomStateDetail.as_view(), name='api-custom-state-detail'
),
path('', CustomStateList.as_view(), name='api-custom-state-list'),
]),
),
# Generic status views
path(
'',
include([
path(
f'<str:{StatusView.MODEL_REF}>/',
include([path('', StatusView.as_view(), name='api-status')]),
),
path('', AllStatusViews.as_view(), name='api-status-all'),
]),
),
]

View File

@ -0,0 +1,89 @@
"""Helper functions for custom status labels."""
from InvenTree.helpers import inheritors
from .states import ColorEnum, StatusCode
def get_custom_status_labels(include_custom: bool = True):
"""Return a dict of custom status labels."""
return {cls.tag(): cls for cls in get_custom_classes(include_custom)}
def get_status_api_response(base_class=StatusCode, prefix=None):
"""Return a dict of status classes (custom and class defined).
Args:
base_class: The base class to search for subclasses.
prefix: A list of strings to prefix the class names with.
"""
return {
'__'.join([*(prefix or []), k.__name__]): {
'class': k.__name__,
'values': k.dict(),
}
for k in get_custom_classes(base_class=base_class, subclass=False)
}
def state_color_mappings():
"""Return a list of custom user state colors."""
return [(a.name, a.value) for a in ColorEnum]
def state_reference_mappings():
"""Return a list of custom user state references."""
classes = get_custom_classes(include_custom=False)
return [(a.__name__, a.__name__) for a in sorted(classes, key=lambda x: x.__name__)]
def get_logical_value(value, model: str):
"""Return the state model for the selected value."""
from common.models import InvenTreeCustomUserStateModel
return InvenTreeCustomUserStateModel.objects.get(key=value, model__model=model)
def get_custom_classes(
include_custom: bool = True, base_class=StatusCode, subclass=False
):
"""Return a dict of status classes (custom and class defined)."""
discovered_classes = inheritors(base_class, subclass)
if not include_custom:
return discovered_classes
# Gather DB settings
from common.models import InvenTreeCustomUserStateModel
custom_db_states = {}
custom_db_mdls = {}
for item in list(InvenTreeCustomUserStateModel.objects.all()):
if not custom_db_states.get(item.reference_status):
custom_db_states[item.reference_status] = []
custom_db_states[item.reference_status].append(item)
custom_db_mdls[item.model.app_label] = item.reference_status
custom_db_mdls_keys = custom_db_mdls.keys()
states = {}
for cls in discovered_classes:
tag = cls.tag()
states[tag] = cls
if custom_db_mdls and tag in custom_db_mdls_keys:
data = [(str(m.name), (m.value, m.label, m.color)) for m in states[tag]]
data_keys = [i[0] for i in data]
# Extent with non present tags
for entry in custom_db_states[custom_db_mdls[tag]]:
ref_name = str(entry.name.upper().replace(' ', ''))
if ref_name not in data_keys:
data += [
(
str(entry.name.upper().replace(' ', '')),
(entry.key, entry.label, entry.color),
)
]
# Re-assemble the enum
states[tag] = base_class(f'{tag.capitalize()}Status', data)
return states.values()

View File

@ -0,0 +1,249 @@
"""Custom model/serializer fields for InvenTree models that support custom states."""
from typing import Any, Iterable, Optional
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.fields import ChoiceField
from .custom import get_logical_value
class CustomChoiceField(serializers.ChoiceField):
"""Custom Choice Field.
This is not intended to be used directly.
"""
def __init__(self, choices: Iterable, **kwargs):
"""Initialize the field."""
choice_mdl = kwargs.pop('choice_mdl', None)
choice_field = kwargs.pop('choice_field', None)
is_custom = kwargs.pop('is_custom', False)
kwargs.pop('max_value', None)
kwargs.pop('min_value', None)
super().__init__(choices, **kwargs)
self.choice_mdl = choice_mdl
self.choice_field = choice_field
self.is_custom = is_custom
def to_internal_value(self, data):
"""Map the choice (that might be a custom one) back to the logical value."""
try:
return super().to_internal_value(data)
except serializers.ValidationError:
try:
logical = get_logical_value(data, self.choice_mdl._meta.model_name)
if self.is_custom:
return logical.key
return logical.logical_key
except (ObjectDoesNotExist, Exception):
raise serializers.ValidationError('Invalid choice')
def get_field_info(self, field, field_info):
"""Return the field information for the given item."""
from common.models import InvenTreeCustomUserStateModel
# Static choices
choices = [
{
'value': choice_value,
'display_name': force_str(choice_name, strings_only=True),
}
for choice_value, choice_name in field.choices.items()
]
# Dynamic choices from InvenTreeCustomUserStateModel
objs = InvenTreeCustomUserStateModel.objects.filter(
model__model=field.choice_mdl._meta.model_name
)
dyn_choices = [
{'value': choice.key, 'display_name': choice.label} for choice in objs.all()
]
if dyn_choices:
all_choices = choices + dyn_choices
field_info['choices'] = sorted(all_choices, key=lambda kv: kv['value'])
else:
field_info['choices'] = choices
return field_info
class ExtraCustomChoiceField(CustomChoiceField):
"""Custom Choice Field that returns value of status if empty.
This is not intended to be used directly.
"""
def to_representation(self, value):
"""Return the value of the status if it is empty."""
return super().to_representation(value) or value
class InvenTreeCustomStatusModelField(models.PositiveIntegerField):
"""Custom model field for extendable status codes.
Adds a secondary *_custom_key field to the model which can be used to store additional status information.
Models using this model field must also include the InvenTreeCustomStatusSerializerMixin in all serializers that create or update the value.
"""
def deconstruct(self):
"""Deconstruct the field for migrations."""
name, path, args, kwargs = super().deconstruct()
return name, path, args, kwargs
def contribute_to_class(self, cls, name):
"""Add the _custom_key field to the model."""
cls._meta.supports_custom_status = True
if not hasattr(self, '_custom_key_field'):
self.add_field(cls, name)
super().contribute_to_class(cls, name)
def clean(self, value: Any, model_instance: Any) -> Any:
"""Ensure that the value is not an empty string."""
if value == '':
value = None
return super().clean(value, model_instance)
def add_field(self, cls, name):
"""Adds custom_key_field to the model class to save additional status information."""
custom_key_field = ExtraInvenTreeCustomStatusModelField(
default=None,
verbose_name=_('Custom status key'),
help_text=_('Additional status information for this item'),
blank=True,
null=True,
)
cls.add_to_class(f'{name}_custom_key', custom_key_field)
self._custom_key_field = custom_key_field
class ExtraInvenTreeCustomStatusModelField(models.PositiveIntegerField):
"""Custom field used to detect custom extenteded fields.
This is not intended to be used directly, if you want to support custom states in your model use InvenTreeCustomStatusModelField.
"""
class InvenTreeCustomStatusSerializerMixin:
"""Mixin to ensure custom status fields are set.
This mixin must be used to ensure that custom status fields are set correctly when updating a model.
"""
_custom_fields: Optional[list] = None
_custom_fields_leader: Optional[list] = None
_custom_fields_follower: Optional[list] = None
_is_gathering = False
def update(self, instance, validated_data):
"""Ensure the custom field is updated if the leader was changed."""
self.gather_custom_fields()
# Mirror values from leader to follower
for field in self._custom_fields_leader:
follower_field_name = f'{field}_custom_key'
if (
field in self.initial_data
and self.instance
and self.initial_data[field]
!= getattr(self.instance, follower_field_name, None)
):
setattr(self.instance, follower_field_name, self.initial_data[field])
# Mirror values from follower to leader
for field in self._custom_fields_follower:
leader_field_name = field.replace('_custom_key', '')
if field in validated_data and leader_field_name not in self.initial_data:
try:
reference = get_logical_value(
validated_data[field],
self.fields[field].choice_mdl._meta.model_name,
)
validated_data[leader_field_name] = reference.logical_key
except (ObjectDoesNotExist, Exception):
if validated_data[field] in self.fields[leader_field_name].choices:
validated_data[leader_field_name] = validated_data[field]
else:
raise serializers.ValidationError('Invalid choice')
return super().update(instance, validated_data)
def to_representation(self, instance):
"""Ensure custom state fields are not served empty."""
data = super().to_representation(instance)
for field in self.gather_custom_fields():
if data[field] is None:
data[field] = data[
field.replace('_custom_key', '')
] # Use "normal" status field instead
return data
def gather_custom_fields(self):
"""Gather all custom fields on the serializer."""
if self._custom_fields_follower:
self._is_gathering = False
return self._custom_fields_follower
if self._is_gathering:
self._custom_fields = {}
else:
self._is_gathering = True
# Gather fields
self._custom_fields = {
k: v.is_custom
for k, v in self.fields.items()
if isinstance(v, CustomChoiceField)
}
# Separate fields for easier/cheaper access
self._custom_fields_follower = [k for k, v in self._custom_fields.items() if v]
self._custom_fields_leader = [
k for k, v in self._custom_fields.items() if not v
]
return self._custom_fields_follower
def build_standard_field(self, field_name, model_field):
"""Use custom field for custom status model.
This is required because of DRF overwriting all fields with choice sets.
"""
field_cls, field_kwargs = super().build_standard_field(field_name, model_field)
if issubclass(field_cls, ChoiceField) and isinstance(
model_field, InvenTreeCustomStatusModelField
):
field_cls = CustomChoiceField
field_kwargs['choice_mdl'] = model_field.model
field_kwargs['choice_field'] = model_field.name
elif isinstance(model_field, ExtraInvenTreeCustomStatusModelField):
field_cls = ExtraCustomChoiceField
field_kwargs['choice_mdl'] = model_field.model
field_kwargs['choice_field'] = model_field.name
field_kwargs['is_custom'] = True
# Inherit choices from leader
self.gather_custom_fields()
if field_name in self._custom_fields:
leader_field_name = field_name.replace('_custom_key', '')
leader_field = self.fields[leader_field_name]
if hasattr(leader_field, 'choices'):
field_kwargs['choices'] = list(leader_field.choices.items())
elif hasattr(model_field.model, leader_field_name):
leader_model_field = getattr(
model_field.model, leader_field_name
).field
if hasattr(leader_model_field, 'choices'):
field_kwargs['choices'] = leader_model_field.choices
if getattr(leader_field, 'read_only', False) is True:
field_kwargs['read_only'] = True
if 'choices' not in field_kwargs:
field_kwargs['choices'] = []
return field_cls, field_kwargs

View File

@ -2,6 +2,7 @@
import enum
import re
from enum import Enum
class BaseEnum(enum.IntEnum):
@ -65,10 +66,23 @@ class StatusCode(BaseEnum):
# Normal item definition
if len(args) == 1:
obj.label = args[0]
obj.color = 'secondary'
obj.color = ColorEnum.secondary
else:
obj.label = args[1]
obj.color = args[2] if len(args) > 2 else 'secondary'
obj.color = args[2] if len(args) > 2 else ColorEnum.secondary
# Ensure color is a valid value
if isinstance(obj.color, str):
try:
obj.color = ColorEnum(obj.color)
except ValueError:
raise ValueError(
f"Invalid color value '{obj.color}' for status '{obj.label}'"
)
# Set color value as string
obj.color = obj.color.value
obj.color_class = obj.color
return obj
@ -181,3 +195,15 @@ class StatusCode(BaseEnum):
ret['list'] = cls.list()
return ret
class ColorEnum(Enum):
"""Enum for color values."""
primary = 'primary'
secondary = 'secondary'
success = 'success'
danger = 'danger'
warning = 'warning'
info = 'info'
dark = 'dark'

View File

@ -3,15 +3,21 @@
from django.utils.safestring import mark_safe
from generic.templatetags.generic import register
from InvenTree.helpers import inheritors
from .states import StatusCode
from .custom import get_custom_status_labels
@register.simple_tag
def status_label(typ: str, key: int, *args, **kwargs):
def status_label(typ: str, key: int, include_custom: bool = False, *args, **kwargs):
"""Render a status label."""
state = {cls.tag(): cls for cls in inheritors(StatusCode)}.get(typ, None)
state = get_custom_status_labels(include_custom=include_custom).get(typ, None)
if state:
return mark_safe(state.render(key, large=kwargs.get('large', False)))
raise ValueError(f"Unknown status type '{typ}'")
@register.simple_tag
def display_status_label(typ: str, key: int, fallback: int, *args, **kwargs):
"""Render a status label."""
render_key = int(key) if key else fallback
return status_label(typ, render_key, *args, include_custom=True, **kwargs)

View File

@ -1,11 +1,15 @@
"""Tests for the generic states module."""
from django.contrib.contenttypes.models import ContentType
from django.test.client import RequestFactory
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.test import force_authenticate
from InvenTree.unit_test import InvenTreeTestCase
from common.models import InvenTreeCustomUserStateModel
from generic.states import ColorEnum
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
from .api import StatusView
from .states import StatusCode
@ -14,9 +18,9 @@ from .states import StatusCode
class GeneralStatus(StatusCode):
"""Defines a set of status codes for tests."""
PENDING = 10, _('Pending'), 'secondary'
PENDING = 10, _('Pending'), ColorEnum.secondary
PLACED = 20, _('Placed'), 'primary'
COMPLETE = 30, _('Complete'), 'success'
COMPLETE = 30, _('Complete'), ColorEnum.success
ABC = None # This should be ignored
_DEF = None # This should be ignored
jkl = None # This should be ignored
@ -120,8 +124,19 @@ class GeneralStateTest(InvenTreeTestCase):
# label
self.assertEqual(GeneralStatus.label(10), 'Pending')
def test_tag_function(self):
"""Test that the status code tag functions."""
def test_color(self):
"""Test that the color enum validation works."""
with self.assertRaises(ValueError) as e:
class TTTT(StatusCode):
PENDING = 10, _('Pending'), 'invalid'
self.assertEqual(
str(e.exception), "Invalid color value 'invalid' for status 'Pending'"
)
def test_tag_status_label(self):
"""Test that the status_label tag."""
from .tags import status_label
self.assertEqual(
@ -137,6 +152,21 @@ class GeneralStateTest(InvenTreeTestCase):
# Test non-existent key
self.assertEqual(status_label('general', 100), '100')
def test_tag_display_status_label(self):
"""Test that the display_status_label tag (mainly the same as status_label)."""
from .tags import display_status_label
self.assertEqual(
display_status_label('general', 10, 11),
"<span class='badge rounded-pill bg-secondary'>Pending</span>",
)
# Fallback
self.assertEqual(display_status_label('general', None, 11), '11')
self.assertEqual(
display_status_label('general', None, 10),
"<span class='badge rounded-pill bg-secondary'>Pending</span>",
)
def test_api(self):
"""Test StatusView API view."""
view = StatusView.as_view()
@ -191,3 +221,59 @@ class GeneralStateTest(InvenTreeTestCase):
self.assertEqual(
str(e.exception), '`status_class` not a valid StatusCode class'
)
class ApiTests(InvenTreeAPITestCase):
"""Test the API for the generic states module."""
def test_all_states(self):
"""Test the API endpoint for listing all status models."""
response = self.get(reverse('api-status-all'))
self.assertEqual(len(response.data), 12)
# Test the BuildStatus model
build_status = response.data['BuildStatus']
self.assertEqual(build_status['class'], 'BuildStatus')
self.assertEqual(len(build_status['values']), 5)
pending = build_status['values']['PENDING']
self.assertEqual(pending['key'], 10)
self.assertEqual(pending['name'], 'PENDING')
self.assertEqual(pending['label'], 'Pending')
# Test the StockStatus model (static)
stock_status = response.data['StockStatus']
self.assertEqual(stock_status['class'], 'StockStatus')
self.assertEqual(len(stock_status['values']), 8)
in_stock = stock_status['values']['OK']
self.assertEqual(in_stock['key'], 10)
self.assertEqual(in_stock['name'], 'OK')
self.assertEqual(in_stock['label'], 'OK')
# MachineStatus model
machine_status = response.data['MachineStatus__LabelPrinterStatus']
self.assertEqual(machine_status['class'], 'LabelPrinterStatus')
self.assertEqual(len(machine_status['values']), 6)
connected = machine_status['values']['CONNECTED']
self.assertEqual(connected['key'], 100)
self.assertEqual(connected['name'], 'CONNECTED')
# Add custom status
InvenTreeCustomUserStateModel.objects.create(
key=11,
name='OK - advanced',
label='OK - adv.',
color='secondary',
logical_key=10,
model=ContentType.objects.get(model='stockitem'),
reference_status='StockStatus',
)
response = self.get(reverse('api-status-all'))
self.assertEqual(len(response.data), 12)
stock_status_cstm = response.data['StockStatus']
self.assertEqual(stock_status_cstm['class'], 'StockStatus')
self.assertEqual(len(stock_status_cstm['values']), 9)
ok_advanced = stock_status_cstm['values']['OK']
self.assertEqual(ok_advanced['key'], 10)
self.assertEqual(ok_advanced['name'], 'OK')
self.assertEqual(ok_advanced['label'], 'OK')

View File

@ -2,18 +2,26 @@
from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode
from generic.states import ColorEnum, StatusCode
class DataImportStatusCode(StatusCode):
"""Defines a set of status codes for a DataImportSession."""
INITIAL = 0, _('Initializing'), 'secondary' # Import session has been created
MAPPING = 10, _('Mapping Columns'), 'primary' # Import fields are being mapped
IMPORTING = 20, _('Importing Data'), 'primary' # Data is being imported
INITIAL = (
0,
_('Initializing'),
ColorEnum.secondary,
) # Import session has been created
MAPPING = (
10,
_('Mapping Columns'),
ColorEnum.primary,
) # Import fields are being mapped
IMPORTING = 20, _('Importing Data'), ColorEnum.primary # Data is being imported
PROCESSING = (
30,
_('Processing Data'),
'primary',
ColorEnum.primary,
) # Data is being processed by the user
COMPLETE = 40, _('Complete'), 'success' # Import has been completed
COMPLETE = 40, _('Complete'), ColorEnum.success # Import has been completed

View File

@ -12,6 +12,7 @@ from PIL.Image import Image
from rest_framework import serializers
from rest_framework.request import Request
from generic.states import ColorEnum
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
from plugin import registry as plg_registry
from plugin.base.label.mixins import LabelPrintingMixin
@ -228,12 +229,12 @@ class LabelPrinterStatus(MachineStatus):
DISCONNECTED: The driver cannot establish a connection to the printer
"""
CONNECTED = 100, _('Connected'), 'success'
UNKNOWN = 101, _('Unknown'), 'secondary'
PRINTING = 110, _('Printing'), 'primary'
NO_MEDIA = 301, _('No media'), 'warning'
PAPER_JAM = 302, _('Paper jam'), 'warning'
DISCONNECTED = 400, _('Disconnected'), 'danger'
CONNECTED = 100, _('Connected'), ColorEnum.success
UNKNOWN = 101, _('Unknown'), ColorEnum.secondary
PRINTING = 110, _('Printing'), ColorEnum.primary
NO_MEDIA = 301, _('No media'), ColorEnum.warning
PAPER_JAM = 302, _('Paper jam'), ColorEnum.warning
DISCONNECTED = 400, _('Disconnected'), ColorEnum.danger
class LabelPrinterMachine(BaseMachineType):

View File

@ -0,0 +1,100 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
from django.db import migrations
import generic.states.fields
import InvenTree.status_codes
class Migration(migrations.Migration):
dependencies = [
("order", "0100_remove_returnorderattachment_order_and_more"),
]
operations = [
migrations.AddField(
model_name="purchaseorder",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AddField(
model_name="returnorder",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AddField(
model_name="returnorderlineitem",
name="outcome_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AddField(
model_name="salesorder",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AlterField(
model_name="purchaseorder",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.PurchaseOrderStatus.items(),
default=10,
help_text="Purchase order status",
verbose_name="Status",
),
),
migrations.AlterField(
model_name="returnorder",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.ReturnOrderStatus.items(),
default=10,
help_text="Return order status",
verbose_name="Status",
),
),
migrations.AlterField(
model_name="returnorderlineitem",
name="outcome",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.ReturnOrderLineStatus.items(),
default=10,
help_text="Outcome for this line item",
verbose_name="Outcome",
),
),
migrations.AlterField(
model_name="salesorder",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.SalesOrderStatus.items(),
default=10,
help_text="Sales order status",
verbose_name="Status",
),
),
]

View File

@ -37,6 +37,7 @@ from common.notifications import InvenTreeNotificationBodies
from common.settings import get_global_setting
from company.models import Address, Company, Contact, SupplierPart
from generic.states import StateTransitionMixin
from generic.states.fields import InvenTreeCustomStatusModelField
from InvenTree.exceptions import log_error
from InvenTree.fields import (
InvenTreeModelMoneyField,
@ -470,7 +471,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
validators=[order.validators.validate_purchase_order_reference],
)
status = models.PositiveIntegerField(
status = InvenTreeCustomStatusModelField(
default=PurchaseOrderStatus.PENDING.value,
choices=PurchaseOrderStatus.items(),
verbose_name=_('Status'),
@ -996,7 +997,7 @@ class SalesOrder(TotalPriceMixin, Order):
"""Accessor helper for Order base."""
return self.customer
status = models.PositiveIntegerField(
status = InvenTreeCustomStatusModelField(
default=SalesOrderStatus.PENDING.value,
choices=SalesOrderStatus.items(),
verbose_name=_('Status'),
@ -2153,7 +2154,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Accessor helper for Order base class."""
return self.customer
status = models.PositiveIntegerField(
status = InvenTreeCustomStatusModelField(
default=ReturnOrderStatus.PENDING.value,
choices=ReturnOrderStatus.items(),
verbose_name=_('Status'),
@ -2404,7 +2405,7 @@ class ReturnOrderLineItem(OrderLineItem):
"""Return True if this item has been received."""
return self.received_date is not None
outcome = models.PositiveIntegerField(
outcome = InvenTreeCustomStatusModelField(
default=ReturnOrderLineStatus.PENDING.value,
choices=ReturnOrderLineStatus.items(),
verbose_name=_('Outcome'),

View File

@ -32,6 +32,7 @@ from company.serializers import (
ContactSerializer,
SupplierPartSerializer,
)
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.helpers import (
@ -161,6 +162,7 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
'address_detail',
'status',
'status_text',
'status_custom_key',
'notes',
'barcode_hash',
'overdue',
@ -216,7 +218,11 @@ class AbstractExtraLineMeta:
@register_importer()
class PurchaseOrderSerializer(
NotesFieldMixin, TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
NotesFieldMixin,
TotalPriceMixin,
InvenTreeCustomStatusSerializerMixin,
AbstractOrderSerializer,
InvenTreeModelSerializer,
):
"""Serializer for a PurchaseOrder object."""
@ -859,7 +865,11 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
@register_importer()
class SalesOrderSerializer(
NotesFieldMixin, TotalPriceMixin, AbstractOrderSerializer, InvenTreeModelSerializer
NotesFieldMixin,
TotalPriceMixin,
InvenTreeCustomStatusSerializerMixin,
AbstractOrderSerializer,
InvenTreeModelSerializer,
):
"""Serializer for the SalesOrder model class."""
@ -1642,7 +1652,11 @@ class SalesOrderExtraLineSerializer(
@register_importer()
class ReturnOrderSerializer(
NotesFieldMixin, AbstractOrderSerializer, TotalPriceMixin, InvenTreeModelSerializer
NotesFieldMixin,
InvenTreeCustomStatusSerializerMixin,
AbstractOrderSerializer,
TotalPriceMixin,
InvenTreeModelSerializer,
):
"""Serializer for the ReturnOrder model class."""

View File

@ -2,20 +2,20 @@
from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode
from generic.states import ColorEnum, StatusCode
class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder."""
# Order status codes
PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed)
PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold
COMPLETE = 30, _('Complete'), 'success' # Order has been completed
CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled
LOST = 50, _('Lost'), 'warning' # Order was lost
RETURNED = 60, _('Returned'), 'warning' # Order was returned
PENDING = 10, _('Pending'), ColorEnum.secondary # Order is pending (not yet placed)
PLACED = 20, _('Placed'), ColorEnum.primary # Order has been placed with supplier
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
LOST = 50, _('Lost'), ColorEnum.warning # Order was lost
RETURNED = 60, _('Returned'), ColorEnum.warning # Order was returned
class PurchaseOrderStatusGroups:
@ -39,18 +39,18 @@ class PurchaseOrderStatusGroups:
class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder."""
PENDING = 10, _('Pending'), 'secondary' # Order is pending
PENDING = 10, _('Pending'), ColorEnum.secondary # Order is pending
IN_PROGRESS = (
15,
_('In Progress'),
'primary',
ColorEnum.primary,
) # Order has been issued, and is in progress
SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold
COMPLETE = 30, _('Complete'), 'success' # Order is complete
CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled
LOST = 50, _('Lost'), 'warning' # Order was lost
RETURNED = 60, _('Returned'), 'warning' # Order was returned
SHIPPED = 20, _('Shipped'), ColorEnum.success # Order has been shipped to customer
ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Order is on hold
COMPLETE = 30, _('Complete'), ColorEnum.success # Order is complete
CANCELLED = 40, _('Cancelled'), ColorEnum.danger # Order has been cancelled
LOST = 50, _('Lost'), ColorEnum.warning # Order was lost
RETURNED = 60, _('Returned'), ColorEnum.warning # Order was returned
class SalesOrderStatusGroups:
@ -71,15 +71,15 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder."""
# Order is pending, waiting for receipt of items
PENDING = 10, _('Pending'), 'secondary'
PENDING = 10, _('Pending'), ColorEnum.secondary
# Items have been received, and are being inspected
IN_PROGRESS = 20, _('In Progress'), 'primary'
IN_PROGRESS = 20, _('In Progress'), ColorEnum.primary
ON_HOLD = 25, _('On Hold'), 'warning'
ON_HOLD = 25, _('On Hold'), ColorEnum.warning
COMPLETE = 30, _('Complete'), 'success'
CANCELLED = 40, _('Cancelled'), 'danger'
COMPLETE = 30, _('Complete'), ColorEnum.success
CANCELLED = 40, _('Cancelled'), ColorEnum.danger
class ReturnOrderStatusGroups:
@ -95,19 +95,19 @@ class ReturnOrderStatusGroups:
class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem."""
PENDING = 10, _('Pending'), 'secondary'
PENDING = 10, _('Pending'), ColorEnum.secondary
# Item is to be returned to customer, no other action
RETURN = 20, _('Return'), 'success'
RETURN = 20, _('Return'), ColorEnum.success
# Item is to be repaired, and returned to customer
REPAIR = 30, _('Repair'), 'primary'
REPAIR = 30, _('Repair'), ColorEnum.primary
# Item is to be replaced (new item shipped)
REPLACE = 40, _('Replace'), 'warning'
REPLACE = 40, _('Replace'), ColorEnum.warning
# Item is to be refunded (cannot be repaired)
REFUND = 50, _('Refund'), 'info'
REFUND = 50, _('Refund'), ColorEnum.info
# Item is rejected
REJECT = 60, _('Reject'), 'danger'
REJECT = 60, _('Reject'), ColorEnum.danger

View File

@ -122,7 +122,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% status_label 'purchase_order' order.status %}
{% display_status_label 'purchase_order' order.status_custom_key order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}

View File

@ -115,7 +115,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% status_label 'return_order' order.status %}
{% display_status_label 'return_order' order.status_custom_key order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}

View File

@ -124,7 +124,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td>
<td>
{% status_label 'sales_order' order.status %}
{% display_status_label 'sales_order' order.status_custom_key order.status %}
{% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %}

View File

@ -125,6 +125,7 @@ class LabelMixinTests(PrintTestMixins, InvenTreeAPITestCase):
self.assertGreater(len(plugins), 0)
plugin = registry.get_plugin('samplelabelprinter')
self.assertIsNotNone(plugin)
config = plugin.plugin_config()
# Ensure that the plugin is not active

View File

@ -491,7 +491,10 @@ class PrintTestMixins:
def do_activate_plugin(self):
"""Activate the 'samplelabel' plugin."""
config = registry.get_plugin(self.plugin_ref).plugin_config()
plugin = registry.get_plugin(self.plugin_ref)
self.assertIsNotNone(plugin)
config = plugin.plugin_config()
self.assertIsNotNone(config)
config.active = True
config.save()

View File

@ -142,6 +142,7 @@ class StockItemResource(InvenTreeResource):
'barcode_hash',
'barcode_data',
'owner',
'status_custom_key',
]
id = Field(

View File

@ -0,0 +1,38 @@
# Generated by Django 4.2.14 on 2024-08-07 22:40
import django.core.validators
from django.db import migrations
import generic.states
import generic.states.fields
import InvenTree.status_codes
class Migration(migrations.Migration):
dependencies = [
("stock", "0112_alter_stocklocation_custom_icon_and_more"),
]
operations = [
migrations.AddField(
model_name="stockitem",
name="status_custom_key",
field=generic.states.fields.ExtraInvenTreeCustomStatusModelField(
blank=True,
default=None,
help_text="Additional status information for this item",
null=True,
verbose_name="Custom status key",
),
),
migrations.AlterField(
model_name="stockitem",
name="status",
field=generic.states.fields.InvenTreeCustomStatusModelField(
choices=InvenTree.status_codes.StockStatus.items(),
default=10,
validators=[django.core.validators.MinValueValidator(0)],
),
),
]

View File

@ -37,13 +37,18 @@ from build import models as BuildModels
from common.icons import validate_icon
from common.settings import get_global_setting
from company import models as CompanyModels
from generic.states.fields import InvenTreeCustomStatusModelField
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
from order.status_codes import SalesOrderStatusGroups
from InvenTree.status_codes import (
SalesOrderStatusGroups,
StockHistoryCode,
StockStatus,
StockStatusGroups,
)
from part import models as PartModels
from plugin.events import trigger_event
from stock import models as StockModels
from stock.generators import generate_batch_code
from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups
from users.models import Owner
logger = logging.getLogger('inventree')
@ -940,7 +945,7 @@ class StockItem(
help_text=_('Delete this Stock Item when stock is depleted'),
)
status = models.PositiveIntegerField(
status = InvenTreeCustomStatusModelField(
default=StockStatus.OK.value,
choices=StockStatus.items(),
validators=[MinValueValidator(0)],

View File

@ -27,6 +27,7 @@ import part.serializers as part_serializers
import stock.filters
import stock.status_codes
from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
@ -238,7 +239,10 @@ class StockItemTestResultSerializer(
)
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(
required=False
required=False,
allow_null=True,
label=_('Attachment'),
help_text=_('Test result attachment'),
)
def validate(self, data):
@ -326,7 +330,9 @@ class StockItemSerializerBrief(
@register_importer()
class StockItemSerializer(
DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeTagModelSerializer
DataImportExportSerializerMixin,
InvenTreeCustomStatusSerializerMixin,
InvenTree.serializers.InvenTreeTagModelSerializer,
):
"""Serializer for a StockItem.
@ -373,6 +379,7 @@ class StockItemSerializer(
'serial',
'status',
'status_text',
'status_custom_key',
'stocktake_date',
'supplier_part',
'sku',

View File

@ -2,24 +2,28 @@
from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode
from generic.states import ColorEnum, StatusCode
class StockStatus(StatusCode):
"""Status codes for Stock."""
OK = 10, _('OK'), 'success' # Item is OK
ATTENTION = 50, _('Attention needed'), 'warning' # Item requires attention
DAMAGED = 55, _('Damaged'), 'warning' # Item is damaged
DESTROYED = 60, _('Destroyed'), 'danger' # Item is destroyed
REJECTED = 65, _('Rejected'), 'danger' # Item is rejected
LOST = 70, _('Lost'), 'dark' # Item has been lost
OK = 10, _('OK'), ColorEnum.success # Item is OK
ATTENTION = 50, _('Attention needed'), ColorEnum.warning # Item requires attention
DAMAGED = 55, _('Damaged'), ColorEnum.warning # Item is damaged
DESTROYED = 60, _('Destroyed'), ColorEnum.danger # Item is destroyed
REJECTED = 65, _('Rejected'), ColorEnum.danger # Item is rejected
LOST = 70, _('Lost'), ColorEnum.dark # Item has been lost
QUARANTINED = (
75,
_('Quarantined'),
'info',
ColorEnum.info,
) # Item has been quarantined and is unavailable
RETURNED = 85, _('Returned'), 'warning' # Item has been returned from a customer
RETURNED = (
85,
_('Returned'),
ColorEnum.warning,
) # Item has been returned from a customer
class StockStatusGroups:

View File

@ -425,7 +425,7 @@
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% status_label 'stock' item.status %}</td>
<td>{% display_status_label 'stock' item.status_custom_key item.status %}</td>
</tr>
{% if item.expiry_date %}
<tr>

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
from enum import IntEnum
import django.http
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.urls import reverse
@ -17,7 +18,7 @@ from rest_framework import status
import build.models
import company.models
import part.models
from common.models import InvenTreeSetting
from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting
from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part, PartTestTemplate
from stock.models import (
@ -925,6 +926,108 @@ class StockItemListTest(StockAPITestCase):
)
class CustomStockItemStatusTest(StockAPITestCase):
"""Tests for custom stock item statuses."""
list_url = reverse('api-stock-list')
def setUp(self):
"""Setup for all tests."""
super().setUp()
self.status = InvenTreeCustomUserStateModel.objects.create(
key=11,
name='OK - advanced',
label='OK - adv.',
color='secondary',
logical_key=10,
model=ContentType.objects.get(model='stockitem'),
reference_status='StockStatus',
)
self.status2 = InvenTreeCustomUserStateModel.objects.create(
key=51,
name='attention 2',
label='attention 2',
color='secondary',
logical_key=50,
model=ContentType.objects.get(model='stockitem'),
reference_status='StockStatus',
)
def test_custom_status(self):
"""Tests interaction with states."""
# Create a stock item with the custom status code via the API
response = self.post(
self.list_url,
{
'name': 'Test Type 1',
'description': 'Test desc 1',
'quantity': 1,
'part': 1,
'status_custom_key': self.status.key,
},
expected_code=201,
)
self.assertEqual(response.data['status'], self.status.logical_key)
self.assertEqual(response.data['status_custom_key'], self.status.key)
pk = response.data['pk']
# Update the stock item with another custom status code via the API
response = self.patch(
reverse('api-stock-detail', kwargs={'pk': pk}),
{'status_custom_key': self.status2.key},
expected_code=200,
)
self.assertEqual(response.data['status'], self.status2.logical_key)
self.assertEqual(response.data['status_custom_key'], self.status2.key)
# Try if status_custom_key is rewrite with status bying set
response = self.patch(
reverse('api-stock-detail', kwargs={'pk': pk}),
{'status': self.status.logical_key},
expected_code=200,
)
self.assertEqual(response.data['status'], self.status.logical_key)
self.assertEqual(response.data['status_custom_key'], self.status.logical_key)
# Create a stock item with a normal status code via the API
response = self.post(
self.list_url,
{
'name': 'Test Type 1',
'description': 'Test desc 1',
'quantity': 1,
'part': 1,
'status_key': self.status.key,
},
expected_code=201,
)
self.assertEqual(response.data['status'], self.status.logical_key)
self.assertEqual(response.data['status_custom_key'], self.status.logical_key)
# Test case with wrong key
response = self.patch(
reverse('api-stock-detail', kwargs={'pk': pk}),
{'status_custom_key': 23456789},
expected_code=400,
)
self.assertIn('Invalid choice', str(response.data))
def test_options(self):
"""Test the StockItem OPTIONS endpoint to contain custom StockStatuses."""
response = self.options(self.list_url)
self.assertEqual(response.status_code, 200)
# Check that the response contains the custom StockStatuses
actions = response.data['actions']['POST']
self.assertIn('status_custom_key', actions)
status_custom_key = actions['status_custom_key']
self.assertEqual(len(status_custom_key['choices']), 10)
status = status_custom_key['choices'][1]
self.assertEqual(status['value'], self.status.key)
self.assertEqual(status['display_name'], self.status.label)
class StockItemTest(StockAPITestCase):
"""Series of API tests for the StockItem API."""

View File

@ -615,7 +615,7 @@ function completeBuildOutputs(build_id, outputs, options={}) {
method: 'POST',
preFormContent: html,
fields: {
status: {},
status_custom_key: {},
location: {
filters: {
structural: false,
@ -644,7 +644,7 @@ function completeBuildOutputs(build_id, outputs, options={}) {
// Extract data elements from the form
var data = {
outputs: [],
status: getFormFieldValue('status', {}, opts),
status_custom_key: getFormFieldValue('status_custom_key', {}, opts),
location: getFormFieldValue('location', {}, opts),
notes: getFormFieldValue('notes', {}, opts),
accept_incomplete_allocation: getFormFieldValue('accept_incomplete_allocation', {type: 'boolean'}, opts),
@ -1153,7 +1153,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
if (row.build_detail) {
html += `- <small>${row.build_detail.title}</small>`;
html += buildStatusDisplay(row.build_detail.status, {
html += buildStatusDisplay(row.build_detail.status_custom_key, {
classes: 'float-right',
});
}
@ -1556,7 +1556,7 @@ function loadBuildOutputTable(build_info, options={}) {
text += ` <small>({% trans "Batch" %}: ${row.batch})</small>`;
}
text += stockStatusDisplay(row.status, {classes: 'float-right'});
text += stockStatusDisplay(row.status_custom_key, {classes: 'float-right'});
return text;
}
@ -2362,7 +2362,7 @@ function loadBuildTable(table, options) {
}
},
{
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
sortable: true,
formatter: function(value) {

View File

@ -1761,7 +1761,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
var html = renderLink(order.reference, `/order/purchase-order/${order.pk}/`);
html += purchaseOrderStatusDisplay(
order.status,
order.status_custom_key,
{
classes: 'float-right',
}

View File

@ -1788,12 +1788,12 @@ function loadPurchaseOrderTable(table, options) {
}
},
{
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
switchable: true,
sortable: true,
formatter: function(value, row) {
return purchaseOrderStatusDisplay(row.status);
return purchaseOrderStatusDisplay(row.status_custom_key);
}
},
{

View File

@ -326,10 +326,10 @@ function loadReturnOrderTable(table, options={}) {
},
{
sortable: true,
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
formatter: function(value, row) {
return returnOrderStatusDisplay(row.status);
return returnOrderStatusDisplay(row.status_custom_key);
}
},
{

View File

@ -851,10 +851,10 @@ function loadSalesOrderTable(table, options) {
},
{
sortable: true,
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
formatter: function(value, row) {
return salesOrderStatusDisplay(row.status);
return salesOrderStatusDisplay(row.status_custom_key);
}
},
{

View File

@ -380,7 +380,7 @@ function stockItemFields(options={}) {
batch: {
icon: 'fa-layer-group',
},
status: {},
status_custom_key: {},
expiry_date: {
icon: 'fa-calendar-alt',
},
@ -698,7 +698,7 @@ function assignStockToCustomer(items, options={}) {
var thumbnail = thumbnailImage(part.thumbnail || part.image);
var status = stockStatusDisplay(item.status, {classes: 'float-right'});
var status = stockStatusDisplay(item.status_custom_key, {classes: 'float-right'});
var quantity = '';
@ -879,7 +879,7 @@ function mergeStockItems(items, options={}) {
quantity = `{% trans "Quantity" %}: ${item.quantity}`;
}
quantity += stockStatusDisplay(item.status, {classes: 'float-right'});
quantity += stockStatusDisplay(item.status_custom_key, {classes: 'float-right'});
let buttons = wrapButtons(
makeIconButton(
@ -1113,7 +1113,7 @@ function adjustStock(action, items, options={}) {
var thumb = thumbnailImage(item.part_detail.thumbnail || item.part_detail.image);
var status = stockStatusDisplay(item.status, {
var status = stockStatusDisplay(item.status_custom_key, {
classes: 'float-right'
});
@ -1922,7 +1922,8 @@ function makeStockActions(table) {
}
},
{
label: 'status',
label: 'status_custom_key',
icon: 'fa-info-circle icon-blue',
title: '{% trans "Change stock status" %}',
permission: 'stock.change',
@ -2257,7 +2258,7 @@ function loadStockTable(table, options) {
columns.push(col);
col = {
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
formatter: function(value) {
return stockStatusDisplay(value);
@ -3075,11 +3076,11 @@ function loadStockTrackingTable(table, options) {
}
// Status information
if (details.status) {
if (details.status_custom_key) {
html += `<tr><th>{% trans "Status" %}</td>`;
html += '<td>';
html += stockStatusDisplay(details.status);
html += stockStatusDisplay(details.status_custom_key);
html += '</td></tr>';
}
@ -3200,7 +3201,7 @@ function loadInstalledInTable(table, options) {
}
},
{
field: 'status',
field: 'status_custom_key',
title: '{% trans "Status" %}',
formatter: function(value) {
return stockStatusDisplay(value);
@ -3401,7 +3402,7 @@ function setStockStatus(items, options={}) {
method: 'POST',
preFormContent: html,
fields: {
status: {},
status_custom_key: {},
note: {},
},
processBeforeUpload: function(data) {

View File

@ -345,6 +345,7 @@ class RuleSet(models.Model):
'common_projectcode',
'common_webhookendpoint',
'common_webhookmessage',
'common_inventreecustomuserstatemodel',
'users_owner',
# Third-party tables
'error_report_error',

View File

@ -10,59 +10,74 @@ build==1.2.1 \
--hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \
--hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4
# via pip-tools
cffi==1.16.0 \
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
--hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
--hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
--hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
--hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
--hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
--hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
--hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
--hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
--hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
--hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
--hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
--hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
--hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
--hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
--hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
--hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
--hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
--hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
cffi==1.17.0 \
--hash=sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f \
--hash=sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab \
--hash=sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499 \
--hash=sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058 \
--hash=sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693 \
--hash=sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb \
--hash=sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377 \
--hash=sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885 \
--hash=sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2 \
--hash=sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401 \
--hash=sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4 \
--hash=sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b \
--hash=sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59 \
--hash=sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f \
--hash=sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c \
--hash=sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555 \
--hash=sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa \
--hash=sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424 \
--hash=sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb \
--hash=sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2 \
--hash=sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8 \
--hash=sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e \
--hash=sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9 \
--hash=sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82 \
--hash=sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828 \
--hash=sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759 \
--hash=sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc \
--hash=sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118 \
--hash=sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf \
--hash=sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932 \
--hash=sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a \
--hash=sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29 \
--hash=sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206 \
--hash=sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2 \
--hash=sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c \
--hash=sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c \
--hash=sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0 \
--hash=sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a \
--hash=sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195 \
--hash=sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6 \
--hash=sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9 \
--hash=sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc \
--hash=sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb \
--hash=sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0 \
--hash=sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7 \
--hash=sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb \
--hash=sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a \
--hash=sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492 \
--hash=sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720 \
--hash=sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42 \
--hash=sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7 \
--hash=sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d \
--hash=sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d \
--hash=sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb \
--hash=sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4 \
--hash=sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2 \
--hash=sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b \
--hash=sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8 \
--hash=sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e \
--hash=sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204 \
--hash=sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3 \
--hash=sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150 \
--hash=sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4 \
--hash=sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76 \
--hash=sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e \
--hash=sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb \
--hash=sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91
# via
# -c src/backend/requirements.txt
# cryptography
@ -168,93 +183,108 @@ click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via pip-tools
coverage[toml]==7.5.4 \
--hash=sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f \
--hash=sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d \
--hash=sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747 \
--hash=sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f \
--hash=sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d \
--hash=sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f \
--hash=sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47 \
--hash=sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e \
--hash=sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba \
--hash=sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c \
--hash=sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b \
--hash=sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4 \
--hash=sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7 \
--hash=sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555 \
--hash=sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233 \
--hash=sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace \
--hash=sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805 \
--hash=sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136 \
--hash=sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4 \
--hash=sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d \
--hash=sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806 \
--hash=sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99 \
--hash=sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8 \
--hash=sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b \
--hash=sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5 \
--hash=sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da \
--hash=sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0 \
--hash=sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078 \
--hash=sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f \
--hash=sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029 \
--hash=sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353 \
--hash=sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638 \
--hash=sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9 \
--hash=sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f \
--hash=sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7 \
--hash=sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3 \
--hash=sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e \
--hash=sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016 \
--hash=sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088 \
--hash=sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4 \
--hash=sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882 \
--hash=sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7 \
--hash=sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53 \
--hash=sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d \
--hash=sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080 \
--hash=sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5 \
--hash=sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d \
--hash=sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c \
--hash=sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8 \
--hash=sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633 \
--hash=sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9 \
--hash=sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c
coverage[toml]==7.6.1 \
--hash=sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca \
--hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \
--hash=sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6 \
--hash=sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989 \
--hash=sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c \
--hash=sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b \
--hash=sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223 \
--hash=sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f \
--hash=sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56 \
--hash=sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3 \
--hash=sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8 \
--hash=sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb \
--hash=sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388 \
--hash=sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0 \
--hash=sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a \
--hash=sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8 \
--hash=sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f \
--hash=sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a \
--hash=sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962 \
--hash=sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8 \
--hash=sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391 \
--hash=sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc \
--hash=sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2 \
--hash=sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155 \
--hash=sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb \
--hash=sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0 \
--hash=sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c \
--hash=sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a \
--hash=sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004 \
--hash=sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060 \
--hash=sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232 \
--hash=sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93 \
--hash=sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129 \
--hash=sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163 \
--hash=sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de \
--hash=sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6 \
--hash=sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23 \
--hash=sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569 \
--hash=sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d \
--hash=sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778 \
--hash=sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d \
--hash=sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36 \
--hash=sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a \
--hash=sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6 \
--hash=sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34 \
--hash=sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704 \
--hash=sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106 \
--hash=sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9 \
--hash=sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862 \
--hash=sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b \
--hash=sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255 \
--hash=sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16 \
--hash=sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3 \
--hash=sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133 \
--hash=sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb \
--hash=sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657 \
--hash=sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d \
--hash=sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca \
--hash=sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36 \
--hash=sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c \
--hash=sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e \
--hash=sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff \
--hash=sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7 \
--hash=sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5 \
--hash=sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02 \
--hash=sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c \
--hash=sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df \
--hash=sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3 \
--hash=sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a \
--hash=sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959 \
--hash=sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234 \
--hash=sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc
# via -r src/backend/requirements-dev.in
cryptography==42.0.8 \
--hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \
--hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \
--hash=sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b \
--hash=sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c \
--hash=sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1 \
--hash=sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648 \
--hash=sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949 \
--hash=sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba \
--hash=sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c \
--hash=sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9 \
--hash=sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d \
--hash=sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c \
--hash=sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e \
--hash=sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2 \
--hash=sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d \
--hash=sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7 \
--hash=sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70 \
--hash=sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2 \
--hash=sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7 \
--hash=sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14 \
--hash=sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe \
--hash=sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e \
--hash=sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71 \
--hash=sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961 \
--hash=sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7 \
--hash=sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c \
--hash=sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28 \
--hash=sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842 \
--hash=sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902 \
--hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \
--hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \
--hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e
cryptography==43.0.0 \
--hash=sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709 \
--hash=sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069 \
--hash=sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2 \
--hash=sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b \
--hash=sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e \
--hash=sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70 \
--hash=sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778 \
--hash=sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22 \
--hash=sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895 \
--hash=sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf \
--hash=sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431 \
--hash=sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f \
--hash=sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947 \
--hash=sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74 \
--hash=sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc \
--hash=sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66 \
--hash=sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66 \
--hash=sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf \
--hash=sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f \
--hash=sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5 \
--hash=sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e \
--hash=sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f \
--hash=sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55 \
--hash=sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1 \
--hash=sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47 \
--hash=sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5 \
--hash=sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0
# via
# -c src/backend/requirements.txt
# pdfminer-six
@ -287,13 +317,13 @@ filelock==3.15.4 \
--hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \
--hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7
# via virtualenv
identify==2.5.36 \
--hash=sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa \
--hash=sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d
identify==2.6.0 \
--hash=sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf \
--hash=sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0
# via pre-commit
importlib-metadata==7.1.0 \
--hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \
--hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2
importlib-metadata==8.0.0 \
--hash=sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f \
--hash=sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812
# via
# -c src/backend/requirements.txt
# build
@ -311,9 +341,9 @@ packaging==24.1 \
# via
# -c src/backend/requirements.txt
# build
pdfminer-six==20231228 \
--hash=sha256:6004da3ad1a7a4d45930cb950393df89b068e73be365a6ff64a838d37bcb08c4 \
--hash=sha256:e8d3c3310e6fbc1fe414090123ab01351634b4ecb021232206c4c9a8ca3e3b8f
pdfminer-six==20240706 \
--hash=sha256:c631a46d5da957a9ffe4460c5dce21e8431dabb615fee5f9f4400603a58d95a6 \
--hash=sha256:f4f70e74174b4b3542fcb8406a210b6e2e27cd0f0b5fd04534a8cc0d8951e38c
# via -r src/backend/requirements-dev.in
pip==24.2 \
--hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \
@ -327,9 +357,9 @@ platformdirs==4.2.2 \
--hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
--hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
# via virtualenv
pre-commit==3.7.1 \
--hash=sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a \
--hash=sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5
pre-commit==3.8.0 \
--hash=sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af \
--hash=sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f
# via -r src/backend/requirements-dev.in
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
@ -343,71 +373,73 @@ pyproject-hooks==1.1.0 \
# via
# build
# pip-tools
pyyaml==6.0.1 \
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
--hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
--hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
--hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
--hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
--hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
--hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
--hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
--hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via
# -c src/backend/requirements.txt
# pre-commit
setuptools==72.1.0 \
--hash=sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1 \
--hash=sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements-dev.in
# pip-tools
sqlparse==0.5.0 \
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
sqlparse==0.5.1 \
--hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \
--hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e
# via
# -c src/backend/requirements.txt
# django
@ -415,6 +447,7 @@ tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via
# -c src/backend/requirements.txt
# build
# coverage
# pip-tools
@ -429,13 +462,13 @@ virtualenv==20.26.3 \
--hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \
--hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589
# via pre-commit
wheel==0.43.0 \
--hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \
--hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via pip-tools
zipp==3.19.2 \
--hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \
--hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
# via
# -c src/backend/requirements.txt
# importlib-metadata

View File

@ -62,3 +62,6 @@ opentelemetry-exporter-otlp
opentelemetry-instrumentation-django
opentelemetry-instrumentation-requests
opentelemetry-instrumentation-redis
# pinned sub-deps
pydyf==0.10.0 # Fixed 2024-08-22 see https://github.com/inventree/InvenTree/pull/7961/files

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
import { t } from '@lingui/macro';
import { InvenTreeIcon } from '../../functions/icons';
import { ActionButton } from './ActionButton';
export default function RemoveRowButton({
onClick,
tooltip = t`Remove this row`
}: {
onClick: () => void;
tooltip?: string;
}) {
return (
<ActionButton
onClick={onClick}
icon={<InvenTreeIcon icon="square_x" />}
tooltip={tooltip}
tooltipAlignment="top"
color="red"
/>
);
}

View File

@ -502,7 +502,20 @@ export function ApiForm({
}
if (typeof v === 'object' && Array.isArray(v)) {
form.setError(path, { message: v.join(', ') });
if (field?.field_type == 'table') {
// Special handling for "table" fields - they have nested errors
v.forEach((item: any, idx: number) => {
for (const [key, value] of Object.entries(item)) {
const path: string = `${k}.${idx}.${key}`;
if (Array.isArray(value)) {
form.setError(path, { message: value.join(', ') });
}
}
});
} else {
// Standard error handling for other fields
form.setError(path, { message: v.join(', ') });
}
} else {
processErrors(v, path);
}

View File

@ -1,37 +1,53 @@
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { ApiFormField, ApiFormFieldType } from './fields/ApiFormField';
export function StandaloneField({
fieldDefinition,
fieldName = 'field',
defaultValue,
hideLabels
hideLabels,
error
}: {
fieldDefinition: ApiFormFieldType;
fieldName?: string;
defaultValue?: any;
hideLabels?: boolean;
error?: string;
}) {
// Field must have a defined name
const name = useMemo(() => fieldName ?? 'field', [fieldName]);
const defaultValues = useMemo(() => {
if (defaultValue)
return {
field: defaultValue
[name]: defaultValue
};
return {};
}, [defaultValue]);
const form = useForm<{}>({
const form = useForm({
criteriaMode: 'all',
defaultValues
});
useEffect(() => {
form.clearErrors();
if (!!error) {
form.setError(name, { message: error });
}
}, [form, error]);
return (
<FormProvider {...form}>
<ApiFormField
fieldName="field"
fieldName={name}
definition={fieldDefinition}
control={form.control}
hideLabels={hideLabels}
setFields={undefined}
/>
</FormProvider>
);

View File

@ -204,8 +204,8 @@ export function ApiFormField({
}, [value]);
// Construct the individual field
function buildField() {
switch (definition.field_type) {
const fieldInstance = useMemo(() => {
switch (fieldDefinition.field_type) {
case 'related field':
return (
<RelatedModelField
@ -236,7 +236,7 @@ export function ApiFormField({
checked={booleanValue}
ref={ref}
id={fieldId}
aria-label={`boolean-field-${field.name}`}
aria-label={`boolean-field-${fieldName}`}
radius="lg"
size="sm"
error={error?.message}
@ -322,16 +322,30 @@ export function ApiFormField({
</Alert>
);
}
}
}, [
booleanValue,
control,
controller,
field,
fieldId,
fieldName,
fieldDefinition,
numericalValue,
onChange,
reducedDefinition,
ref,
setFields,
value
]);
if (definition.hidden) {
if (fieldDefinition.hidden) {
return null;
}
return (
<Stack>
{definition.preFieldContent}
{buildField()}
{fieldInstance}
{definition.postFieldContent}
</Stack>
);

View File

@ -207,7 +207,7 @@ export function RelatedModelField({
setPk(_pk);
// Run custom callback for this field (if provided)
definition.onValueChange?.(_pk, value.data ?? {});
definition.onValueChange?.(_pk, value?.data ?? {});
},
[field.onChange, definition]
);

View File

@ -1,12 +1,21 @@
import { Trans, t } from '@lingui/macro';
import { Container, Group, Table } from '@mantine/core';
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { InvenTreeIcon } from '../../../functions/icons';
import { StandaloneField } from '../StandaloneField';
import { ApiFormFieldType } from './ApiFormField';
export interface TableFieldRowProps {
item: any;
idx: number;
rowErrors: any;
control: UseControllerReturn<FieldValues, any>;
changeFn: (idx: number, key: string, value: any) => void;
removeFn: (idx: number) => void;
}
export function TableField({
definition,
fieldName,
@ -25,6 +34,7 @@ export function TableField({
const onRowFieldChange = (idx: number, key: string, value: any) => {
const val = field.value;
val[idx][key] = value;
field.onChange(val);
};
@ -34,6 +44,16 @@ export function TableField({
field.onChange(val);
};
// Extract errors associated with the current row
const rowErrors = useCallback(
(idx: number) => {
if (Array.isArray(error)) {
return error[idx];
}
},
[error]
);
return (
<Table highlightOnHover striped aria-label={`table-field-${field.name}`}>
<Table.Thead>
@ -49,18 +69,21 @@ export function TableField({
// Table fields require render function
if (!definition.modelRenderer) {
return (
<Table.Tr>{t`modelRenderer entry required for tables`}</Table.Tr>
<Table.Tr key="table-row-no-renderer">{t`modelRenderer entry required for tables`}</Table.Tr>
);
}
return definition.modelRenderer({
item: item,
idx: idx,
rowErrors: rowErrors(idx),
control: control,
changeFn: onRowFieldChange,
removeFn: removeRow
});
})
) : (
<Table.Tr>
<Table.Tr key="table-row-no-entries">
<Table.Td
style={{ textAlign: 'center' }}
colSpan={definition.headers?.length}
@ -92,11 +115,13 @@ export function TableFieldExtraRow({
fieldDefinition,
defaultValue,
emptyValue,
error,
onValueChange
}: {
visible: boolean;
fieldDefinition: ApiFormFieldType;
defaultValue?: any;
error?: string;
emptyValue?: any;
onValueChange: (value: any) => void;
}) {
@ -129,6 +154,7 @@ export function TableFieldExtraRow({
<StandaloneField
fieldDefinition={field}
defaultValue={defaultValue}
error={error}
/>
</Group>
</Table.Td>

View File

@ -20,7 +20,7 @@ import { ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType';
import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons';
import { InvenTreeQRCode } from './QRCode';
import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode';
export type ActionDropdownItem = {
icon?: ReactNode;
@ -112,69 +112,91 @@ export function ActionDropdown({
// Dropdown menu for barcode actions
export function BarcodeActionDropdown({
actions
}: {
actions: ActionDropdownItem[];
}) {
model,
pk,
hash = null,
actions = [],
perm: permission = true
}: Readonly<{
model: ModelType;
pk: number;
hash?: boolean | null;
actions?: ActionDropdownItem[];
perm?: boolean;
}>) {
const hidden = hash === null;
const prop = { model, pk, hash };
return (
<ActionDropdown
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={actions}
actions={[
GeneralBarcodeAction({
mdl_prop: prop,
title: t`View`,
icon: <IconQrcode />,
tooltip: t`View barcode`,
ChildItem: InvenTreeQRCode
}),
GeneralBarcodeAction({
hidden: hidden || hash || !permission,
mdl_prop: prop,
title: t`Link Barcode`,
icon: <IconLink />,
tooltip: t`Link a custom barcode to this item`,
ChildItem: QRCodeLink
}),
GeneralBarcodeAction({
hidden: hidden || !hash || !permission,
mdl_prop: prop,
title: t`Unlink Barcode`,
icon: <IconUnlink />,
tooltip: t`Unlink custom barcode`,
ChildItem: QRCodeUnlink
}),
...actions
]}
/>
);
}
// Common action button for viewing a barcode
export function ViewBarcodeAction({
hidden = false,
model,
pk
}: {
hidden?: boolean;
export type QrCodeType = {
model: ModelType;
pk: number;
hash?: boolean | null;
};
function GeneralBarcodeAction({
hidden = false,
mdl_prop,
title,
icon,
tooltip,
ChildItem
}: {
hidden?: boolean;
mdl_prop: QrCodeType;
title: string;
icon: ReactNode;
tooltip: string;
ChildItem: any;
}): ActionDropdownItem {
const onClick = () => {
modals.open({
title: t`View Barcode`,
children: <InvenTreeQRCode model={model} pk={pk} />
title: title,
children: <ChildItem mdl_prop={mdl_prop} />
});
};
return {
icon: <IconQrcode />,
name: t`View`,
tooltip: t`View barcode`,
icon: icon,
name: title,
tooltip: tooltip,
onClick: onClick,
hidden: hidden
};
}
// Common action button for linking a custom barcode
export function LinkBarcodeAction(
props: ActionDropdownItem
): ActionDropdownItem {
return {
...props,
icon: <IconLink />,
name: t`Link Barcode`,
tooltip: t`Link custom barcode`
};
}
// Common action button for un-linking a custom barcode
export function UnlinkBarcodeAction(
props: ActionDropdownItem
): ActionDropdownItem {
return {
...props,
icon: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode`
};
}
// Common action button for editing an item
export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem {
return {

View File

@ -18,8 +18,11 @@ export function ProgressBar(props: Readonly<ProgressBarProps>) {
let maximum = props.maximum ?? 100;
let value = Math.max(props.value, 0);
// Calculate progress as a percentage of the maximum value
return Math.min(100, (value / maximum) * 100);
if (maximum == 0) {
return 0;
}
return (value / maximum) * 100;
}, [props]);
return (

View File

@ -1,24 +1,28 @@
import { Trans, t } from '@lingui/macro';
import {
Alert,
Box,
Button,
Code,
Group,
Image,
Select,
Skeleton,
Stack,
Text
Text,
TextInput
} from '@mantine/core';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton';
import { QrCodeType } from './ActionDropdown';
type QRCodeProps = {
ecl?: 'L' | 'M' | 'Q' | 'H';
@ -51,15 +55,13 @@ export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => {
};
type InvenTreeQRCodeProps = {
model: ModelType;
pk: number;
mdl_prop: QrCodeType;
showEclSelector?: boolean;
} & Omit<QRCodeProps, 'data'>;
export const InvenTreeQRCode = ({
mdl_prop,
showEclSelector = true,
model,
pk,
ecl: eclProp = 'Q',
...props
}: InvenTreeQRCodeProps) => {
@ -71,11 +73,11 @@ export const InvenTreeQRCode = ({
}, [eclProp]);
const { data } = useQuery({
queryKey: ['qr-code', model, pk],
queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk],
queryFn: async () => {
const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), {
model,
pk
model: mdl_prop.model,
pk: mdl_prop.pk
});
return res.data?.barcode as string;
@ -94,6 +96,15 @@ export const InvenTreeQRCode = ({
return (
<Stack>
{mdl_prop.hash ? (
<Alert variant="outline" color="red" title={t`Custom bascode`}>
<Trans>
A custom barcode is registered for this item. The shown code is not
that custom barcode.
</Trans>
</Alert>
) : null}
<QRCode data={data} ecl={ecl} {...props} />
{data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && (
@ -128,3 +139,55 @@ export const InvenTreeQRCode = ({
</Stack>
);
};
export const QRCodeLink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
const [barcode, setBarcode] = useState('');
function linkBarcode() {
api
.post(apiUrl(ApiEndpoints.barcode_link), {
[mdl_prop.model]: mdl_prop.pk,
barcode: barcode
})
.then((response) => {
modals.closeAll();
location.reload();
});
}
return (
<Box>
<TextInput
label={t`Barcode`}
value={barcode}
onChange={(event) => setBarcode(event.currentTarget.value)}
placeholder={t`Scan barcode data here using barcode scanner`}
/>
<Button color="green" onClick={linkBarcode} mt="lg" fullWidth>
<Trans>Link</Trans>
</Button>
</Box>
);
};
export const QRCodeUnlink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
function unlinkBarcode() {
api
.post(apiUrl(ApiEndpoints.barcode_unlink), {
[mdl_prop.model]: mdl_prop.pk
})
.then((response) => {
modals.closeAll();
location.reload();
});
}
return (
<Box>
<Text>
<Trans>This will remove the link to the associated barcode</Trans>
</Text>
<Button color="red" onClick={unlinkBarcode}>
<Trans>Unlink Barcode</Trans>
</Button>
</Box>
);
};

View File

@ -19,7 +19,7 @@ export function RenderBuildOrder(
primary={instance.reference}
secondary={instance.title}
suffix={StatusRenderer({
status: instance.status,
status: instance.status_custom_key,
type: ModelType.build
})}
image={instance.part_detail?.thumbnail || instance.part_detail?.image}
@ -39,7 +39,7 @@ export function RenderBuildLine({
primary={instance.part_detail.full_name}
secondary={instance.quantity}
suffix={StatusRenderer({
status: instance.status,
status: instance.status_custom_key,
type: ModelType.build
})}
image={instance.part_detail.thumbnail || instance.part_detail.image}

View File

@ -15,6 +15,12 @@ export function RenderProjectCode({
);
}
export function RenderContentType({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return instance && <RenderInlineModel primary={instance.app_labeled_name} />;
}
export function RenderImportSession({
instance
}: {

View File

@ -16,7 +16,11 @@ import {
RenderManufacturerPart,
RenderSupplierPart
} from './Company';
import { RenderImportSession, RenderProjectCode } from './Generic';
import {
RenderContentType,
RenderImportSession,
RenderProjectCode
} from './Generic';
import { ModelInformationDict } from './ModelType';
import {
RenderPurchaseOrder,
@ -87,7 +91,8 @@ const RendererLookup: EnumDictionary<
[ModelType.importsession]: RenderImportSession,
[ModelType.reporttemplate]: RenderReportTemplate,
[ModelType.labeltemplate]: RenderLabelTemplate,
[ModelType.pluginconfig]: RenderPlugin
[ModelType.pluginconfig]: RenderPlugin,
[ModelType.contenttype]: RenderContentType
};
export type RenderInstanceProps = {

View File

@ -241,6 +241,11 @@ export const ModelInformationDict: ModelDict = {
url_overview: '/pluginconfig',
url_detail: '/pluginconfig/:pk/',
api_endpoint: ApiEndpoints.plugin_list
},
contenttype: {
label: t`Content Type`,
label_multiple: t`Content Types`,
api_endpoint: ApiEndpoints.content_type_list
}
};

View File

@ -21,7 +21,7 @@ export function RenderPurchaseOrder(
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
status: instance.status_custom_key,
type: ModelType.purchaseorder
})}
image={supplier.thumnbnail || supplier.image}
@ -49,7 +49,7 @@ export function RenderReturnOrder(
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
status: instance.status_custom_key,
type: ModelType.returnorder
})}
image={customer.thumnbnail || customer.image}
@ -94,7 +94,7 @@ export function RenderSalesOrder(
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
status: instance.status_custom_key,
type: ModelType.salesorder
})}
image={customer.thumnbnail || customer.image}

View File

@ -0,0 +1,19 @@
import { SimpleGrid } from '@mantine/core';
import { FactItem } from './FactItem';
export function FactCollection({
items,
minItems = 3
}: {
items: { title: string; value: any }[];
minItems?: number;
}) {
return (
<SimpleGrid cols={minItems} spacing="xs">
{items.map((item, index) => (
<FactItem key={index} title={item.title} value={item.value} />
))}
</SimpleGrid>
);
}

View File

@ -0,0 +1,14 @@
import { Paper, Stack, Text } from '@mantine/core';
import { StylishText } from '../items/StylishText';
export function FactItem({ title, value }: { title: string; value: number }) {
return (
<Paper p="md" shadow="xs">
<Stack gap="xs">
<StylishText size="md">{title}</StylishText>
<Text>{value}</Text>
</Stack>
</Paper>
);
}

View File

@ -39,14 +39,18 @@ export enum ApiEndpoints {
settings_global_list = 'settings/global/',
settings_user_list = 'settings/user/',
barcode = 'barcode/',
barcode_link = 'barcode/link/',
barcode_unlink = 'barcode/unlink/',
generate_barcode = 'barcode/generate/',
news = 'news/',
global_status = 'generic/status/',
custom_state_list = 'generic/status/custom/',
version = 'version/',
license = 'license/',
sso_providers = 'auth/providers/',
group_list = 'user/group/',
owner_list = 'user/owner/',
content_type_list = 'contenttype/',
icons = 'icons/',
// Data import endpoints
@ -70,6 +74,9 @@ export enum ApiEndpoints {
build_output_create = 'build/:id/create-output/',
build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-outputs/',
build_order_auto_allocate = 'build/:id/auto-allocate/',
build_order_allocate = 'build/:id/allocate/',
build_order_deallocate = 'build/:id/unallocate/',
build_line_list = 'build/line/',
build_item_list = 'build/item/',

View File

@ -31,5 +31,6 @@ export enum ModelType {
group = 'group',
reporttemplate = 'reporttemplate',
labeltemplate = 'labeltemplate',
pluginconfig = 'pluginconfig'
pluginconfig = 'pluginconfig',
contenttype = 'contenttype'
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Alert, Stack, Text } from '@mantine/core';
import { Alert, Stack, Table, Text } from '@mantine/core';
import {
IconCalendar,
IconLink,
@ -10,16 +10,26 @@ import {
IconUsersGroup
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import {
ApiFormFieldSet,
ApiFormFieldType
} from '../components/forms/fields/ApiFormField';
import { TableFieldRowProps } from '../components/forms/fields/TableField';
import { ProgressBar } from '../components/items/ProgressBar';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { resolveItem } from '../functions/conversion';
import { InvenTreeIcon } from '../functions/icons';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
import { useSelectedRows } from '../hooks/UseSelectedRows';
import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState';
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
@ -240,7 +250,7 @@ function buildOutputFormTable(outputs: any[], onRemove: (output: any) => void) {
tooltip={t`Remove output`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
onClick={() => onRemove(record)}
onClick={() => onRemove(record.pk)}
disabled={outputs.length <= 1}
/>
)
@ -259,13 +269,11 @@ export function useCompleteBuildOutputsForm({
outputs: any[];
onFormSuccess: (response: any) => void;
}) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
const [location, setLocation] = useState<number | null>(null);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
const { selectedRows, removeRow } = useSelectedRows({
rows: outputs
});
useEffect(() => {
if (location) {
@ -277,31 +285,21 @@ export function useCompleteBuildOutputsForm({
);
}, [location, build.destination, build.part_detail]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput);
}, [selectedOutputs, removeOutput]);
return buildOutputFormTable(selectedRows, removeRow);
}, [selectedRows, removeRow]);
const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
value: selectedRows.map((output: any) => {
return {
output: output.pk
};
})
},
status: {},
status_custom_key: {},
location: {
filters: {
structural: false
@ -314,7 +312,7 @@ export function useCompleteBuildOutputsForm({
notes: {},
accept_incomplete_allocation: {}
};
}, [selectedOutputs, location]);
}, [selectedRows, location]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_complete, build.pk),
@ -327,6 +325,9 @@ export function useCompleteBuildOutputsForm({
});
}
/*
* Dynamic form for scraping multiple build outputs
*/
export function useScrapBuildOutputsForm({
build,
outputs,
@ -337,21 +338,10 @@ export function useScrapBuildOutputsForm({
onFormSuccess: (response: any) => void;
}) {
const [location, setLocation] = useState<number | null>(null);
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
const { selectedRows, removeRow } = useSelectedRows({
rows: outputs
});
useEffect(() => {
if (location) {
@ -364,14 +354,14 @@ export function useScrapBuildOutputsForm({
}, [location, build.destination, build.part_detail]);
const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput);
}, [selectedOutputs, removeOutput]);
return buildOutputFormTable(selectedRows, removeRow);
}, [selectedRows, removeRow]);
const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
value: selectedRows.map((output: any) => {
return {
output: output.pk,
quantity: output.quantity
@ -387,7 +377,7 @@ export function useScrapBuildOutputsForm({
notes: {},
discard_allocations: {}
};
}, [location, selectedOutputs]);
}, [location, selectedRows]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_scrap, build.pk),
@ -409,21 +399,9 @@ export function useCancelBuildOutputsForm({
outputs: any[];
onFormSuccess: (response: any) => void;
}) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
useEffect(() => {
setSelectedOutputs(outputs);
}, [outputs]);
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
const { selectedRows, removeRow } = useSelectedRows({
rows: outputs
});
const preFormContent = useMemo(() => {
return (
@ -431,23 +409,23 @@ export function useCancelBuildOutputsForm({
<Alert color="red" title={t`Cancel Build Outputs`}>
<Text>{t`Selected build outputs will be deleted`}</Text>
</Alert>
{buildOutputFormTable(selectedOutputs, removeOutput)}
{buildOutputFormTable(selectedRows, removeRow)}
</Stack>
);
}, [selectedOutputs, removeOutput]);
}, [selectedRows, removeRow]);
const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => {
return {
outputs: {
hidden: true,
value: selectedOutputs.map((output) => {
value: selectedRows.map((output: any) => {
return {
output: output.pk
};
})
}
};
}, [selectedOutputs]);
}, [selectedRows]);
return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_delete, build.pk),
@ -459,3 +437,233 @@ export function useCancelBuildOutputsForm({
successMessage: t`Build outputs have been cancelled`
});
}
function buildAllocationFormTable(
outputs: any[],
onRemove: (output: any) => void
) {
return (
<DataTable
idAccessor="pk"
records={outputs}
columns={[
{
accessor: 'part',
title: t`Part`,
render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'allocated',
title: t`Allocated`,
render: (record: any) => (
<ProgressBar
value={record.allocated}
maximum={record.quantity}
progressLabel
/>
)
},
{
accessor: 'actions',
title: '',
render: (record: any) => (
<ActionButton
key={`remove-line-${record.pk}`}
tooltip={t`Remove line`}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
onClick={() => onRemove(record.pk)}
disabled={outputs.length <= 1}
/>
)
}
]}
/>
);
}
// Construct a single row in the 'allocate stock to build' table
function BuildAllocateLineRow({
props,
record,
sourceLocation
}: {
props: TableFieldRowProps;
record: any;
sourceLocation: number | undefined;
}) {
const stockField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'related field',
api_url: apiUrl(ApiEndpoints.stock_item_list),
model: ModelType.stockitem,
filters: {
available: true,
part_detail: true,
location_detail: true,
bom_item: record.bom_item,
location: sourceLocation,
cascade: sourceLocation ? true : undefined
},
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) {
let available = instance.quantity - instance.allocated;
props.changeFn(
props.idx,
'quantity',
Math.min(props.item.quantity, available)
);
}
}
};
}, [props]);
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]);
const partDetail = useMemo(
() => PartColumn(record.part_detail),
[record.part_detail]
);
return (
<>
<Table.Tr key={`table-row-${record.pk}`}>
<Table.Td>{partDetail}</Table.Td>
<Table.Td>
<ProgressBar
value={record.allocated}
maximum={record.quantity}
progressLabel
/>
</Table.Td>
<Table.Td>
<StandaloneField
fieldName="stock_item"
fieldDefinition={stockField}
error={props.rowErrors?.stock_item?.message}
/>
</Table.Td>
<Table.Td>
<StandaloneField
fieldName="quantity"
fieldDefinition={quantityField}
error={props.rowErrors?.quantity?.message}
/>
</Table.Td>
<Table.Td>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
</Table.Tr>
</>
);
}
/*
* Dynamic form for allocating stock against multiple build order line items
*/
export function useAllocateStockToBuildForm({
buildId,
outputId,
build,
lineItems,
onFormSuccess
}: {
buildId: number;
outputId?: number | null;
build: any;
lineItems: any[];
onFormSuccess: (response: any) => void;
}) {
const [sourceLocation, setSourceLocation] = useState<number | undefined>(
undefined
);
const buildAllocateFields: ApiFormFieldSet = useMemo(() => {
const fields: ApiFormFieldSet = {
items: {
field_type: 'table',
value: [],
headers: [t`Part`, t`Allocated`, t`Stock Item`, t`Quantity`],
modelRenderer: (row: TableFieldRowProps) => {
// Find the matching record from the passed 'lineItems'
const record =
lineItems.find((item) => item.pk == row.item.build_line) ?? {};
return (
<BuildAllocateLineRow
props={row}
record={record}
sourceLocation={sourceLocation}
/>
);
}
}
};
return fields;
}, [lineItems, sourceLocation]);
useEffect(() => {
setSourceLocation(build.take_from);
}, [build.take_from]);
const sourceLocationField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'related field',
api_url: apiUrl(ApiEndpoints.stock_location_list),
model: ModelType.stocklocation,
required: false,
label: t`Source Location`,
description: t`Select the source location for the stock allocation`,
name: 'source_location',
value: build.take_from,
onValueChange: (value: any) => {
setSourceLocation(value);
}
};
}, [build?.take_from]);
const preFormContent = useMemo(() => {
return (
<Stack gap="xs">
<StandaloneField fieldDefinition={sourceLocationField} />
</Stack>
);
}, [sourceLocationField]);
return useCreateApiFormModal({
url: ApiEndpoints.build_order_allocate,
pk: buildId,
title: t`Allocate Stock`,
fields: buildAllocateFields,
preFormContent: preFormContent,
successMessage: t`Stock items allocated`,
onFormSuccess: onFormSuccess,
initialData: {
items: lineItems.map((item) => {
return {
build_line: item.pk,
stock_item: undefined,
quantity: Math.max(0, item.quantity - item.allocated),
output: null
};
})
},
size: '80%'
});
}

View File

@ -12,6 +12,18 @@ export function projectCodeFields(): ApiFormFieldSet {
};
}
export function customStateFields(): ApiFormFieldSet {
return {
key: {},
name: {},
label: {},
color: {},
logical_key: {},
model: {},
reference_status: {}
};
}
export function customUnitsFields(): ApiFormFieldSet {
return {
name: {},

View File

@ -5,7 +5,6 @@ import {
FocusTrap,
Group,
Modal,
NumberInput,
Table,
TextInput
} from '@mantine/core';
@ -28,12 +27,16 @@ import { useEffect, useMemo, useState } from 'react';
import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { TableFieldExtraRow } from '../components/forms/fields/TableField';
import {
TableFieldExtraRow,
TableFieldRowProps
} from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar';
import { StylishText } from '../components/items/StylishText';
@ -191,67 +194,53 @@ export function usePurchaseOrderFields(): ApiFormFieldSet {
* Render a table row for a single TableField entry
*/
function LineItemFormRow({
input,
props,
record,
statuses
}: {
input: any;
props: TableFieldRowProps;
record: any;
statuses: any;
}) {
// Barcode Modal state
const [opened, { open, close }] = useDisclosure(false);
const [opened, { open, close }] = useDisclosure(false, {
onClose: () => props.changeFn(props.idx, 'barcode', undefined)
});
// Location value
const [location, setLocation] = useState(
input.item.location ??
record.part_detail.default_location ??
record.part_detail.category_default_location
);
const [locationOpen, locationHandlers] = useDisclosure(
location ? true : false,
{
onClose: () => input.changeFn(input.idx, 'location', null),
onOpen: () => input.changeFn(input.idx, 'location', location)
}
);
// Change form value when state is altered
useEffect(() => {
input.changeFn(input.idx, 'location', location);
}, [location]);
const [locationOpen, locationHandlers] = useDisclosure(false, {
onClose: () => props.changeFn(props.idx, 'location', undefined)
});
// Batch code generator
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) {
setBatchCode(value);
if (value) {
props.changeFn(props.idx, 'batch_code', value);
}
});
// Serial numbebr generator
const serialNumberGenerator = useSerialNumberGenerator((value: any) => {
if (!serials) {
setSerials(value);
if (value) {
props.changeFn(props.idx, 'serial_numbers', value);
}
});
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'packaging', undefined);
props.changeFn(props.idx, 'packaging', undefined);
}
});
const [noteOpen, noteHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'note', undefined);
props.changeFn(props.idx, 'note', undefined);
}
});
// State for serializing
const [batchCode, setBatchCode] = useState<string>('');
const [serials, setSerials] = useState<string>('');
const [batchOpen, batchHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'batch_code', undefined);
input.changeFn(input.idx, 'serial_numbers', '');
props.changeFn(props.idx, 'batch_code', undefined);
props.changeFn(props.idx, 'serial_numbers', undefined);
},
onOpen: () => {
// Generate a new batch code
@ -262,23 +251,23 @@ function LineItemFormRow({
// Generate new serial numbers
serialNumberGenerator.update({
part: record?.supplier_part_detail?.part,
quantity: input.item.quantity
quantity: props.item.quantity
});
}
});
// Status value
const [statusOpen, statusHandlers] = useDisclosure(false, {
onClose: () => input.changeFn(input.idx, 'status', 10)
onClose: () => props.changeFn(props.idx, 'status', undefined)
});
// Barcode value
const [barcodeInput, setBarcodeInput] = useState<any>('');
const [barcode, setBarcode] = useState(null);
const [barcode, setBarcode] = useState<String | undefined>(undefined);
// Change form value when state is altered
useEffect(() => {
input.changeFn(input.idx, 'barcode', barcode);
props.changeFn(props.idx, 'barcode', barcode);
}, [barcode]);
// Update location field description on state change
@ -370,13 +359,16 @@ function LineItemFormRow({
progressLabel
/>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<NumberInput
value={input.item.quantity}
style={{ width: '100px' }}
max={input.item.quantity}
min={0}
onChange={(value) => input.changeFn(input.idx, 'quantity', value)}
<Table.Td style={{ whiteSpace: 'nowrap' }}>
<StandaloneField
fieldName="quantity"
fieldDefinition={{
field_type: 'number',
value: props.item.quantity,
onValueChange: (value) =>
props.changeFn(props.idx, 'quantity', value)
}}
error={props.rowErrors?.quantity?.message}
/>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
@ -403,6 +395,7 @@ function LineItemFormRow({
size="sm"
icon={<InvenTreeIcon icon="packaging" />}
tooltip={t`Adjust Packaging`}
tooltipAlignment="top"
onClick={() => packagingHandlers.toggle()}
variant={packagingOpen ? 'filled' : 'transparent'}
/>
@ -427,7 +420,7 @@ function LineItemFormRow({
tooltipAlignment="top"
variant="filled"
color="red"
onClick={() => setBarcode(null)}
onClick={() => setBarcode(undefined)}
/>
) : (
<ActionButton
@ -438,13 +431,7 @@ function LineItemFormRow({
onClick={() => open()}
/>
)}
<ActionButton
onClick={() => input.removeFn(input.idx)}
icon={<InvenTreeIcon icon="square_x" />}
tooltip={t`Remove item from list`}
tooltipAlignment="top"
color="red"
/>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Flex>
</Table.Td>
</Table.Tr>
@ -464,7 +451,7 @@ function LineItemFormRow({
structural: false
},
onValueChange: (value) => {
setLocation(value);
props.changeFn(props.idx, 'location', value);
},
description: locationDescription,
value: location,
@ -485,7 +472,9 @@ function LineItemFormRow({
icon={<InvenTreeIcon icon="default_location" />}
tooltip={t`Store at default location`}
onClick={() =>
setLocation(
props.changeFn(
props.idx,
'location',
record.part_detail.default_location ??
record.part_detail.category_default_location
)
@ -497,7 +486,9 @@ function LineItemFormRow({
<ActionButton
icon={<InvenTreeIcon icon="destination" />}
tooltip={t`Store at line item destination `}
onClick={() => setLocation(record.destination)}
onClick={() =>
props.changeFn(props.idx, 'location', record.destination)
}
tooltipAlignment="top"
/>
)}
@ -507,7 +498,13 @@ function LineItemFormRow({
<ActionButton
icon={<InvenTreeIcon icon="repeat_destination" />}
tooltip={t`Store with already received stock`}
onClick={() => setLocation(record.destination_detail.pk)}
onClick={() =>
props.changeFn(
props.idx,
'location',
record.destination_detail.pk
)
}
tooltipAlignment="top"
/>
)}
@ -518,51 +515,56 @@ function LineItemFormRow({
)}
<TableFieldExtraRow
visible={batchOpen}
onValueChange={(value) => input.changeFn(input.idx, 'batch', value)}
onValueChange={(value) => props.changeFn(props.idx, 'batch', value)}
fieldDefinition={{
field_type: 'string',
label: t`Batch Code`,
value: batchCode
value: props.item.batch_code
}}
error={props.rowErrors?.batch_code?.message}
/>
<TableFieldExtraRow
visible={batchOpen && record.trackable}
onValueChange={(value) =>
input.changeFn(input.idx, 'serial_numbers', value)
props.changeFn(props.idx, 'serial_numbers', value)
}
fieldDefinition={{
field_type: 'string',
label: t`Serial numbers`,
value: serials
value: props.item.serial_numbers
}}
error={props.rowErrors?.serial_numbers?.message}
/>
<TableFieldExtraRow
visible={packagingOpen}
onValueChange={(value) => input.changeFn(input.idx, 'packaging', value)}
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
fieldDefinition={{
field_type: 'string',
label: t`Packaging`
}}
defaultValue={record?.supplier_part_detail?.packaging}
error={props.rowErrors?.packaging?.message}
/>
<TableFieldExtraRow
visible={statusOpen}
defaultValue={10}
onValueChange={(value) => input.changeFn(input.idx, 'status', value)}
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
fieldDefinition={{
field_type: 'choice',
api_url: apiUrl(ApiEndpoints.stock_status),
choices: statuses,
label: t`Status`
}}
error={props.rowErrors?.status?.message}
/>
<TableFieldExtraRow
visible={noteOpen}
onValueChange={(value) => input.changeFn(input.idx, 'note', value)}
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
fieldDefinition={{
field_type: 'string',
label: t`Note`
}}
error={props.rowErrors?.note?.message}
/>
</>
);
@ -624,12 +626,12 @@ export function useReceiveLineItems(props: LineItemsForm) {
barcode: null
};
}),
modelRenderer: (instance) => {
const record = records[instance.item.line_item];
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.line_item];
return (
<LineItemFormRow
input={instance}
props={row}
record={record}
statuses={data}
key={record.pk}
@ -645,18 +647,14 @@ export function useReceiveLineItems(props: LineItemsForm) {
}
};
const url = apiUrl(ApiEndpoints.purchase_order_receive, null, {
id: props.orderPk
});
return useCreateApiFormModal({
...props.formProps,
url: url,
url: apiUrl(ApiEndpoints.purchase_order_receive, props.orderPk),
title: t`Receive Line Items`,
fields: fields,
initialData: {
location: null
},
size: 'xl'
size: '80%'
});
}

View File

@ -1,17 +1,22 @@
import { t } from '@lingui/macro';
import { Flex, Group, NumberInput, Skeleton, Table, Text } from '@mantine/core';
import { Flex, Group, Skeleton, Table, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { Suspense, useCallback, useMemo, useState } from 'react';
import { Suspense, useMemo, useState } from 'react';
import { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { TableFieldExtraRow } from '../components/forms/fields/TableField';
import {
TableFieldExtraRow,
TableFieldRowProps
} from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { StylishText } from '../components/items/StylishText';
import { StatusRenderer } from '../components/render/StatusRenderer';
@ -138,7 +143,9 @@ export function useStockFields({
value: batchCode,
onValueChange: (value) => setBatchCode(value)
},
status: {},
status_custom_key: {
label: t`Stock Status`
},
expiry_date: {
// TODO: icon
},
@ -294,54 +301,37 @@ type StockRow = {
};
function StockOperationsRow({
input,
props,
transfer = false,
add = false,
setMax = false,
merge = false,
record
}: {
input: StockRow;
props: TableFieldRowProps;
transfer?: boolean;
add?: boolean;
setMax?: boolean;
merge?: boolean;
record?: any;
}) {
const item = input.item;
const [value, setValue] = useState<StockItemQuantity>(
add ? 0 : item.quantity ?? 0
);
const onChange = useCallback(
(value: any) => {
setValue(value);
input.changeFn(input.idx, 'quantity', value);
},
[item]
);
const changeSubItem = useCallback(
(key: string, value: any) => {
input.changeFn(input.idx, key, value);
},
[input]
const [quantity, setQuantity] = useState<StockItemQuantity>(
add ? 0 : props.item?.quantity ?? 0
);
const removeAndRefresh = () => {
input.removeFn(input.idx);
props.removeFn(props.idx);
};
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
onOpen: () => {
if (transfer) {
input.changeFn(input.idx, 'packaging', record?.packaging || undefined);
props.changeFn(props.idx, 'packaging', record?.packaging || undefined);
}
},
onClose: () => {
if (transfer) {
input.changeFn(input.idx, 'packaging', undefined);
props.changeFn(props.idx, 'packaging', undefined);
}
}
});
@ -377,25 +367,24 @@ function StockOperationsRow({
{record.location ? record.location_detail?.pathstring : '-'}
</Table.Td>
<Table.Td>
<Flex align="center" gap="xs">
<Group justify="space-between">
<Text>{stockString}</Text>
<StatusRenderer
status={record.status}
type={ModelType.stockitem}
/>
</Group>
</Flex>
<Group grow justify="space-between" wrap="nowrap">
<Text>{stockString}</Text>
<StatusRenderer status={record.status} type={ModelType.stockitem} />
</Group>
</Table.Td>
{!merge && (
<Table.Td>
<NumberInput
value={value}
onChange={onChange}
disabled={!!record.serial && record.quantity == 1}
max={setMax ? record.quantity : undefined}
min={0}
style={{ maxWidth: '100px' }}
<StandaloneField
fieldName="quantity"
fieldDefinition={{
field_type: 'number',
value: quantity,
onValueChange: (value: any) => {
setQuantity(value);
props.changeFn(props.idx, 'quantity', value);
}
}}
error={props.rowErrors?.quantity?.message}
/>
</Table.Td>
)}
@ -403,7 +392,9 @@ function StockOperationsRow({
<Flex gap="3px">
{transfer && (
<ActionButton
onClick={() => moveToDefault(record, value, removeAndRefresh)}
onClick={() =>
moveToDefault(record, props.item.quantity, removeAndRefresh)
}
icon={<InvenTreeIcon icon="default_location" />}
tooltip={t`Move to default location`}
tooltipAlignment="top"
@ -422,13 +413,7 @@ function StockOperationsRow({
variant={packagingOpen ? 'filled' : 'transparent'}
/>
)}
<ActionButton
onClick={() => input.removeFn(input.idx)}
icon={<InvenTreeIcon icon="square_x" />}
tooltip={t`Remove item from list`}
tooltipAlignment="top"
color="red"
/>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Flex>
</Table.Td>
</Table.Tr>
@ -436,7 +421,7 @@ function StockOperationsRow({
<TableFieldExtraRow
visible={transfer && packagingOpen}
onValueChange={(value: any) => {
input.changeFn(input.idx, 'packaging', value || undefined);
props.changeFn(props.idx, 'packaging', value || undefined);
}}
fieldDefinition={{
field_type: 'string',
@ -464,9 +449,9 @@ function mapAdjustmentItems(items: any[]) {
return {
pk: elem.pk,
quantity: elem.quantity,
batch: elem.batch,
status: elem.status,
packaging: elem.packaging,
batch: elem.batch || undefined,
status: elem.status || undefined,
packaging: elem.packaging || undefined,
obj: elem
};
});
@ -485,14 +470,16 @@ function stockTransferFields(items: any[]): ApiFormFieldSet {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.pk];
return (
<StockOperationsRow
input={val}
props={row}
transfer
setMax
key={val.item.pk}
record={records[val.item.pk]}
key={record.pk}
record={record}
/>
);
},
@ -520,13 +507,16 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.pk];
return (
<StockOperationsRow
input={val}
props={row}
setMax
key={val.item.pk}
record={records[val.item.pk]}
add
key={record.pk}
record={record}
/>
);
},
@ -549,14 +539,11 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.pk];
return (
<StockOperationsRow
input={val}
add
key={val.item.pk}
record={records[val.item.pk]}
/>
<StockOperationsRow props={row} add key={record.pk} record={record} />
);
},
headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`]
@ -578,12 +565,12 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
items: {
field_type: 'table',
value: mapAdjustmentItems(items),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
return (
<StockOperationsRow
input={val}
key={val.item.pk}
record={records[val.item.pk]}
props={row}
key={row.item.pk}
record={records[row.item.pk]}
/>
);
},
@ -608,13 +595,13 @@ function stockChangeStatusFields(items: any[]): ApiFormFieldSet {
value: items.map((elem) => {
return elem.pk;
}),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
return (
<StockOperationsRow
input={val}
key={val.item}
props={row}
key={row.item}
merge
record={records[val.item]}
record={records[row.item]}
/>
);
},
@ -643,13 +630,13 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
obj: elem
};
}),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
return (
<StockOperationsRow
input={val}
key={val.item.item}
props={row}
key={row.item.item}
merge
record={records[val.item.item]}
record={records[row.item.item]}
/>
);
},
@ -685,13 +672,13 @@ function stockAssignFields(items: any[]): ApiFormFieldSet {
obj: elem
};
}),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
return (
<StockOperationsRow
input={val}
key={val.item.item}
props={row}
key={row.item.item}
merge
record={records[val.item.item]}
record={records[row.item.item]}
/>
);
},
@ -721,13 +708,15 @@ function stockDeleteFields(items: any[]): ApiFormFieldSet {
value: items.map((elem) => {
return elem.pk;
}),
modelRenderer: (val) => {
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item];
return (
<StockOperationsRow
input={val}
key={val.item}
props={row}
key={record.pk}
merge
record={records[val.item]}
record={record}
/>
);
},
@ -815,6 +804,7 @@ function stockOperationModal({
url: endpoint,
fields: fields,
title: title,
size: '80%',
onFormSuccess: () => refresh()
});
}
@ -922,10 +912,14 @@ export function stockLocationFields(): ApiFormFieldSet {
// Construct a set of fields for
export function useTestResultFields({
partId,
itemId
itemId,
templateId,
editTemplate = false
}: {
partId: number;
itemId: number;
templateId: number | undefined;
editTemplate?: boolean;
}): ApiFormFieldSet {
// Valid field choices
const [choices, setChoices] = useState<any[]>([]);
@ -947,6 +941,7 @@ export function useTestResultFields({
hidden: true
},
template: {
disabled: !editTemplate && !!templateId,
filters: {
include_inherited: true,
part: partId
@ -990,5 +985,13 @@ export function useTestResultFields({
hidden: !includeTestStation
}
};
}, [choices, fieldType, partId, itemId, includeTestStation]);
}, [
choices,
editTemplate,
fieldType,
partId,
itemId,
templateId,
includeTestStation
]);
}

View File

@ -0,0 +1,37 @@
import { useCallback, useEffect, useState } from 'react';
/**
* Hook to manage multiple selected rows in a multi-action modal.
*
* - The hook is initially provided with a list of rows
* - A callback is provided to remove a row, based on the provided ID value
*/
export function useSelectedRows<T>({
rows,
pkField = 'pk'
}: {
rows: T[];
pkField?: string;
}) {
const [selectedRows, setSelectedRows] = useState<T[]>(rows);
// Update selection whenever input rows are updated
useEffect(() => {
setSelectedRows(rows);
}, [rows]);
// Callback to remove the selected row
const removeRow = useCallback(
(pk: any) => {
setSelectedRows((rows) =>
rows.filter((row: any) => row[pkField ?? 'pk'] !== pk)
);
},
[pkField]
);
return {
selectedRows,
removeRow
};
}

View File

@ -36,9 +36,13 @@ export type TableState = {
setRecordCount: (count: number) => void;
page: number;
setPage: (page: number) => void;
pageSize: number;
setPageSize: (pageSize: number) => void;
records: any[];
setRecords: (records: any[]) => void;
updateRecord: (record: any) => void;
editable: boolean;
setEditable: (value: boolean) => void;
};
/**
@ -97,6 +101,7 @@ export function useTable(tableName: string): TableState {
// Pagination data
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(25);
// A list of hidden columns, saved to local storage
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
@ -131,6 +136,8 @@ export function useTable(tableName: string): TableState {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [editable, setEditable] = useState<boolean>(false);
return {
tableKey,
refreshTable,
@ -154,8 +161,12 @@ export function useTable(tableName: string): TableState {
setRecordCount,
page,
setPage,
pageSize,
setPageSize,
records,
setRecords,
updateRecord
updateRecord,
editable,
setEditable
};
}

View File

@ -1,21 +1,24 @@
import { t } from '@lingui/macro';
import { Divider, Stack } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconReload } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { InvenTreeTable } from '../InvenTreeTable';
import { api } from '../../../../App';
import { ActionButton } from '../../../../components/buttons/ActionButton';
import { FactCollection } from '../../../../components/settings/FactCollection';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { useTable } from '../../../../hooks/UseTable';
import { apiUrl } from '../../../../states/ApiState';
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
/*
* Table for displaying available currencies
*/
export default function CurrencyTable() {
export function CurrencyTable({
setInfo
}: Readonly<{ setInfo: (info: any) => void }>) {
const table = useTable('currency');
const columns = useMemo(() => {
return [
{
@ -53,6 +56,7 @@ export default function CurrencyTable() {
const tableActions = useMemo(() => {
return [
<ActionButton
key="refresh"
onClick={refreshCurrencies}
tooltip={t`Refresh currency exchange rates`}
icon={<IconReload />}
@ -66,8 +70,10 @@ export default function CurrencyTable() {
tableState={table}
columns={columns}
props={{
idAccessor: 'currency',
tableActions: tableActions,
dataFormatter: (data: any) => {
setInfo(data);
let rates = data.exchange_rates ?? {};
return Object.entries(rates).map(([currency, rate]) => {
@ -81,3 +87,20 @@ export default function CurrencyTable() {
/>
);
}
export default function CurrencyManagmentPanel() {
const [info, setInfo] = useState<any>({});
return (
<Stack gap="xs">
<FactCollection
items={[
{ title: t`Last fetched`, value: info?.updated },
{ title: t`Base currency`, value: info?.base_currency }
]}
/>
<Divider />
<CurrencyTable setInfo={setInfo} />
</Stack>
);
}

View File

@ -44,6 +44,10 @@ const TaskManagementPanel = Loadable(
lazy(() => import('./TaskManagementPanel'))
);
const CurrencyManagmentPanel = Loadable(
lazy(() => import('./CurrencyManagmentPanel'))
);
const PluginManagementPanel = Loadable(
lazy(() => import('./PluginManagementPanel'))
);
@ -64,6 +68,10 @@ const ProjectCodeTable = Loadable(
lazy(() => import('../../../../tables/settings/ProjectCodeTable'))
);
const CustomStateTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomStateTable'))
);
const CustomUnitsTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomUnitsTable'))
);
@ -80,10 +88,6 @@ const LocationTypesTable = Loadable(
lazy(() => import('../../../../tables/stock/LocationTypesTable'))
);
const CurrencyTable = Loadable(
lazy(() => import('../../../../tables/settings/CurrencyTable'))
);
export default function AdminCenter() {
const user = useUserState();
const navigate = useNavigate();
@ -133,7 +137,7 @@ export default function AdminCenter() {
name: 'currencies',
label: t`Currencies`,
icon: <IconCoins />,
content: <CurrencyTable />
content: <CurrencyManagmentPanel />
},
{
name: 'projectcodes',
@ -147,6 +151,12 @@ export default function AdminCenter() {
</Stack>
)
},
{
name: 'customstates',
label: t`Custom States`,
icon: <IconListDetails />,
content: <CustomStateTable />
},
{
name: 'customunits',
label: t`Custom Units`,

View File

@ -1,16 +1,9 @@
import { t } from '@lingui/macro';
import {
Accordion,
Alert,
Divider,
Paper,
SimpleGrid,
Stack,
Text
} from '@mantine/core';
import { Accordion, Alert, Divider, Stack, Text } from '@mantine/core';
import { lazy } from 'react';
import { StylishText } from '../../../../components/items/StylishText';
import { FactCollection } from '../../../../components/settings/FactCollection';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { Loadable } from '../../../../functions/loading';
import { useInstance } from '../../../../hooks/UseInstance';
@ -27,17 +20,6 @@ const FailedTasksTable = Loadable(
lazy(() => import('../../../../tables/settings/FailedTasksTable'))
);
function TaskCountOverview({ title, value }: { title: string; value: number }) {
return (
<Paper p="md" shadow="xs">
<Stack gap="xs">
<StylishText size="md">{title}</StylishText>
<Text>{value}</Text>
</Stack>
</Paper>
);
}
export default function TaskManagementPanel() {
const { instance: taskInfo } = useInstance({
endpoint: ApiEndpoints.task_overview,
@ -55,20 +37,13 @@ export default function TaskManagementPanel() {
</Alert>
)}
<Stack gap="xs">
<SimpleGrid cols={3} spacing="xs">
<TaskCountOverview
title={t`Pending Tasks`}
value={taskInfo?.pending_tasks}
/>
<TaskCountOverview
title={t`Scheduled Tasks`}
value={taskInfo?.scheduled_tasks}
/>
<TaskCountOverview
title={t`Failed Tasks`}
value={taskInfo?.failed_tasks}
/>
</SimpleGrid>
<FactCollection
items={[
{ title: t`Pending Tasks`, value: taskInfo?.pending_tasks },
{ title: t`Scheduled Tasks`, value: taskInfo?.scheduled_tasks },
{ title: t`Failed Tasks`, value: taskInfo?.failed_tasks }
]}
/>
<Divider />
<Accordion defaultValue="pending">
<Accordion.Item value="pending" key="pending-tasks">

View File

@ -30,10 +30,7 @@ import {
CancelItemAction,
DuplicateItemAction,
EditItemAction,
HoldItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
HoldItemAction
} from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
@ -43,7 +40,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
import { notYetImplemented } from '../../functions/notifications';
import {
useCreateApiFormModal,
useEditApiFormModal
@ -257,7 +253,7 @@ export default function BuildDetail() {
label: t`Line Items`,
icon: <IconListNumbers />,
content: build?.pk ? (
<BuildLineTable buildId={build.pk} />
<BuildLineTable build={build} buildId={build.pk} />
) : (
<Skeleton />
)
@ -472,20 +468,9 @@ export default function BuildDetail() {
/>,
<AdminButton model={ModelType.build} pk={build.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.build,
pk: build.pk
}),
LinkBarcodeAction({
hidden: build?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !build?.barcode_hash,
onClick: notYetImplemented
})
]}
model={ModelType.build}
pk={build.pk}
hash={build?.barcode_hash}
/>,
<PrintingActions
modelType={ModelType.build}
@ -526,7 +511,7 @@ export default function BuildDetail() {
? []
: [
<StatusRenderer
status={build.status}
status={build.status_custom_key}
type={ModelType.build}
options={{ size: 'lg' }}
/>

View File

@ -22,10 +22,7 @@ import {
BarcodeActionDropdown,
DeleteItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
EditItemAction
} from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
@ -34,7 +31,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSupplierPartFields } from '../../forms/CompanyForms';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
@ -271,24 +267,10 @@ export default function SupplierPartDetail() {
return [
<AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.supplierpart,
pk: supplierPart.pk
}),
LinkBarcodeAction({
hidden:
supplierPart.barcode_hash ||
!user.hasChangeRole(UserRoles.purchase_order),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden:
!supplierPart.barcode_hash ||
!user.hasChangeRole(UserRoles.purchase_order),
onClick: notYetImplemented
})
]}
model={ModelType.supplierpart}
pk={supplierPart.pk}
hash={supplierPart.barcode_hash}
perm={user.hasChangeRole(UserRoles.purchase_order)}
/>,
<ActionDropdown
tooltip={t`Supplier Part Actions`}

View File

@ -51,10 +51,7 @@ import {
BarcodeActionDropdown,
DeleteItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
EditItemAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
@ -74,7 +71,6 @@ import {
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
@ -626,7 +622,7 @@ export default function PartDetail() {
name: 'builds',
label: t`Build Orders`,
icon: <IconTools />,
hidden: !part.assembly,
hidden: !part.assembly || !part.active,
content: part?.pk ? <BuildOrderTable partId={part.pk} /> : <Skeleton />
},
{
@ -993,20 +989,10 @@ export default function PartDetail() {
return [
<AdminButton model={ModelType.part} pk={part.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.part,
pk: part.pk
}),
LinkBarcodeAction({
hidden: part?.barcode_hash || !user.hasChangeRole(UserRoles.part),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !part?.barcode_hash || !user.hasChangeRole(UserRoles.part),
onClick: notYetImplemented
})
]}
model={ModelType.part}
pk={part.pk}
hash={part?.barcode_hash}
perm={user.hasChangeRole(UserRoles.part)}
key="action_dropdown"
/>,
<PrintingActions

View File

@ -24,10 +24,7 @@ import {
CancelItemAction,
DuplicateItemAction,
EditItemAction,
HoldItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
HoldItemAction
} from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -39,7 +36,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import {
useCreateApiFormModal,
useEditApiFormModal
@ -403,20 +399,9 @@ export default function PurchaseOrderDetail() {
/>,
<AdminButton model={ModelType.purchaseorder} pk={order.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.purchaseorder,
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
model={ModelType.purchaseorder}
pk={order.pk}
hash={order?.barcode_hash}
/>,
<PrintingActions
modelType={ModelType.purchaseorder}
@ -459,7 +444,7 @@ export default function PurchaseOrderDetail() {
? []
: [
<StatusRenderer
status={order.status}
status={order.status_custom_key}
type={ModelType.purchaseorder}
options={{ size: 'lg' }}
/>

View File

@ -23,10 +23,7 @@ import {
CancelItemAction,
DuplicateItemAction,
EditItemAction,
HoldItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
HoldItemAction
} from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -38,7 +35,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import {
useCreateApiFormModal,
useEditApiFormModal
@ -300,7 +296,7 @@ export default function ReturnOrderDetail() {
? []
: [
<StatusRenderer
status={order.status}
status={order.status_custom_key}
type={ModelType.returnorder}
options={{ size: 'lg' }}
/>
@ -404,20 +400,9 @@ export default function ReturnOrderDetail() {
/>,
<AdminButton model={ModelType.returnorder} pk={order.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.returnorder,
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
model={ModelType.returnorder}
pk={order.pk}
hash={order?.barcode_hash}
/>,
<PrintingActions
modelType={ModelType.returnorder}

View File

@ -26,10 +26,7 @@ import {
CancelItemAction,
DuplicateItemAction,
EditItemAction,
HoldItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
HoldItemAction
} from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -41,7 +38,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import {
useCreateApiFormModal,
useEditApiFormModal
@ -444,20 +440,9 @@ export default function SalesOrderDetail() {
/>,
<AdminButton model={ModelType.salesorder} pk={order.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.salesorder,
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
model={ModelType.salesorder}
pk={order.pk}
hash={order?.barcode_hash}
/>,
<PrintingActions
modelType={ModelType.salesorder}
@ -498,7 +483,7 @@ export default function SalesOrderDetail() {
? []
: [
<StatusRenderer
status={order.status}
status={order.status_custom_key}
type={ModelType.salesorder}
options={{ size: 'lg' }}
key={order.pk}

View File

@ -18,10 +18,7 @@ import {
ActionDropdown,
BarcodeActionDropdown,
DeleteItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
EditItemAction
} from '../../components/items/ActionDropdown';
import { ApiIcon } from '../../components/items/ApiIcon';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -287,17 +284,9 @@ export default function Stock() {
/>,
location.pk ? (
<BarcodeActionDropdown
model={ModelType.stocklocation}
pk={location.pk}
actions={[
ViewBarcodeAction({
model: ModelType.stocklocation,
pk: location.pk
}),
LinkBarcodeAction({
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
onClick: notYetImplemented
}),
{
name: 'Scan in stock items',
icon: <InvenTreeIcon icon="stock" />,

View File

@ -27,10 +27,7 @@ import {
BarcodeActionDropdown,
DeleteItemAction,
DuplicateItemAction,
EditItemAction,
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
EditItemAction
} from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -50,7 +47,6 @@ import {
useTransferStockItem
} from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
@ -477,22 +473,10 @@ export default function StockDetail() {
() => [
<AdminButton model={ModelType.stockitem} pk={stockitem.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.stockitem,
pk: stockitem.pk
}),
LinkBarcodeAction({
hidden:
stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden:
!stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock),
onClick: notYetImplemented
})
]}
model={ModelType.stockitem}
pk={stockitem.pk}
hash={stockitem?.barcode_hash}
perm={user.hasChangeRole(UserRoles.stock)}
/>,
<PrintingActions
modelType={ModelType.stockitem}
@ -601,7 +585,7 @@ export default function StockDetail() {
key="batch"
/>,
<StatusRenderer
status={stockitem.status}
status={stockitem.status_custom_key}
type={ModelType.stockitem}
options={{ size: 'lg' }}
key="status"

View File

@ -1,3 +1,5 @@
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
export type TableColumnProps<T = any> = {
accessor?: string; // The key in the record to access
title?: string; // The title of the column - Note: this may be supplied by the API, and is not required, but it can be overridden if required
@ -5,6 +7,8 @@ export type TableColumnProps<T = any> = {
sortable?: boolean; // Whether the column is sortable
switchable?: boolean; // Whether the column is switchable
hidden?: boolean; // Whether the column is hidden
editable?: boolean; // Whether the value of this column can be edited
definition?: ApiFormFieldType; // Optional field definition for the column
render?: (record: T, index?: number) => any; // A custom render function
filter?: any; // A custom filter function
filtering?: boolean; // Whether the column is filterable

View File

@ -178,7 +178,7 @@ export function StatusColumn({
sortable: sortable ?? true,
title: title,
hidden: hidden,
render: TableStatusRenderer(model, accessor ?? 'status')
render: TableStatusRenderer(model, accessor ?? 'status_custom_key')
};
}

View File

@ -55,6 +55,7 @@ import { RowAction, RowActions } from './RowActions';
import { TableSearchInput } from './Search';
const defaultPageSize: number = 25;
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
/**
* Set of optional properties which can be passed to an InvenTreeTable component
@ -74,7 +75,6 @@ const defaultPageSize: number = 25;
* @param enableRefresh : boolean - Enable refresh actions
* @param enableColumnSwitching : boolean - Enable column switching
* @param enableColumnCaching : boolean - Enable caching of column names via API
* @param pageSize : number - Number of records per page
* @param barcodeActions : any[] - List of barcode actions
* @param tableFilters : TableFilter[] - List of custom filters
* @param tableActions : any[] - List of custom action groups
@ -100,7 +100,6 @@ export type InvenTreeTableProps<T = any> = {
enableLabels?: boolean;
enableReports?: boolean;
afterBulkDelete?: () => void;
pageSize?: number;
barcodeActions?: React.ReactNode[];
tableFilters?: TableFilter[];
tableActions?: React.ReactNode[];
@ -129,7 +128,6 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
enableRefresh: true,
enableSearch: true,
enableSelection: false,
pageSize: defaultPageSize,
defaultSortColumn: '',
barcodeActions: [],
tableFilters: [],
@ -360,7 +358,8 @@ export function InvenTreeTable<T = any>({
// Pagination
if (tableProps.enablePagination && paginate) {
let pageSize = tableProps.pageSize ?? defaultPageSize;
let pageSize = tableState.pageSize ?? defaultPageSize;
if (pageSize != tableState.pageSize) tableState.setPageSize(pageSize);
queryParams.limit = pageSize;
queryParams.offset = (tableState.page - 1) * pageSize;
}
@ -588,6 +587,13 @@ export function InvenTreeTable<T = any>({
[props.onRowClick, props.onCellClick]
);
// pagination refresth table if pageSize changes
function updatePageSize(newData: number) {
tableState.setPageSize(newData);
tableState.setPage(1);
tableState.refreshTable();
}
return (
<>
{deleteRecords.modal}
@ -697,6 +703,7 @@ export function InvenTreeTable<T = any>({
<DataTable
withTableBorder
withColumnBorders
striped
highlightOnHover
loaderType="dots"
@ -704,7 +711,7 @@ export function InvenTreeTable<T = any>({
idAccessor={tableProps.idAccessor}
minHeight={300}
totalRecords={tableState.recordCount}
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
recordsPerPage={tableState.pageSize}
page={tableState.page}
onPageChange={tableState.setPage}
sortStatus={sortStatus}
@ -732,6 +739,8 @@ export function InvenTreeTable<T = any>({
overflow: 'hidden'
})
}}
recordsPerPageOptions={PAGE_SIZES}
onRecordsPerPageChange={updatePageSize}
/>
</Box>
</Stack>

Some files were not shown because too many files have changed in this diff Show More