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 contents: read
jobs: jobs:
build: synchronize-with-crowdin:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
pull-requests: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -39,16 +40,29 @@ jobs:
apt-dependency: gettext apt-dependency: gettext
- name: Make Translations - name: Make Translations
run: invoke translate 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: | run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]" git config --local user.name "github-actions[bot]"
git checkout -b l10_local git add src/backend/InvenTree/locale/en/LC_MESSAGES/django.po src/frontend/src/locales/en/messages.po
git add "*.po" git commit -m "add translations"
git commit -m "updated translation base" git reset --hard
- name: Push changes git reset HEAD~
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0 - name: crowdin action
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # pin@v2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} upload_sources: true
branch: l10 upload_translations: false
force: true 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 # LDAP required packages
django-auth-ldap # Django integration for ldap auth django-auth-ldap # Django integration for ldap auth
python-ldap # LDAP auth support python-ldap # LDAP auth support
django<5.0 # Force lower to match main project
# Upgraded python package installer # Upgraded python package installer
uv uv

View File

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

View File

@ -104,9 +104,9 @@ jc==1.25.3 \
--hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \ --hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \
--hash=sha256:fa3140ceda6cba1210d1362f363cd79a0514741e8a1dd6167db2b2e2d5f24f7b --hash=sha256:fa3140ceda6cba1210d1362f363cd79a0514741e8a1dd6167db2b2e2d5f24f7b
# via -r contrib/dev_reqs/requirements.in # via -r contrib/dev_reqs/requirements.in
pygments==2.17.2 \ pygments==2.18.0 \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a
# via jc # via jc
pyyaml==6.0.2 \ pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ --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" "commit_message": "Fix: New translations %original_file_name% from Crowdin"
"append_commit_message": false "append_commit_message": false
"preserve_hierarchy": true
files: files:
- source: /src/backend/InvenTree/locale/en/LC_MESSAGES/django.po - 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% translation: /src/backend/InvenTree/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%
- source: /src/frontend/src/locales/en/messages.po - 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% 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 ```py
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from generic.states import ColorEnum
from plugin.machine import BaseDriver, BaseMachineType, MachineStatus from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
class ABCBaseDriver(BaseDriver): class ABCBaseDriver(BaseDriver):
@ -72,9 +74,9 @@ class ABCMachine(BaseMachineType):
base_driver = ABCBaseDriver base_driver = ABCBaseDriver
class ABCStatus(MachineStatus): class ABCStatus(MachineStatus):
CONNECTED = 100, _('Connected'), 'success' CONNECTED = 100, _('Connected'), ColorEnum.success
STANDBY = 101, _('Standby'), 'success' STANDBY = 101, _('Standby'), ColorEnum.success
PRINTING = 110, _('Printing'), 'primary' PRINTING = 110, _('Printing'), ColorEnum.primary
MACHINE_STATUS = ABCStatus MACHINE_STATUS = ABCStatus
default_machine_status = ABCStatus.DISCONNECTED 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 show_source: True
members: [] members: []
Purchase Order Status supports [custom states](../concepts/custom_states.md).
### Purchase Order Currency ### 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. 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 show_source: True
members: [] members: []
Return Order Status supports [custom states](../concepts/custom_states.md).
## Create a Return Order ## 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. 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 show_source: True
members: [] members: []
Sales Order Status supports [custom states](../concepts/custom_states.md).
### Sales Order Currency ### 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. 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: 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 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> | | <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 show_source: True
members: [] members: []
Stock Status supports [custom states](../concepts/custom_states.md).
### Default Status Code ### Default Status Code
The default status code for any newly created Stock Item is <span class='badge inventree success'>OK</span> 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: - Core Concepts:
- Terminology: concepts/terminology.md - Terminology: concepts/terminology.md
- Physical Units: concepts/units.md - Physical Units: concepts/units.md
- Custom States: concepts/custom_states.md
- Development: - Development:
- Contributing: develop/contributing.md - Contributing: develop/contributing.md
- Devcontainer: develop/devcontainer.md - Devcontainer: develop/devcontainer.md

View File

@ -1,13 +1,27 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ 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 v245 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
- Documented pagination fields (no functional changes) - Documented pagination fields (no functional changes)

View File

@ -3,9 +3,9 @@
"""Provides extra global data to all templates.""" """Provides extra global data to all templates."""
import InvenTree.email import InvenTree.email
import InvenTree.ready
import InvenTree.status import InvenTree.status
from generic.states import StatusCode from generic.states.custom import get_custom_classes
from InvenTree.helpers import inheritors
from users.models import RuleSet, check_user_role from users.models import RuleSet, check_user_role
@ -53,7 +53,10 @@ def status_codes(request):
return {} return {}
request._inventree_status_codes = True 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): def user_roles(request):

View File

@ -953,8 +953,15 @@ def get_objectreference(
Inheritors_T = TypeVar('Inheritors_T') Inheritors_T = TypeVar('Inheritors_T')
def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]: def inheritors(
"""Return all classes that are subclasses from the supplied cls.""" 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() subcls = set()
work = [cls] work = [cls]
@ -963,7 +970,8 @@ def inheritors(cls: type[Inheritors_T]) -> set[type[Inheritors_T]]:
for child in parent.__subclasses__(): for child in parent.__subclasses__():
if child not in subcls: if child not in subcls:
subcls.add(child) subcls.add(child)
work.append(child) if subclasses:
work.append(child)
return subcls return subcls

View File

@ -363,7 +363,7 @@ class InvenTreeMetadata(SimpleMetadata):
field_info['type'] = 'related field' field_info['type'] = 'related field'
field_info['model'] = model._meta.model_name field_info['model'] = model._meta.model_name
# Special case for 'user' model # Special case for special models
if field_info['model'] == 'user': if field_info['model'] == 'user':
field_info['api_url'] = '/api/user/' field_info['api_url'] = '/api/user/'
elif field_info['model'] == 'contenttype': elif field_info['model'] == 'contenttype':
@ -381,6 +381,14 @@ class InvenTreeMetadata(SimpleMetadata):
if field_info['type'] == 'dependent field': if field_info['type'] == 'dependent field':
field_info['depends_on'] = field.depends_on 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 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 report.mixins
import stock.models import stock.models
import users.models import users.models
import generic.states
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -315,7 +316,7 @@ class Build(
help_text=_('Number of stock items which have been completed') help_text=_('Number of stock items which have been completed')
) )
status = models.PositiveIntegerField( status = generic.states.fields.InvenTreeCustomStatusModelField(
verbose_name=_('Build Status'), verbose_name=_('Build Status'),
default=BuildStatus.PENDING.value, default=BuildStatus.PENDING.value,
choices=BuildStatus.items(), choices=BuildStatus.items(),

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</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>
<tr> <tr>
<td><span class='fas fa-check-circle'></span></td> <td><span class='fas fa-check-circle'></span></td>

View File

@ -29,7 +29,7 @@ import common.models
import common.serializers import common.serializers
from common.icons import get_icon_packs from common.icons import get_icon_packs
from common.settings import get_global_setting 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 importer.mixins import DataExportViewMixin
from InvenTree.api import BulkDeleteMixin, MetadataView from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.config import CONFIG_LOOKUPS from InvenTree.config import CONFIG_LOOKUPS
@ -655,6 +655,8 @@ class ContentTypeList(ListAPI):
queryset = ContentType.objects.all() queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['app_label', 'model']
class ContentTypeDetail(RetrieveAPI): class ContentTypeDetail(RetrieveAPI):
@ -965,16 +967,7 @@ common_api_urls = [
]), ]),
), ),
# Status # Status
path( path('generic/status/', include(generic_states_api_urls)),
'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'),
]),
),
# Contenttype # Contenttype
path( path(
'contenttype/', '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 plugin.base.barcodes.helper
import report.helpers import report.helpers
import users.models 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 InvenTree.sanitizer import sanitize_svg
from plugin import registry from plugin import registry
@ -3339,3 +3341,109 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
raise ValidationError(_('Invalid model type specified for attachment')) raise ValidationError(_('Invalid model type specified for attachment'))
return model_class.check_attachment_permission(permission, user) 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.models as common_models
import common.validators import common.validators
import generic.states.custom
from importer.mixins import DataImportExportSerializerMixin from importer.mixins import DataImportExportSerializerMixin
from importer.registry import register_importer from importer.registry import register_importer
from InvenTree.helpers import get_objectreference from InvenTree.helpers import get_objectreference
@ -308,6 +309,32 @@ class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSeria
responsible_detail = OwnerSerializer(source='responsible', read_only=True) 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): class FlagSerializer(serializers.Serializer):
"""Serializer for feature flags.""" """Serializer for feature flags."""

View File

@ -33,6 +33,7 @@ from .models import (
Attachment, Attachment,
ColorTheme, ColorTheme,
CustomUnit, CustomUnit,
InvenTreeCustomUserStateModel,
InvenTreeSetting, InvenTreeSetting,
InvenTreeUserSetting, InvenTreeUserSetting,
NotesImage, NotesImage,
@ -1586,3 +1587,93 @@ class ValidatorsTest(TestCase):
common.validators.validate_icon('ti:package:non-existing-variant') common.validators.validate_icon('ti:package:non-existing-variant')
common.validators.validate_icon('ti:package:outline') 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. 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 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 import inspect
from django.urls import include, path
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions, serializers from rest_framework import permissions, serializers
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.response import Response 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 InvenTree.serializers import EmptySerializer
from machine.machine_type import MachineStatus
from .states import StatusCode from .states import StatusCode
@ -73,18 +83,52 @@ class AllStatusViews(StatusView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes.""" """Perform a GET request to learn information about status codes."""
data = {} data = get_status_api_response()
# Extend with MachineStatus classes
def discover_status_codes(parent_status_class, prefix=None): data.update(get_status_api_response(MachineStatus, prefix=['MachineStatus']))
"""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)
return Response(data) 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 enum
import re import re
from enum import Enum
class BaseEnum(enum.IntEnum): class BaseEnum(enum.IntEnum):
@ -65,10 +66,23 @@ class StatusCode(BaseEnum):
# Normal item definition # Normal item definition
if len(args) == 1: if len(args) == 1:
obj.label = args[0] obj.label = args[0]
obj.color = 'secondary' obj.color = ColorEnum.secondary
else: else:
obj.label = args[1] 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 return obj
@ -181,3 +195,15 @@ class StatusCode(BaseEnum):
ret['list'] = cls.list() ret['list'] = cls.list()
return ret 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 django.utils.safestring import mark_safe
from generic.templatetags.generic import register 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 @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.""" """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: if state:
return mark_safe(state.render(key, large=kwargs.get('large', False))) return mark_safe(state.render(key, large=kwargs.get('large', False)))
raise ValueError(f"Unknown status type '{typ}'") 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.""" """Tests for the generic states module."""
from django.contrib.contenttypes.models import ContentType
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.test import force_authenticate 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 .api import StatusView
from .states import StatusCode from .states import StatusCode
@ -14,9 +18,9 @@ from .states import StatusCode
class GeneralStatus(StatusCode): class GeneralStatus(StatusCode):
"""Defines a set of status codes for tests.""" """Defines a set of status codes for tests."""
PENDING = 10, _('Pending'), 'secondary' PENDING = 10, _('Pending'), ColorEnum.secondary
PLACED = 20, _('Placed'), 'primary' PLACED = 20, _('Placed'), 'primary'
COMPLETE = 30, _('Complete'), 'success' COMPLETE = 30, _('Complete'), ColorEnum.success
ABC = None # This should be ignored ABC = None # This should be ignored
_DEF = None # This should be ignored _DEF = None # This should be ignored
jkl = None # This should be ignored jkl = None # This should be ignored
@ -120,8 +124,19 @@ class GeneralStateTest(InvenTreeTestCase):
# label # label
self.assertEqual(GeneralStatus.label(10), 'Pending') self.assertEqual(GeneralStatus.label(10), 'Pending')
def test_tag_function(self): def test_color(self):
"""Test that the status code tag functions.""" """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 from .tags import status_label
self.assertEqual( self.assertEqual(
@ -137,6 +152,21 @@ class GeneralStateTest(InvenTreeTestCase):
# Test non-existent key # Test non-existent key
self.assertEqual(status_label('general', 100), '100') 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): def test_api(self):
"""Test StatusView API view.""" """Test StatusView API view."""
view = StatusView.as_view() view = StatusView.as_view()
@ -191,3 +221,59 @@ class GeneralStateTest(InvenTreeTestCase):
self.assertEqual( self.assertEqual(
str(e.exception), '`status_class` not a valid StatusCode class' 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 django.utils.translation import gettext_lazy as _
from generic.states import StatusCode from generic.states import ColorEnum, StatusCode
class DataImportStatusCode(StatusCode): class DataImportStatusCode(StatusCode):
"""Defines a set of status codes for a DataImportSession.""" """Defines a set of status codes for a DataImportSession."""
INITIAL = 0, _('Initializing'), 'secondary' # Import session has been created INITIAL = (
MAPPING = 10, _('Mapping Columns'), 'primary' # Import fields are being mapped 0,
IMPORTING = 20, _('Importing Data'), 'primary' # Data is being imported _('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 = ( PROCESSING = (
30, 30,
_('Processing Data'), _('Processing Data'),
'primary', ColorEnum.primary,
) # Data is being processed by the user ) # 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 import serializers
from rest_framework.request import Request from rest_framework.request import Request
from generic.states import ColorEnum
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
from plugin import registry as plg_registry from plugin import registry as plg_registry
from plugin.base.label.mixins import LabelPrintingMixin from plugin.base.label.mixins import LabelPrintingMixin
@ -228,12 +229,12 @@ class LabelPrinterStatus(MachineStatus):
DISCONNECTED: The driver cannot establish a connection to the printer DISCONNECTED: The driver cannot establish a connection to the printer
""" """
CONNECTED = 100, _('Connected'), 'success' CONNECTED = 100, _('Connected'), ColorEnum.success
UNKNOWN = 101, _('Unknown'), 'secondary' UNKNOWN = 101, _('Unknown'), ColorEnum.secondary
PRINTING = 110, _('Printing'), 'primary' PRINTING = 110, _('Printing'), ColorEnum.primary
NO_MEDIA = 301, _('No media'), 'warning' NO_MEDIA = 301, _('No media'), ColorEnum.warning
PAPER_JAM = 302, _('Paper jam'), 'warning' PAPER_JAM = 302, _('Paper jam'), ColorEnum.warning
DISCONNECTED = 400, _('Disconnected'), 'danger' DISCONNECTED = 400, _('Disconnected'), ColorEnum.danger
class LabelPrinterMachine(BaseMachineType): 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 common.settings import get_global_setting
from company.models import Address, Company, Contact, SupplierPart from company.models import Address, Company, Contact, SupplierPart
from generic.states import StateTransitionMixin from generic.states import StateTransitionMixin
from generic.states.fields import InvenTreeCustomStatusModelField
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
from InvenTree.fields import ( from InvenTree.fields import (
InvenTreeModelMoneyField, InvenTreeModelMoneyField,
@ -470,7 +471,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
validators=[order.validators.validate_purchase_order_reference], validators=[order.validators.validate_purchase_order_reference],
) )
status = models.PositiveIntegerField( status = InvenTreeCustomStatusModelField(
default=PurchaseOrderStatus.PENDING.value, default=PurchaseOrderStatus.PENDING.value,
choices=PurchaseOrderStatus.items(), choices=PurchaseOrderStatus.items(),
verbose_name=_('Status'), verbose_name=_('Status'),
@ -996,7 +997,7 @@ class SalesOrder(TotalPriceMixin, Order):
"""Accessor helper for Order base.""" """Accessor helper for Order base."""
return self.customer return self.customer
status = models.PositiveIntegerField( status = InvenTreeCustomStatusModelField(
default=SalesOrderStatus.PENDING.value, default=SalesOrderStatus.PENDING.value,
choices=SalesOrderStatus.items(), choices=SalesOrderStatus.items(),
verbose_name=_('Status'), verbose_name=_('Status'),
@ -2153,7 +2154,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Accessor helper for Order base class.""" """Accessor helper for Order base class."""
return self.customer return self.customer
status = models.PositiveIntegerField( status = InvenTreeCustomStatusModelField(
default=ReturnOrderStatus.PENDING.value, default=ReturnOrderStatus.PENDING.value,
choices=ReturnOrderStatus.items(), choices=ReturnOrderStatus.items(),
verbose_name=_('Status'), verbose_name=_('Status'),
@ -2404,7 +2405,7 @@ class ReturnOrderLineItem(OrderLineItem):
"""Return True if this item has been received.""" """Return True if this item has been received."""
return self.received_date is not None return self.received_date is not None
outcome = models.PositiveIntegerField( outcome = InvenTreeCustomStatusModelField(
default=ReturnOrderLineStatus.PENDING.value, default=ReturnOrderLineStatus.PENDING.value,
choices=ReturnOrderLineStatus.items(), choices=ReturnOrderLineStatus.items(),
verbose_name=_('Outcome'), verbose_name=_('Outcome'),

View File

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

View File

@ -2,20 +2,20 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode from generic.states import ColorEnum, StatusCode
class PurchaseOrderStatus(StatusCode): class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder.""" """Defines a set of status codes for a PurchaseOrder."""
# Order status codes # Order status codes
PENDING = 10, _('Pending'), 'secondary' # Order is pending (not yet placed) PENDING = 10, _('Pending'), ColorEnum.secondary # Order is pending (not yet placed)
PLACED = 20, _('Placed'), 'primary' # Order has been placed with supplier PLACED = 20, _('Placed'), ColorEnum.primary # Order has been placed with supplier
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Order is on hold
COMPLETE = 30, _('Complete'), 'success' # Order has been completed COMPLETE = 30, _('Complete'), ColorEnum.success # Order has been completed
CANCELLED = 40, _('Cancelled'), 'danger' # Order was cancelled CANCELLED = 40, _('Cancelled'), ColorEnum.danger # Order was cancelled
LOST = 50, _('Lost'), 'warning' # Order was lost LOST = 50, _('Lost'), ColorEnum.warning # Order was lost
RETURNED = 60, _('Returned'), 'warning' # Order was returned RETURNED = 60, _('Returned'), ColorEnum.warning # Order was returned
class PurchaseOrderStatusGroups: class PurchaseOrderStatusGroups:
@ -39,18 +39,18 @@ class PurchaseOrderStatusGroups:
class SalesOrderStatus(StatusCode): class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder.""" """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 = ( IN_PROGRESS = (
15, 15,
_('In Progress'), _('In Progress'),
'primary', ColorEnum.primary,
) # Order has been issued, and is in progress ) # Order has been issued, and is in progress
SHIPPED = 20, _('Shipped'), 'success' # Order has been shipped to customer SHIPPED = 20, _('Shipped'), ColorEnum.success # Order has been shipped to customer
ON_HOLD = 25, _('On Hold'), 'warning' # Order is on hold ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Order is on hold
COMPLETE = 30, _('Complete'), 'success' # Order is complete COMPLETE = 30, _('Complete'), ColorEnum.success # Order is complete
CANCELLED = 40, _('Cancelled'), 'danger' # Order has been cancelled CANCELLED = 40, _('Cancelled'), ColorEnum.danger # Order has been cancelled
LOST = 50, _('Lost'), 'warning' # Order was lost LOST = 50, _('Lost'), ColorEnum.warning # Order was lost
RETURNED = 60, _('Returned'), 'warning' # Order was returned RETURNED = 60, _('Returned'), ColorEnum.warning # Order was returned
class SalesOrderStatusGroups: class SalesOrderStatusGroups:
@ -71,15 +71,15 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder.""" """Defines a set of status codes for a ReturnOrder."""
# Order is pending, waiting for receipt of items # 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 # 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' COMPLETE = 30, _('Complete'), ColorEnum.success
CANCELLED = 40, _('Cancelled'), 'danger' CANCELLED = 40, _('Cancelled'), ColorEnum.danger
class ReturnOrderStatusGroups: class ReturnOrderStatusGroups:
@ -95,19 +95,19 @@ class ReturnOrderStatusGroups:
class ReturnOrderLineStatus(StatusCode): class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem.""" """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 # 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 # 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) # 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) # Item is to be refunded (cannot be repaired)
REFUND = 50, _('Refund'), 'info' REFUND = 50, _('Refund'), ColorEnum.info
# Item is rejected # 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><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
<td> <td>
{% status_label 'purchase_order' order.status %} {% display_status_label 'purchase_order' order.status_custom_key order.status %}
{% if order.is_overdue %} {% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

@ -491,7 +491,10 @@ class PrintTestMixins:
def do_activate_plugin(self): def do_activate_plugin(self):
"""Activate the 'samplelabel' plugin.""" """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.active = True
config.save() config.save()

View File

@ -142,6 +142,7 @@ class StockItemResource(InvenTreeResource):
'barcode_hash', 'barcode_hash',
'barcode_data', 'barcode_data',
'owner', 'owner',
'status_custom_key',
] ]
id = Field( 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.icons import validate_icon
from common.settings import get_global_setting from common.settings import get_global_setting
from company import models as CompanyModels from company import models as CompanyModels
from generic.states.fields import InvenTreeCustomStatusModelField
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField 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 part import models as PartModels
from plugin.events import trigger_event from plugin.events import trigger_event
from stock import models as StockModels from stock import models as StockModels
from stock.generators import generate_batch_code from stock.generators import generate_batch_code
from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups
from users.models import Owner from users.models import Owner
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -940,7 +945,7 @@ class StockItem(
help_text=_('Delete this Stock Item when stock is depleted'), help_text=_('Delete this Stock Item when stock is depleted'),
) )
status = models.PositiveIntegerField( status = InvenTreeCustomStatusModelField(
default=StockStatus.OK.value, default=StockStatus.OK.value,
choices=StockStatus.items(), choices=StockStatus.items(),
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],

View File

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

View File

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

View File

@ -425,7 +425,7 @@
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</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> </tr>
{% if item.expiry_date %} {% if item.expiry_date %}
<tr> <tr>

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
from enum import IntEnum from enum import IntEnum
import django.http import django.http
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
@ -17,7 +18,7 @@ from rest_framework import status
import build.models import build.models
import company.models import company.models
import part.models import part.models
from common.models import InvenTreeSetting from common.models import InvenTreeCustomUserStateModel, InvenTreeSetting
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part, PartTestTemplate from part.models import Part, PartTestTemplate
from stock.models import ( 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): class StockItemTest(StockAPITestCase):
"""Series of API tests for the StockItem API.""" """Series of API tests for the StockItem API."""

View File

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

View File

@ -1788,12 +1788,12 @@ function loadPurchaseOrderTable(table, options) {
} }
}, },
{ {
field: 'status', field: 'status_custom_key',
title: '{% trans "Status" %}', title: '{% trans "Status" %}',
switchable: true, switchable: true,
sortable: true, sortable: true,
formatter: function(value, row) { 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, sortable: true,
field: 'status', field: 'status_custom_key',
title: '{% trans "Status" %}', title: '{% trans "Status" %}',
formatter: function(value, row) { 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, sortable: true,
field: 'status', field: 'status_custom_key',
title: '{% trans "Status" %}', title: '{% trans "Status" %}',
formatter: function(value, row) { formatter: function(value, row) {
return salesOrderStatusDisplay(row.status); return salesOrderStatusDisplay(row.status_custom_key);
} }
}, },
{ {

View File

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

View File

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

View File

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

View File

@ -62,3 +62,6 @@ opentelemetry-exporter-otlp
opentelemetry-instrumentation-django opentelemetry-instrumentation-django
opentelemetry-instrumentation-requests opentelemetry-instrumentation-requests
opentelemetry-instrumentation-redis 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)) { 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 { } else {
processErrors(v, path); 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 { FormProvider, useForm } from 'react-hook-form';
import { ApiFormField, ApiFormFieldType } from './fields/ApiFormField'; import { ApiFormField, ApiFormFieldType } from './fields/ApiFormField';
export function StandaloneField({ export function StandaloneField({
fieldDefinition, fieldDefinition,
fieldName = 'field',
defaultValue, defaultValue,
hideLabels hideLabels,
error
}: { }: {
fieldDefinition: ApiFormFieldType; fieldDefinition: ApiFormFieldType;
fieldName?: string;
defaultValue?: any; defaultValue?: any;
hideLabels?: boolean; hideLabels?: boolean;
error?: string;
}) { }) {
// Field must have a defined name
const name = useMemo(() => fieldName ?? 'field', [fieldName]);
const defaultValues = useMemo(() => { const defaultValues = useMemo(() => {
if (defaultValue) if (defaultValue)
return { return {
field: defaultValue [name]: defaultValue
}; };
return {}; return {};
}, [defaultValue]); }, [defaultValue]);
const form = useForm<{}>({ const form = useForm({
criteriaMode: 'all', criteriaMode: 'all',
defaultValues defaultValues
}); });
useEffect(() => {
form.clearErrors();
if (!!error) {
form.setError(name, { message: error });
}
}, [form, error]);
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<ApiFormField <ApiFormField
fieldName="field" fieldName={name}
definition={fieldDefinition} definition={fieldDefinition}
control={form.control} control={form.control}
hideLabels={hideLabels} hideLabels={hideLabels}
setFields={undefined}
/> />
</FormProvider> </FormProvider>
); );

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ import { ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { InvenTreeQRCode } from './QRCode'; import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode';
export type ActionDropdownItem = { export type ActionDropdownItem = {
icon?: ReactNode; icon?: ReactNode;
@ -112,69 +112,91 @@ export function ActionDropdown({
// Dropdown menu for barcode actions // Dropdown menu for barcode actions
export function BarcodeActionDropdown({ export function BarcodeActionDropdown({
actions model,
}: { pk,
actions: ActionDropdownItem[]; 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 ( return (
<ActionDropdown <ActionDropdown
tooltip={t`Barcode Actions`} tooltip={t`Barcode Actions`}
icon={<IconQrcode />} 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 type QrCodeType = {
export function ViewBarcodeAction({
hidden = false,
model,
pk
}: {
hidden?: boolean;
model: ModelType; model: ModelType;
pk: number; 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 { }): ActionDropdownItem {
const onClick = () => { const onClick = () => {
modals.open({ modals.open({
title: t`View Barcode`, title: title,
children: <InvenTreeQRCode model={model} pk={pk} /> children: <ChildItem mdl_prop={mdl_prop} />
}); });
}; };
return { return {
icon: <IconQrcode />, icon: icon,
name: t`View`, name: title,
tooltip: t`View barcode`, tooltip: tooltip,
onClick: onClick, onClick: onClick,
hidden: hidden 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 // Common action button for editing an item
export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem { export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem {
return { return {

View File

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

View File

@ -1,24 +1,28 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { import {
Alert,
Box, Box,
Button,
Code, Code,
Group, Group,
Image, Image,
Select, Select,
Skeleton, Skeleton,
Stack, Stack,
Text Text,
TextInput
} from '@mantine/core'; } from '@mantine/core';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode'; import QR from 'qrcode';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import { QrCodeType } from './ActionDropdown';
type QRCodeProps = { type QRCodeProps = {
ecl?: 'L' | 'M' | 'Q' | 'H'; ecl?: 'L' | 'M' | 'Q' | 'H';
@ -51,15 +55,13 @@ export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => {
}; };
type InvenTreeQRCodeProps = { type InvenTreeQRCodeProps = {
model: ModelType; mdl_prop: QrCodeType;
pk: number;
showEclSelector?: boolean; showEclSelector?: boolean;
} & Omit<QRCodeProps, 'data'>; } & Omit<QRCodeProps, 'data'>;
export const InvenTreeQRCode = ({ export const InvenTreeQRCode = ({
mdl_prop,
showEclSelector = true, showEclSelector = true,
model,
pk,
ecl: eclProp = 'Q', ecl: eclProp = 'Q',
...props ...props
}: InvenTreeQRCodeProps) => { }: InvenTreeQRCodeProps) => {
@ -71,11 +73,11 @@ export const InvenTreeQRCode = ({
}, [eclProp]); }, [eclProp]);
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['qr-code', model, pk], queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk],
queryFn: async () => { queryFn: async () => {
const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), { const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), {
model, model: mdl_prop.model,
pk pk: mdl_prop.pk
}); });
return res.data?.barcode as string; return res.data?.barcode as string;
@ -94,6 +96,15 @@ export const InvenTreeQRCode = ({
return ( return (
<Stack> <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} /> <QRCode data={data} ecl={ecl} {...props} />
{data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && ( {data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && (
@ -128,3 +139,55 @@ export const InvenTreeQRCode = ({
</Stack> </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} primary={instance.reference}
secondary={instance.title} secondary={instance.title}
suffix={StatusRenderer({ suffix={StatusRenderer({
status: instance.status, status: instance.status_custom_key,
type: ModelType.build type: ModelType.build
})} })}
image={instance.part_detail?.thumbnail || instance.part_detail?.image} image={instance.part_detail?.thumbnail || instance.part_detail?.image}
@ -39,7 +39,7 @@ export function RenderBuildLine({
primary={instance.part_detail.full_name} primary={instance.part_detail.full_name}
secondary={instance.quantity} secondary={instance.quantity}
suffix={StatusRenderer({ suffix={StatusRenderer({
status: instance.status, status: instance.status_custom_key,
type: ModelType.build type: ModelType.build
})} })}
image={instance.part_detail.thumbnail || instance.part_detail.image} 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({ export function RenderImportSession({
instance instance
}: { }: {

View File

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

View File

@ -241,6 +241,11 @@ export const ModelInformationDict: ModelDict = {
url_overview: '/pluginconfig', url_overview: '/pluginconfig',
url_detail: '/pluginconfig/:pk/', url_detail: '/pluginconfig/:pk/',
api_endpoint: ApiEndpoints.plugin_list 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} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({ suffix={StatusRenderer({
status: instance.status, status: instance.status_custom_key,
type: ModelType.purchaseorder type: ModelType.purchaseorder
})} })}
image={supplier.thumnbnail || supplier.image} image={supplier.thumnbnail || supplier.image}
@ -49,7 +49,7 @@ export function RenderReturnOrder(
primary={instance.reference} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({ suffix={StatusRenderer({
status: instance.status, status: instance.status_custom_key,
type: ModelType.returnorder type: ModelType.returnorder
})} })}
image={customer.thumnbnail || customer.image} image={customer.thumnbnail || customer.image}
@ -94,7 +94,7 @@ export function RenderSalesOrder(
primary={instance.reference} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({ suffix={StatusRenderer({
status: instance.status, status: instance.status_custom_key,
type: ModelType.salesorder type: ModelType.salesorder
})} })}
image={customer.thumnbnail || customer.image} 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_global_list = 'settings/global/',
settings_user_list = 'settings/user/', settings_user_list = 'settings/user/',
barcode = 'barcode/', barcode = 'barcode/',
barcode_link = 'barcode/link/',
barcode_unlink = 'barcode/unlink/',
generate_barcode = 'barcode/generate/', generate_barcode = 'barcode/generate/',
news = 'news/', news = 'news/',
global_status = 'generic/status/', global_status = 'generic/status/',
custom_state_list = 'generic/status/custom/',
version = 'version/', version = 'version/',
license = 'license/', license = 'license/',
sso_providers = 'auth/providers/', sso_providers = 'auth/providers/',
group_list = 'user/group/', group_list = 'user/group/',
owner_list = 'user/owner/', owner_list = 'user/owner/',
content_type_list = 'contenttype/',
icons = 'icons/', icons = 'icons/',
// Data import endpoints // Data import endpoints
@ -70,6 +74,9 @@ export enum ApiEndpoints {
build_output_create = 'build/:id/create-output/', build_output_create = 'build/:id/create-output/',
build_output_scrap = 'build/:id/scrap-outputs/', build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-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_line_list = 'build/line/',
build_item_list = 'build/item/', build_item_list = 'build/item/',

View File

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

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Alert, Stack, Text } from '@mantine/core'; import { Alert, Stack, Table, Text } from '@mantine/core';
import { import {
IconCalendar, IconCalendar,
IconLink, IconLink,
@ -10,16 +10,26 @@ import {
IconUsersGroup IconUsersGroup
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable'; 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 { api } from '../App';
import { ActionButton } from '../components/buttons/ActionButton'; 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 { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType'; import { ModelType } from '../enums/ModelType';
import { resolveItem } from '../functions/conversion';
import { InvenTreeIcon } from '../functions/icons'; import { InvenTreeIcon } from '../functions/icons';
import { useCreateApiFormModal } from '../hooks/UseForm'; import { useCreateApiFormModal } from '../hooks/UseForm';
import { useBatchCodeGenerator } from '../hooks/UseGenerator'; import { useBatchCodeGenerator } from '../hooks/UseGenerator';
import { useSelectedRows } from '../hooks/UseSelectedRows';
import { apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState'; import { useGlobalSettingsState } from '../states/SettingsState';
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers'; import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
@ -240,7 +250,7 @@ function buildOutputFormTable(outputs: any[], onRemove: (output: any) => void) {
tooltip={t`Remove output`} tooltip={t`Remove output`}
icon={<InvenTreeIcon icon="cancel" />} icon={<InvenTreeIcon icon="cancel" />}
color="red" color="red"
onClick={() => onRemove(record)} onClick={() => onRemove(record.pk)}
disabled={outputs.length <= 1} disabled={outputs.length <= 1}
/> />
) )
@ -259,13 +269,11 @@ export function useCompleteBuildOutputsForm({
outputs: any[]; outputs: any[];
onFormSuccess: (response: any) => void; onFormSuccess: (response: any) => void;
}) { }) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
const [location, setLocation] = useState<number | null>(null); const [location, setLocation] = useState<number | null>(null);
useEffect(() => { const { selectedRows, removeRow } = useSelectedRows({
setSelectedOutputs(outputs); rows: outputs
}, [outputs]); });
useEffect(() => { useEffect(() => {
if (location) { if (location) {
@ -277,31 +285,21 @@ export function useCompleteBuildOutputsForm({
); );
}, [location, build.destination, build.part_detail]); }, [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(() => { const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput); return buildOutputFormTable(selectedRows, removeRow);
}, [selectedOutputs, removeOutput]); }, [selectedRows, removeRow]);
const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => { const buildOutputCompleteFields: ApiFormFieldSet = useMemo(() => {
return { return {
outputs: { outputs: {
hidden: true, hidden: true,
value: selectedOutputs.map((output) => { value: selectedRows.map((output: any) => {
return { return {
output: output.pk output: output.pk
}; };
}) })
}, },
status: {}, status_custom_key: {},
location: { location: {
filters: { filters: {
structural: false structural: false
@ -314,7 +312,7 @@ export function useCompleteBuildOutputsForm({
notes: {}, notes: {},
accept_incomplete_allocation: {} accept_incomplete_allocation: {}
}; };
}, [selectedOutputs, location]); }, [selectedRows, location]);
return useCreateApiFormModal({ return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_complete, build.pk), 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({ export function useScrapBuildOutputsForm({
build, build,
outputs, outputs,
@ -337,21 +338,10 @@ export function useScrapBuildOutputsForm({
onFormSuccess: (response: any) => void; onFormSuccess: (response: any) => void;
}) { }) {
const [location, setLocation] = useState<number | null>(null); const [location, setLocation] = useState<number | null>(null);
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
useEffect(() => { const { selectedRows, removeRow } = useSelectedRows({
setSelectedOutputs(outputs); rows: outputs
}, [outputs]); });
// Remove a selected output from the list
const removeOutput = useCallback(
(output: any) => {
setSelectedOutputs(
selectedOutputs.filter((item) => item.pk != output.pk)
);
},
[selectedOutputs]
);
useEffect(() => { useEffect(() => {
if (location) { if (location) {
@ -364,14 +354,14 @@ export function useScrapBuildOutputsForm({
}, [location, build.destination, build.part_detail]); }, [location, build.destination, build.part_detail]);
const preFormContent = useMemo(() => { const preFormContent = useMemo(() => {
return buildOutputFormTable(selectedOutputs, removeOutput); return buildOutputFormTable(selectedRows, removeRow);
}, [selectedOutputs, removeOutput]); }, [selectedRows, removeRow]);
const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => { const buildOutputScrapFields: ApiFormFieldSet = useMemo(() => {
return { return {
outputs: { outputs: {
hidden: true, hidden: true,
value: selectedOutputs.map((output) => { value: selectedRows.map((output: any) => {
return { return {
output: output.pk, output: output.pk,
quantity: output.quantity quantity: output.quantity
@ -387,7 +377,7 @@ export function useScrapBuildOutputsForm({
notes: {}, notes: {},
discard_allocations: {} discard_allocations: {}
}; };
}, [location, selectedOutputs]); }, [location, selectedRows]);
return useCreateApiFormModal({ return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_scrap, build.pk), url: apiUrl(ApiEndpoints.build_output_scrap, build.pk),
@ -409,21 +399,9 @@ export function useCancelBuildOutputsForm({
outputs: any[]; outputs: any[];
onFormSuccess: (response: any) => void; onFormSuccess: (response: any) => void;
}) { }) {
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]); const { selectedRows, removeRow } = useSelectedRows({
rows: outputs
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 preFormContent = useMemo(() => { const preFormContent = useMemo(() => {
return ( return (
@ -431,23 +409,23 @@ export function useCancelBuildOutputsForm({
<Alert color="red" title={t`Cancel Build Outputs`}> <Alert color="red" title={t`Cancel Build Outputs`}>
<Text>{t`Selected build outputs will be deleted`}</Text> <Text>{t`Selected build outputs will be deleted`}</Text>
</Alert> </Alert>
{buildOutputFormTable(selectedOutputs, removeOutput)} {buildOutputFormTable(selectedRows, removeRow)}
</Stack> </Stack>
); );
}, [selectedOutputs, removeOutput]); }, [selectedRows, removeRow]);
const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => { const buildOutputCancelFields: ApiFormFieldSet = useMemo(() => {
return { return {
outputs: { outputs: {
hidden: true, hidden: true,
value: selectedOutputs.map((output) => { value: selectedRows.map((output: any) => {
return { return {
output: output.pk output: output.pk
}; };
}) })
} }
}; };
}, [selectedOutputs]); }, [selectedRows]);
return useCreateApiFormModal({ return useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_output_delete, build.pk), url: apiUrl(ApiEndpoints.build_output_delete, build.pk),
@ -459,3 +437,233 @@ export function useCancelBuildOutputsForm({
successMessage: t`Build outputs have been cancelled` 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 { export function customUnitsFields(): ApiFormFieldSet {
return { return {
name: {}, name: {},

View File

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

View File

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

View File

@ -1,21 +1,24 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Divider, Stack } from '@mantine/core';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconReload } from '@tabler/icons-react'; import { IconReload } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../../../App';
import { ActionButton } from '../../components/buttons/ActionButton'; import { ActionButton } from '../../../../components/buttons/ActionButton';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { FactCollection } from '../../../../components/settings/FactCollection';
import { useTable } from '../../hooks/UseTable'; import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState'; import { useTable } from '../../../../hooks/UseTable';
import { InvenTreeTable } from '../InvenTreeTable'; import { apiUrl } from '../../../../states/ApiState';
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
/* /*
* Table for displaying available currencies * Table for displaying available currencies
*/ */
export default function CurrencyTable() { export function CurrencyTable({
setInfo
}: Readonly<{ setInfo: (info: any) => void }>) {
const table = useTable('currency'); const table = useTable('currency');
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {
@ -53,6 +56,7 @@ export default function CurrencyTable() {
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<ActionButton <ActionButton
key="refresh"
onClick={refreshCurrencies} onClick={refreshCurrencies}
tooltip={t`Refresh currency exchange rates`} tooltip={t`Refresh currency exchange rates`}
icon={<IconReload />} icon={<IconReload />}
@ -66,8 +70,10 @@ export default function CurrencyTable() {
tableState={table} tableState={table}
columns={columns} columns={columns}
props={{ props={{
idAccessor: 'currency',
tableActions: tableActions, tableActions: tableActions,
dataFormatter: (data: any) => { dataFormatter: (data: any) => {
setInfo(data);
let rates = data.exchange_rates ?? {}; let rates = data.exchange_rates ?? {};
return Object.entries(rates).map(([currency, rate]) => { 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')) lazy(() => import('./TaskManagementPanel'))
); );
const CurrencyManagmentPanel = Loadable(
lazy(() => import('./CurrencyManagmentPanel'))
);
const PluginManagementPanel = Loadable( const PluginManagementPanel = Loadable(
lazy(() => import('./PluginManagementPanel')) lazy(() => import('./PluginManagementPanel'))
); );
@ -64,6 +68,10 @@ const ProjectCodeTable = Loadable(
lazy(() => import('../../../../tables/settings/ProjectCodeTable')) lazy(() => import('../../../../tables/settings/ProjectCodeTable'))
); );
const CustomStateTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomStateTable'))
);
const CustomUnitsTable = Loadable( const CustomUnitsTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomUnitsTable')) lazy(() => import('../../../../tables/settings/CustomUnitsTable'))
); );
@ -80,10 +88,6 @@ const LocationTypesTable = Loadable(
lazy(() => import('../../../../tables/stock/LocationTypesTable')) lazy(() => import('../../../../tables/stock/LocationTypesTable'))
); );
const CurrencyTable = Loadable(
lazy(() => import('../../../../tables/settings/CurrencyTable'))
);
export default function AdminCenter() { export default function AdminCenter() {
const user = useUserState(); const user = useUserState();
const navigate = useNavigate(); const navigate = useNavigate();
@ -133,7 +137,7 @@ export default function AdminCenter() {
name: 'currencies', name: 'currencies',
label: t`Currencies`, label: t`Currencies`,
icon: <IconCoins />, icon: <IconCoins />,
content: <CurrencyTable /> content: <CurrencyManagmentPanel />
}, },
{ {
name: 'projectcodes', name: 'projectcodes',
@ -147,6 +151,12 @@ export default function AdminCenter() {
</Stack> </Stack>
) )
}, },
{
name: 'customstates',
label: t`Custom States`,
icon: <IconListDetails />,
content: <CustomStateTable />
},
{ {
name: 'customunits', name: 'customunits',
label: t`Custom Units`, label: t`Custom Units`,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
export type TableColumnProps<T = any> = { export type TableColumnProps<T = any> = {
accessor?: string; // The key in the record to access 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 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 sortable?: boolean; // Whether the column is sortable
switchable?: boolean; // Whether the column is switchable switchable?: boolean; // Whether the column is switchable
hidden?: boolean; // Whether the column is hidden 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 render?: (record: T, index?: number) => any; // A custom render function
filter?: any; // A custom filter function filter?: any; // A custom filter function
filtering?: boolean; // Whether the column is filterable filtering?: boolean; // Whether the column is filterable

View File

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

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