2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-05 06:32:55 +00:00

Merge branch 'master' into custom-states

This commit is contained in:
Oliver
2024-11-28 00:31:00 +11:00
committed by GitHub
106 changed files with 3694 additions and 1638 deletions
+1 -1
View File
@@ -157,7 +157,7 @@ jobs:
- name: Extract Docker metadata
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # pin@v5.5.1
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # pin@v5.6.1
with:
images: |
inventree/inventree
+3 -3
View File
@@ -308,7 +308,7 @@ jobs:
- name: Coverage Tests
run: invoke dev.test --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # pin@v5.0.2
uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # pin@v5.0.7
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -440,7 +440,7 @@ jobs:
- name: Run Tests
run: invoke dev.test --migrations --report --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # pin@v5.0.2
uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # pin@v5.0.7
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -545,7 +545,7 @@ jobs:
if: always()
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # pin@v5.0.2
uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # pin@v5.0.7
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # pin@v0
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
+1 -1
View File
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
sarif_file: results.sarif
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
git reset --hard
git reset HEAD~
- name: crowdin action
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # pin@v2
uses: crowdin/github-action@a9ffb7d5ac46eca1bb1f06656bf888b39462f161 # pin@v2
with:
upload_sources: true
upload_translations: false
+3 -3
View File
@@ -1,7 +1,7 @@
# Example Caddyfile for Inventree
# Example Caddyfile for InvenTree
# The following environment variables may be used:
# - INVENTREE_SITE_URL: The upstream URL of the Inventree site (default: inventree.localhost)
# - INVENTREE_SERVER: The internal URL of the Inventree container (default: http://inventree-server:8000)
# - INVENTREE_SITE_URL: The upstream URL of the InvenTree site (default: inventree.localhost)
# - INVENTREE_SERVER: The internal URL of the InvenTree container (default: http://inventree-server:8000)
#
# Note that while this file is a good starting point, it may need to be modified to suit your specific requirements
+58 -58
View File
@@ -10,9 +10,9 @@ django==4.2.16 \
# via
# -r contrib/container/requirements.in
# django-auth-ldap
django-auth-ldap==4.8.0 \
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
--hash=sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738
django-auth-ldap==5.1.0 \
--hash=sha256:9c607e8d9c53cf2a0ccafbe0acfc33eb1d1fd474c46ec52d30aee0dca1da9668 \
--hash=sha256:a5f7bdb54b2ab80e4e9eb080cd3e06e89e4c9d2d534ddb39b66cd970dd6d3536
# via -r contrib/container/requirements.in
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
@@ -22,32 +22,32 @@ invoke==2.2.0 \
--hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 \
--hash=sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5
# via -r contrib/container/requirements.in
mariadb==1.1.10 \
--hash=sha256:03d6284ef713d1cad40146576a4cc2d6cbc1662060f2a0e59b174e1694521698 \
--hash=sha256:1ce87971c02375236ff8933e6c593c748e7b2f2950b86eabfab4289fd250ea63 \
--hash=sha256:1d81b22efbaaf4c5bc5e4cc4e2ef3c459538c1a939371089d8c5591d6f26a62e \
--hash=sha256:29040e426f877ddc45f337c6eb381b6bbab63cc6bf8431a28effe30162142513 \
--hash=sha256:4521aa721f926946bd71491f872e8babc78fa97755ed2114f5684b77363107cb \
--hash=sha256:49200378c614984f5ec875481662a49d7c97c2be27970b01b32fa4b7520d4e22 \
--hash=sha256:5d652117e2fdf12b9723e7452a05fce9e6ccbae6ea48871b21a3a8fde259dc48 \
--hash=sha256:8c8c6b27486b0e1772a23002c702b5fd244eecf9f05633dd6cb345fc26755a20 \
--hash=sha256:a332893e3ef7ceb7970ab4bd7c844bcb4bd68a051ca51313566f9808d7411f2d \
--hash=sha256:d7b09ec4abd02ed235257feb769f90cd4066e8f536b55b92f5166103d5b66a63 \
--hash=sha256:dff8b28ce4044574870d7bdd2d9f9f5da8e5f95a7ff6d226185db733060d1a93
mariadb==1.1.11 \
--hash=sha256:0f8de8d66ca71bd102f34a970a331b7d75bdf7f8050d80e37cdcc6ff3c85cf7a \
--hash=sha256:2e72ea65f1d7d8563ee84e172f2a583193092bdb6ff83c470ca9722873273ecc \
--hash=sha256:3f64b520089cb60c4f8302f365ed0ae057c4c859ab70fc8b1c4358192c3c8f27 \
--hash=sha256:579420293fa790d5ae0a6cb4bdb7e8be8facc2ceefb6123c2b0e8042b3fa725d \
--hash=sha256:6f28d8ccc597a3a1368be14078110f743900dbb3b0c7f1cce3072d83bec59c8a \
--hash=sha256:c1992ebf9c6f012ac158e33fef9f2c4ba899f721064c4ae3a3489233793296c0 \
--hash=sha256:cf6647cee081e21d0994b409ba8c8fa2077f3972f1de3627c5502fb31d14f806 \
--hash=sha256:d7302ccd15f0beee7b286885cbf6ac71ddc240374691d669784d99f89ba34d79 \
--hash=sha256:dbc4cf0e302ca82d46f9431a0b04f048e9c21ee56d6f3162c29605f84d63b40c \
--hash=sha256:e94f1738bec09c97b601ddbb1908eb24524ba4630f507a775d82ffdb6c5794b3 \
--hash=sha256:f6dfdc954edf02b6519419a054798cda6034dc459d1d482e3329e37aa27d34f0
# via -r contrib/container/requirements.in
mysqlclient==2.2.5 \
--hash=sha256:1d2e2ca0fe8405d8d6464edd01bf059951279e4bc27284d39341bd4737b2bc64 \
--hash=sha256:3f9625bea2b9bcde0ace76b32708762d44597491092c819fd1bff5b4e27f709b \
--hash=sha256:8012c633aab8c91ea8172ac479807135b171501b9cad1a7cd9b58c4dc8dcdab5 \
--hash=sha256:add8643c32f738014d252d2bdebb478623b04802e8396d5903905db36474d3ff \
--hash=sha256:aee14f1872114865679fcb09aac3772de4595fa7dcf2f83a4c7afee15e508854 \
--hash=sha256:b54511648c1455b43ac28f8b4c1f732c5b0c343e87f7a3bd6fc9f9fe0f91934e \
--hash=sha256:b78438314199504c64f69e1e3521f2c9b419f19fcd85158b44c997b64409a6af \
--hash=sha256:e871ede4261d0d42b8ed20a2459db411c7deafedd8e77b7e4ba760be4a6a752b
mysqlclient==2.2.6 \
--hash=sha256:3da70a07753ba6be881f7d75e795e254f6a0c12795778034acc69769b0649d37 \
--hash=sha256:43c5b30be0675080b9c815f457d73397f0442173e7be83d089b126835e2617ae \
--hash=sha256:794857bce4f9a1903a99786dd29ad7887f45a870b3d11585b8c51c4a753c4174 \
--hash=sha256:b0a5cddf1d3488b254605041070086cac743401d876a659a72d706a0d89c8ebb \
--hash=sha256:c0b46d9b78b461dbb62482089ca8040fa916595b1b30f831ebbd1b0a82b43d53 \
--hash=sha256:e940b41d85dfd7b190fa47d52f525f878cfa203d4653bf6a35b271b3c3be125b \
--hash=sha256:e94a92858203d97fd584bdb6d7ee8c56f2590db8d77fd44215c0dcf5e739bc37 \
--hash=sha256:f3efb849d6f7ef4b9788a0eda2e896b975e0ebf1d6bf3dcabea63fd698e5b0b5
# via -r contrib/container/requirements.in
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
packaging==24.2 \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
# via
# gunicorn
# mariadb
@@ -121,9 +121,9 @@ psycopg-binary==3.2.3 \
--hash=sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393 \
--hash=sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea
# via psycopg
psycopg-pool==3.2.3 \
--hash=sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1 \
--hash=sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a
psycopg-pool==3.2.4 \
--hash=sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed \
--hash=sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224
# via psycopg
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
@@ -195,13 +195,13 @@ pyyaml==6.0.2 \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via -r contrib/container/requirements.in
setuptools==75.2.0 \
--hash=sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec \
--hash=sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8
setuptools==75.6.0 \
--hash=sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6 \
--hash=sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d
# via -r contrib/container/requirements.in
sqlparse==0.5.1 \
--hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \
--hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e
sqlparse==0.5.2 \
--hash=sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f \
--hash=sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e
# via django
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
@@ -209,27 +209,27 @@ typing-extensions==4.12.2 \
# via
# psycopg
# psycopg-pool
uv==0.4.25 \
--hash=sha256:18100f0f36419a154306ed6211e3490bf18384cdf3f1a0950848bf64b62fa251 \
--hash=sha256:2d29a78f011ecc2f31c13605acb6574c2894c06d258b0f8d0dbb899986800450 \
--hash=sha256:2fc35b5273f1e018aecd66b70e0fd7d2eb6698853dde3e2fc644e7ebf9f825b1 \
--hash=sha256:3d7680795ea78cdbabbcce73d039b2651cf1fa635ddc1aa3082660f6d6255c50 \
--hash=sha256:4c55040e67470f2b73e95e432aba06f103a0b348ea0b9c6689b1029c8d9e89fd \
--hash=sha256:50c7d0d9e7f392f81b13bf3b7e37768d1486f2fc9d533a54982aa0ed11e4db23 \
--hash=sha256:578ae385fad6bd6f3868828e33d54994c716b315b1bc49106ec1f54c640837e4 \
--hash=sha256:6e981b1465e30102e41946adede9cb08051a5d70c6daf09f91a7ea84f0b75c08 \
--hash=sha256:7d266e02fefef930609328c31c075084295c3cb472bab3f69549fad4fd9d82b3 \
--hash=sha256:94fb2b454afa6bdfeeea4b4581c878944ca9cf3a13712e6762f245f5fbaaf952 \
--hash=sha256:a7022a71ff63a3838796f40e954b76bf7820fc27e96fe002c537e75ff8e34f1d \
--hash=sha256:a7c3a18c20ddb527d296d1222bddf42b78031c50b5b4609d426569b5fb61f5b0 \
--hash=sha256:aae9dcafd20d5ba978c8a4939ab942e8e2e155c109e9945207fbbd81d2892c9e \
--hash=sha256:bdbfd0c476b9e80a3f89af96aed6dd7d2782646311317a9c72614ccce99bb2ad \
--hash=sha256:be2a4fc4fcade9ea5e67e51738c95644360d6e59b6394b74fc579fb617f902f7 \
--hash=sha256:d39077cdfe3246885fcdf32e7066ae731a166101d063629f9cea08738f79e6a3 \
--hash=sha256:e02afb0f6d4b58718347f7d7cfa5a801e985ce42181ba971ed85ef149f6658ca \
--hash=sha256:ec181be2bda10651a3558156409ac481549983e0276d0e3645e3b1464e7f8715
uv==0.5.4 \
--hash=sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83 \
--hash=sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2 \
--hash=sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194 \
--hash=sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58 \
--hash=sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb \
--hash=sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c \
--hash=sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876 \
--hash=sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263 \
--hash=sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa \
--hash=sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01 \
--hash=sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00 \
--hash=sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6 \
--hash=sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39 \
--hash=sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25 \
--hash=sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927 \
--hash=sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4 \
--hash=sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119 \
--hash=sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc
# via -r contrib/container/requirements.in
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
wheel==0.45.1 \
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
--hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248
# via -r contrib/container/requirements.in
+160 -154
View File
@@ -1,104 +1,119 @@
# This file was autogenerated by uv via the following command:
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt --no-strip-extras --generate-hashes
certifi==2024.7.4 \
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
certifi==2024.8.30 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
--hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
# via requests
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
charset-normalizer==3.4.0 \
--hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \
--hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \
--hash=sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8 \
--hash=sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912 \
--hash=sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c \
--hash=sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b \
--hash=sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d \
--hash=sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d \
--hash=sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95 \
--hash=sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e \
--hash=sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565 \
--hash=sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64 \
--hash=sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab \
--hash=sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be \
--hash=sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e \
--hash=sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907 \
--hash=sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0 \
--hash=sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2 \
--hash=sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62 \
--hash=sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62 \
--hash=sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23 \
--hash=sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc \
--hash=sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284 \
--hash=sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca \
--hash=sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455 \
--hash=sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858 \
--hash=sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b \
--hash=sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594 \
--hash=sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc \
--hash=sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db \
--hash=sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b \
--hash=sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea \
--hash=sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6 \
--hash=sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920 \
--hash=sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749 \
--hash=sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7 \
--hash=sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd \
--hash=sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99 \
--hash=sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242 \
--hash=sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee \
--hash=sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129 \
--hash=sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2 \
--hash=sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51 \
--hash=sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee \
--hash=sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8 \
--hash=sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b \
--hash=sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613 \
--hash=sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742 \
--hash=sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe \
--hash=sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3 \
--hash=sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5 \
--hash=sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631 \
--hash=sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7 \
--hash=sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15 \
--hash=sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c \
--hash=sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea \
--hash=sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417 \
--hash=sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250 \
--hash=sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88 \
--hash=sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca \
--hash=sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa \
--hash=sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99 \
--hash=sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149 \
--hash=sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41 \
--hash=sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574 \
--hash=sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0 \
--hash=sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f \
--hash=sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d \
--hash=sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654 \
--hash=sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3 \
--hash=sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19 \
--hash=sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90 \
--hash=sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578 \
--hash=sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9 \
--hash=sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1 \
--hash=sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51 \
--hash=sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719 \
--hash=sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236 \
--hash=sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a \
--hash=sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c \
--hash=sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade \
--hash=sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944 \
--hash=sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc \
--hash=sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6 \
--hash=sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6 \
--hash=sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27 \
--hash=sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6 \
--hash=sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2 \
--hash=sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12 \
--hash=sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf \
--hash=sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114 \
--hash=sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7 \
--hash=sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf \
--hash=sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d \
--hash=sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b \
--hash=sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed \
--hash=sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03 \
--hash=sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4 \
--hash=sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67 \
--hash=sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365 \
--hash=sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a \
--hash=sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748 \
--hash=sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b \
--hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \
--hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482
# via requests
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via requests
jc==1.25.3 \
--hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \
@@ -171,63 +186,54 @@ ruamel-yaml==0.18.6 \
--hash=sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636 \
--hash=sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b
# via jc
ruamel-yaml-clib==0.2.8 \
--hash=sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d \
--hash=sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001 \
--hash=sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462 \
--hash=sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9 \
--hash=sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe \
--hash=sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b \
--hash=sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b \
--hash=sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615 \
--hash=sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62 \
--hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \
--hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \
--hash=sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1 \
--hash=sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9 \
--hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \
--hash=sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899 \
--hash=sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7 \
--hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \
--hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \
--hash=sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa \
--hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \
--hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \
--hash=sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6 \
--hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \
--hash=sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334 \
--hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \
--hash=sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3 \
--hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \
--hash=sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c \
--hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \
--hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \
--hash=sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880 \
--hash=sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f \
--hash=sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d \
--hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \
--hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \
--hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \
--hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \
--hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \
--hash=sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb \
--hash=sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942 \
--hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \
--hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \
--hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \
--hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \
--hash=sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28 \
--hash=sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d \
--hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \
--hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \
--hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \
--hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412
ruamel-yaml-clib==0.2.12 \
--hash=sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b \
--hash=sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4 \
--hash=sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef \
--hash=sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5 \
--hash=sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632 \
--hash=sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6 \
--hash=sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680 \
--hash=sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf \
--hash=sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da \
--hash=sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6 \
--hash=sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a \
--hash=sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519 \
--hash=sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6 \
--hash=sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f \
--hash=sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd \
--hash=sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2 \
--hash=sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52 \
--hash=sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd \
--hash=sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d \
--hash=sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c \
--hash=sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6 \
--hash=sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb \
--hash=sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969 \
--hash=sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28 \
--hash=sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e \
--hash=sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45 \
--hash=sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4 \
--hash=sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12 \
--hash=sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31 \
--hash=sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642 \
--hash=sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e \
--hash=sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285 \
--hash=sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed \
--hash=sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1 \
--hash=sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7 \
--hash=sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3 \
--hash=sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475 \
--hash=sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5 \
--hash=sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76 \
--hash=sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987 \
--hash=sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df
# via ruamel-yaml
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
urllib3==2.2.3 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
# via requests
xmltodict==0.13.0 \
--hash=sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56 \
--hash=sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852
xmltodict==0.14.2 \
--hash=sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553 \
--hash=sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac
# via jc
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+1 -1
View File
@@ -39,7 +39,7 @@ InvenTree allows you to upload simple BOM files in multiple formats, and downloa
## Build Parts
Inventree features a build management system to help you track the progress of your builds.
InvenTree features a build management system to help you track the progress of your builds.
Builds consume stock items to make new parts, you can decide to automatically or manually allocate parts from your current inventory.
[Read more...](./build/build.md)
+1 -1
View File
@@ -61,7 +61,7 @@ A *contact* can be assigned to orders, (such as [purchase orders](./purchase_ord
A company can have multiple registered addresses for use with all types of orders.
An address is broken down to internationally recognised elements that are designed to allow for formatting an address according to user needs.
Addresses are composed differently across the world, and Inventree reflects this by splitting addresses into components:
Addresses are composed differently across the world, and InvenTree reflects this by splitting addresses into components:
- Line 1: Main street address
- Line 2: Extra street address line
- Postal Code: Also known as ZIP code, this is normally a number 3-5 digits in length
+10
View File
@@ -26,6 +26,7 @@ Parameter templates are used to define the different types of parameters which a
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
| Selection List | If set, parameters linked to this template can only be assigned values from the linked [selection list](#selection-lists) |
### Create Template
@@ -105,3 +106,12 @@ Parameter sorting takes unit conversion into account, meaning that values provid
{% with id="sort_by_param_units", url="part/part_sorting_units.png", description="Sort by Parameter Units" %}
{% include 'img.html' %}
{% endwith %}
### Selection Lists
Selection Lists can be used to add a large number of predefined values to a parameter template. This can be useful for parameters which must be selected from a large predefined list of values (e.g. a list of standardised colo codes). Choices on templates are limited to 5000 characters, selection lists can be used to overcome this limitation.
It is possible that plugins lock selection lists to ensure a known state.
Administration of lists can be done through the Part Parameter section in the Admin Center or via the API.
+1 -1
View File
@@ -81,7 +81,7 @@ Multiple improvements have been made to the docker installation process, most no
### QR code scanner
[#2779](https://github.com/inventree/InvenTree/pull/2779) provides a QR code scanner which can be used to quickly scan Inventree generated QR codes using webcams or mobile devices. This feature requires secure (HTTPS) connection to the server.
[#2779](https://github.com/inventree/InvenTree/pull/2779) provides a QR code scanner which can be used to quickly scan InvenTree generated QR codes using webcams or mobile devices. This feature requires secure (HTTPS) connection to the server.
### Order, Order
+72
View File
@@ -78,6 +78,78 @@ To return an element corresponding to a certain key in a container which support
{% endraw %}
```
## Database Helpers
A number of helper functions are available for accessing database objects:
### filter_queryset
The `filter_queryset` function allows for arbitrary filtering of the provided querysert. It takes a queryset and a list of filter arguments, and returns a filtered queryset.
::: report.templatetags.report.filter_queryset
options:
show_docstring_description: false
show_source: False
!!! info "Provided QuerySet"
The provided queryset must be a valid Django queryset object, which is already available in the template context.
!!! warning "Advanced Users"
The `filter_queryset` function is a powerful tool, but it is also easy to misuse. It assumes that the user has a good understanding of Django querysets and the underlying database structure.
#### Example
In a report template which has a `PurchaseOrder` object available in its context, fetch any line items which have a received quantity greater than zero:
```html
{% raw %}
{% load report %}
{% filter_queryset order.lines.all received__gt=0 as received_lines %}
<ul>
{% for line in received_lines %}
<li>{{ line.part.part.full_name }} - {{ line.received }} / {{ line.quantity }}</li>
{% endfor %}
</ul>
{% endraw %}
```
### filter_db_model
The `filter_db_model` function allows for filtering of a database model based on a set of filter arguments. It takes a model class and a list of filter arguments, and returns a filtered queryset.
::: report.templatetags.report.filter_db_model
options:
show_docstring_description: false
show_source: False
#### Example
Generate a list of all active customers:
```html
{% raw %}
{% load report %}
{% filter_db_model company.company is_customer=True active=True as active_customers %}
<ul>
{% for customer in active_customers %}
<li>{{ customer.name }}</li>
{% endfor %}
</ul>
{% endraw %}
```
### Advanced Database Queries
More advanced database filtering should be achieved using a [report plugin](../extend/plugins/report.md), and adding custom context data to the report template.
## Number Formatting
### format_number
+1 -1
View File
@@ -63,7 +63,7 @@ Next you can start configuring the connection. Either use the config file or set
| `ldap.user_dn_template` | `INVENTREE_LDAP_USER_DN_TEMPLATE` | use direct bind as auth user, `ldap.bind_dn` and `ldap.bin_password` is not necessary then, e.g. `uid=%(user)s,dc=example,dc=org` |
| `ldap.global_options` | `INVENTREE_LDAP_GLOBAL_OPTIONS` | set advanced options as dict, e.g. TLS settings. For a list of all available options, see [python-ldap docs](https://www.python-ldap.org/en/latest/reference/ldap.html#ldap-options). (keys and values starting with OPT_ get automatically converted to `python-ldap` keys) |
| `ldap.search_filter_str`| `INVENTREE_LDAP_SEARCH_FILTER_STR` | LDAP search filter str, default: `uid=%(user)s` |
| `ldap.user_attr_map` | `INVENTREE_LDAP_USER_ATTR_MAP` | LDAP <-> Inventree user attribute map, can be json if used as env, in yml directly specify the object. default: `{"first_name": "givenName", "last_name": "sn", "email": "mail"}` |
| `ldap.user_attr_map` | `INVENTREE_LDAP_USER_ATTR_MAP` | LDAP <-> InvenTree user attribute map, can be json if used as env, in yml directly specify the object. default: `{"first_name": "givenName", "last_name": "sn", "email": "mail"}` |
| `ldap.always_update_user` | `INVENTREE_LDAP_ALWAYS_UPDATE_USER` | Always update the user on each login, default: `true` |
| `ldap.cache_timeout` | `INVENTREE_LDAP_CACHE_TIMEOUT` | cache timeout to reduce traffic with LDAP server, default: `3600` (1h) |
| `ldap.group_search` | `INVENTREE_LDAP_GROUP_SEARCH` | Base LDAP DN for group searching; required to enable group features |
+1 -1
View File
@@ -31,7 +31,7 @@ The installer creates the following directories:
| --- | --- |
| `/etc/inventree/` | Configuration files |
| `/opt/inventree/` | InvenTree application files |
| `/opt/inventree/data/` | Inventree data files |
| `/opt/inventree/data/` | InvenTree data files |
#### Performed steps
+1 -1
View File
@@ -68,7 +68,7 @@ python3 -m venv env
The virtual environment needs to be activated to ensure the correct python binaries and libraries are used. The InvenTree instructions assume that the virtual environment is always correctly activated.
To configure Inventree inside a virtual environment, ``cd`` into the inventree base directory and run the following command:
To configure InvenTree inside a virtual environment, ``cd`` into the inventree base directory and run the following command:
```
source env/bin/activate
+1 -1
View File
@@ -100,4 +100,4 @@ InvenTree uses the [Redis](https://redis.io/) cache server to manage cache data.
To optimize and configure your redis deployment follow the [official docker guide](https://redis.io/docs/getting-started/install-stack/docker/#configuration).
!!! tip "Enable Cache"
While a redis container is provided in the default configuration, by default it is not enabled in the Inventree server. You can enable redis cache support by following the [caching configuration guide](./config.md#caching)
While a redis container is provided in the default configuration, by default it is not enabled in the InvenTree server. You can enable redis cache support by following the [caching configuration guide](./config.md#caching)
+6 -6
View File
@@ -311,17 +311,17 @@ mkdocs-git-revision-date-localized-plugin==1.3.0 \
--hash=sha256:439e2f14582204050a664c258861c325064d97cdc848c541e48bb034a6c4d0cb \
--hash=sha256:c99377ee119372d57a9e47cff4e68f04cce634a74831c06bc89b33e456e840a1
# via -r docs/requirements.in
mkdocs-include-markdown-plugin==7.0.1 \
--hash=sha256:4abd341cb1c5eac60ddd1a21540fdff714f1acc99e3b26f37641db60cd175a8d \
--hash=sha256:d619c206109dab4bab281e2d29b645838d55b0576c761b1fbb17e6bff1170206
mkdocs-include-markdown-plugin==7.1.1 \
--hash=sha256:046a452dea2796e93f1385a1db106209a18bb9417162063ffe0a432a97c9b837 \
--hash=sha256:3ca17da4d5d77cfa5f4da564e65dc74ee2aa6a7368119db23d650fb24d95fce9
# via -r docs/requirements.in
mkdocs-macros-plugin==1.3.7 \
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
# via -r docs/requirements.in
mkdocs-material==9.5.44 \
--hash=sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca \
--hash=sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0
mkdocs-material==9.5.46 \
--hash=sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83 \
--hash=sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
+17 -1
View File
@@ -1,13 +1,29 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 283
INVENTREE_API_VERSION = 287
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v287 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
- Adds ability to set stock status when returning items from a customer
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
- Adds "SelectionList" and "SelectionListEntry" API endpoints
v285 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559
- Adds better description for registration endpoints
v284 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
- Adds new date filters to the StockItem API
- Adds new date filters to the BuildOrder API
- Adds new date filters to the SalesOrder API
- Adds new date filters to the PurchaseOrder API
- Adds new date filters to the ReturnOrder API
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
- Adds "note" field to the PartRelated API endpoint
+17 -22
View File
@@ -23,10 +23,8 @@ from django.utils.translation import gettext_lazy as _
import bleach
import pytz
import regex
from bleach import clean
from djmoney.money import Money
from PIL import Image
from common.currency import currency_code_default
@@ -140,6 +138,8 @@ def getStaticUrl(filename):
def TestIfImage(img):
"""Test if an image file is indeed an image."""
from PIL import Image
try:
Image.open(img).verify()
return True
@@ -781,6 +781,8 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
"""
value = str(value).strip()
cleaned = clean(value, strip=True, tags=[], attributes=[])
# Add escaped characters back in
@@ -792,39 +794,32 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
# If the length changed, it means that HTML tags were removed!
if len(cleaned) != len(value) and raise_error:
field = field_name or 'non_field_errors'
raise ValidationError({field: [_('Remove HTML tags from this value')]})
return cleaned
def remove_non_printable_characters(
value: str, remove_newline=True, remove_ascii=True, remove_unicode=True
):
def remove_non_printable_characters(value: str, remove_newline=True) -> str:
"""Remove non-printable / control characters from the provided string."""
cleaned = value
if remove_ascii:
# Remove ASCII control characters
# Note that we do not sub out 0x0A (\n) here, it is done separately below
cleaned = regex.sub('[\x00-\x09]+', '', cleaned)
cleaned = regex.sub('[\x0b-\x1f\x7f]+', '', cleaned)
# Remove ASCII control characters
# Note that we do not sub out 0x0A (\n) here, it is done separately below
regex = re.compile(r'[\u0000-\u0009\u000B-\u001F\u007F-\u009F]')
cleaned = regex.sub('', cleaned)
# Remove Unicode control characters
regex = re.compile(r'[\u200E\u200F\u202A-\u202E]')
cleaned = regex.sub('', cleaned)
if remove_newline:
cleaned = regex.sub('[\x0a]+', '', cleaned)
if remove_unicode:
# Remove Unicode control characters
if remove_newline:
cleaned = regex.sub(r'[^\P{C}]+', '', cleaned)
else:
# Use 'negative-lookahead' to exclude newline character
cleaned = regex.sub('(?![\x0a])[^\\P{C}]+', '', cleaned)
regex = re.compile(r'[\x0A]')
cleaned = regex.sub('', cleaned)
return cleaned
def clean_markdown(value: str):
def clean_markdown(value: str) -> str:
"""Clean a markdown string.
This function will remove javascript and other potentially harmful content from the markdown string.
@@ -883,7 +878,7 @@ def clean_markdown(value: str):
return value
def hash_barcode(barcode_data):
def hash_barcode(barcode_data: str) -> str:
"""Calculate a 'unique' hash for a barcode string.
This hash is used for comparison / lookup.
@@ -99,7 +99,7 @@ class ClassProviderMixin:
@classmethod
def get_is_builtin(cls):
"""Is this Class build in the Inventree source code?"""
"""Is this Class build in the InvenTree source code?"""
try:
Path(cls.get_provider_file()).relative_to(settings.BASE_DIR)
return True
@@ -1,5 +1,6 @@
"""Custom management command to prerender files."""
import logging
import os
from django.conf import settings
@@ -9,6 +10,8 @@ from django.template.loader import render_to_string
from django.utils.module_loading import import_string
from django.utils.translation import override as lang_over
logger = logging.getLogger('inventree')
def render_file(file_name, source, target, locales, ctx):
"""Renders a file into all provided locales."""
@@ -31,6 +34,10 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
"""Django command to prerender files."""
if not settings.ENABLE_CLASSIC_FRONTEND:
logger.info('Classic frontend is disabled. Skipping prerendering.')
return
# static directories
LC_DIR = settings.LOCALE_PATHS[0]
SOURCE_DIR = settings.STATICFILES_I18_SRC
+5 -3
View File
@@ -57,7 +57,7 @@ class CleanMixin:
Ref: https://github.com/mozilla/bleach/issues/192
"""
cleaned = strip_html_tags(data, field_name=field)
cleaned = data
# By default, newline characters are removed
remove_newline = True
@@ -66,13 +66,13 @@ class CleanMixin:
try:
if hasattr(self, 'serializer_class'):
model = self.serializer_class.Meta.model
field = model._meta.get_field(field)
field_base = model._meta.get_field(field)
# The following field types allow newline characters
allow_newline = [(InvenTreeNotesField, True)]
for field_type in allow_newline:
if issubclass(type(field), field_type[0]):
if issubclass(type(field_base), field_type[0]):
remove_newline = False
is_markdown = field_type[1]
break
@@ -86,6 +86,8 @@ class CleanMixin:
cleaned, remove_newline=remove_newline
)
cleaned = strip_html_tags(cleaned, field_name=field)
if is_markdown:
cleaned = clean_markdown(cleaned)
@@ -6,6 +6,7 @@ from importlib import import_module
from django.conf import settings
from django.urls import NoReverseMatch, include, path, reverse
import allauth.socialaccount.providers.openid_connect.views as oidc_views
from allauth.account.models import EmailAddress
from allauth.socialaccount import providers
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
@@ -44,7 +45,7 @@ class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
return super().dispatch(request, *args, **kwargs)
def handle_oauth2(adapter: OAuth2Adapter):
def handle_oauth2(adapter: OAuth2Adapter, provider=None):
"""Define urls for oauth2 endpoints."""
return [
path(
@@ -60,6 +61,22 @@ def handle_oauth2(adapter: OAuth2Adapter):
]
def handle_oidc(provider):
"""Define urls for oidc endpoints."""
return [
path(
'login/',
lambda x: oidc_views.login(x, provider.id),
name=f'{provider.id}_api_login',
),
path(
'connect/',
lambda x: oidc_views.callback(x, provider.id),
name=f'{provider.id}_api_connect',
),
]
legacy = {
'twitter': 'twitter_oauth2',
'bitbucket': 'bitbucket_oauth2',
@@ -70,47 +87,53 @@ legacy = {
# Collect urls for all loaded providers
social_auth_urlpatterns = []
def get_provider_urls() -> list:
"""Collect urls for all loaded providers.
provider_urlpatterns = []
Returns:
list: List of urls for all loaded providers.
"""
auth_provider_routes = []
for name, provider in providers.registry.provider_map.items():
try:
prov_mod = import_module(provider.get_package() + '.views')
except ImportError:
logger.exception('Could not import authentication provider %s', name)
continue
for name, provider in providers.registry.provider_map.items():
try:
prov_mod = import_module(provider.get_package() + '.views')
except ImportError:
logger.exception('Could not import authentication provider %s', name)
continue
# Try to extract the adapter class
adapters = [
cls
for cls in prov_mod.__dict__.values()
if isinstance(cls, type)
and cls != OAuth2Adapter
and issubclass(cls, OAuth2Adapter)
]
# Try to extract the adapter class
adapters = [
cls
for cls in prov_mod.__dict__.values()
if isinstance(cls, type)
and cls != OAuth2Adapter
and issubclass(cls, OAuth2Adapter)
]
# Get urls
urls = []
if len(adapters) == 1:
urls = handle_oauth2(adapter=adapters[0])
elif provider.id in legacy:
logger.warning(
'`%s` is not supported on platform UI. Use `%s` instead.',
provider.id,
legacy[provider.id],
)
continue
else:
logger.error(
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
provider.id,
)
continue
provider_urlpatterns += [path(f'{provider.id}/', include(urls))]
# Get urls
urls = []
if len(adapters) == 1:
if provider.id == 'openid_connect':
urls = handle_oidc(provider)
else:
urls = handle_oauth2(adapter=adapters[0], provider=provider)
elif provider.id in legacy:
logger.warning(
'`%s` is not supported on platform UI. Use `%s` instead.',
provider.id,
legacy[provider.id],
)
continue
else:
logger.error(
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
provider.id,
)
continue
auth_provider_routes += [path(f'{provider.id}/', include(urls))]
social_auth_urlpatterns += provider_urlpatterns
return auth_provider_routes
class SocialProviderListResponseSerializer(serializers.Serializer):
+67 -65
View File
@@ -54,7 +54,7 @@ from .social_auth_urls import (
EmailRemoveView,
EmailVerifyView,
SocialProviderListView,
social_auth_urlpatterns,
get_provider_urls,
)
from .views import (
AboutView,
@@ -78,6 +78,71 @@ from .views import (
admin.site.site_header = 'InvenTree Admin'
settings_urls = [
path('i18n/', include('django.conf.urls.i18n')),
path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'),
# Catch any other urls
path(
'',
SettingsView.as_view(template_name='InvenTree/settings/settings.html'),
name='settings',
),
]
notifications_urls = [
# Catch any other urls
path('', NotificationsView.as_view(), name='notifications')
]
classic_frontendpatterns = [
# Apps
#
path('build/', include(build_urls)),
path('common/', include(common_urls)),
path('company/', include(company_urls)),
path('order/', include(order_urls)),
path('manufacturer-part/', include(manufacturer_part_urls)),
path('part/', include(part_urls)),
path('stock/', include(stock_urls)),
path('supplier-part/', include(supplier_part_urls)),
path('edit-user/', EditUserView.as_view(), name='edit-user'),
path('set-password/', SetPasswordView.as_view(), name='set-password'),
path('index/', IndexView.as_view(), name='index'),
path('notifications/', include(notifications_urls)),
path('search/', SearchView.as_view(), name='search'),
path('settings/', include(settings_urls)),
path('about/', AboutView.as_view(), name='about'),
path('stats/', DatabaseStatsView.as_view(), name='stats'),
# DB user sessions
path(
'accounts/sessions/other/delete/',
view=CustomSessionDeleteOtherView.as_view(),
name='session_delete_other',
),
re_path(
r'^accounts/sessions/(?P<pk>\w+)/delete/$',
view=CustomSessionDeleteView.as_view(),
name='session_delete',
),
# Single Sign On / allauth
# overrides of urlpatterns
path('accounts/email/', CustomEmailView.as_view(), name='account_email'),
path(
'accounts/social/connections/',
CustomConnectionsView.as_view(),
name='socialaccount_connections',
),
re_path(
r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
CustomPasswordResetFromKeyView.as_view(),
name='account_reset_password_from_key',
),
# Override login page
path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
path('accounts/', include('allauth_2fa.urls')), # MFA support
path('accounts/', include('allauth.urls')), # included urlpatterns
]
apipatterns = [
# Global search
@@ -167,7 +232,7 @@ apipatterns = [
path('', EmailListView.as_view(), name='email-list'),
]),
),
path('social/', include(social_auth_urlpatterns)),
path('social/', include(get_provider_urls())),
path(
'social/', SocialAccountListView.as_view(), name='social_account_list'
),
@@ -197,21 +262,6 @@ apipatterns = [
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
]
settings_urls = [
path('i18n/', include('django.conf.urls.i18n')),
path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'),
# Catch any other urls
path(
'',
SettingsView.as_view(template_name='InvenTree/settings/settings.html'),
name='settings',
),
]
notifications_urls = [
# Catch any other urls
path('', NotificationsView.as_view(), name='notifications')
]
# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
@@ -400,54 +450,6 @@ if settings.ENABLE_CLASSIC_FRONTEND:
re_path(r'^js/i18n/', include(translated_javascript_urls)),
]
classic_frontendpatterns = [
# Apps
#
path('build/', include(build_urls)),
path('common/', include(common_urls)),
path('company/', include(company_urls)),
path('order/', include(order_urls)),
path('manufacturer-part/', include(manufacturer_part_urls)),
path('part/', include(part_urls)),
path('stock/', include(stock_urls)),
path('supplier-part/', include(supplier_part_urls)),
path('edit-user/', EditUserView.as_view(), name='edit-user'),
path('set-password/', SetPasswordView.as_view(), name='set-password'),
path('index/', IndexView.as_view(), name='index'),
path('notifications/', include(notifications_urls)),
path('search/', SearchView.as_view(), name='search'),
path('settings/', include(settings_urls)),
path('about/', AboutView.as_view(), name='about'),
path('stats/', DatabaseStatsView.as_view(), name='stats'),
# DB user sessions
path(
'accounts/sessions/other/delete/',
view=CustomSessionDeleteOtherView.as_view(),
name='session_delete_other',
),
re_path(
r'^accounts/sessions/(?P<pk>\w+)/delete/$',
view=CustomSessionDeleteView.as_view(),
name='session_delete',
),
# Single Sign On / allauth
# overrides of urlpatterns
path('accounts/email/', CustomEmailView.as_view(), name='account_email'),
path(
'accounts/social/connections/',
CustomConnectionsView.as_view(),
name='socialaccount_connections',
),
re_path(
r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
CustomPasswordResetFromKeyView.as_view(),
name='account_reset_password_from_key',
),
# Override login page
path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
path('accounts/', include('allauth_2fa.urls')), # MFA support
path('accounts/', include('allauth.urls')), # included urlpatterns
]
urlpatterns = []
+31 -1
View File
@@ -23,7 +23,7 @@ import build.serializers
from build.models import Build, BuildLine, BuildItem
import part.models
from users.models import Owner
from InvenTree.filters import SEARCH_ORDER_FILTER_ALIAS
from InvenTree.filters import InvenTreeDateFilter, SEARCH_ORDER_FILTER_ALIAS
class BuildFilter(rest_filters.FilterSet):
@@ -179,6 +179,36 @@ class BuildFilter(rest_filters.FilterSet):
return queryset.exclude(project_code=None)
return queryset.filter(project_code=None)
created_before = InvenTreeDateFilter(
label=_('Created before'),
field_name='creation_date', lookup_expr='lt'\
)
created_after = InvenTreeDateFilter(
label=_('Created after'),
field_name='creation_date', lookup_expr='gt'
)
target_date_before = InvenTreeDateFilter(
label=_('Target date before'),
field_name='target_date', lookup_expr='lt'
)
target_date_after = InvenTreeDateFilter(
label=_('Target date after'),
field_name='target_date', lookup_expr='gt'
)
completed_before = InvenTreeDateFilter(
label=_('Completed before'),
field_name='completion_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed after'),
field_name='completion_date', lookup_expr='gt'
)
class BuildMixin:
"""Mixin class for Build API endpoints."""
+2
View File
@@ -871,6 +871,7 @@ class Build(
part=self.part,
build=self,
batch=batch,
location=location,
is_building=True
)
@@ -1749,6 +1750,7 @@ class BuildItem(InvenTree.models.InvenTreeMetadataModel):
else:
# Mark the item as "consumed" by the build order
item.consumed_by = self.build
item.location = None
item.save(add_note=False)
item.add_tracking_entry(
+78
View File
@@ -808,6 +808,82 @@ class IconList(ListAPI):
return get_icon_packs().values()
class SelectionListList(ListCreateAPI):
"""List view for SelectionList objects."""
queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
"""Override the queryset method to include entry count."""
return self.serializer_class.annotate_queryset(super().get_queryset())
class SelectionListDetail(RetrieveUpdateDestroyAPI):
"""Detail view for a SelectionList object."""
queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated]
class EntryMixin:
"""Mixin for SelectionEntry views."""
queryset = common.models.SelectionListEntry.objects.all()
serializer_class = common.serializers.SelectionEntrySerializer
permission_classes = [permissions.IsAuthenticated]
lookup_url_kwarg = 'entrypk'
def get_queryset(self):
"""Prefetch related fields."""
pk = self.kwargs.get('pk', None)
queryset = super().get_queryset().filter(list=pk)
queryset = queryset.prefetch_related('list')
return queryset
class SelectionEntryList(EntryMixin, ListCreateAPI):
"""List view for SelectionEntry objects."""
class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
"""Detail view for a SelectionEntry object."""
selection_urls = [
path(
'<int:pk>/',
include([
# Entries
path(
'entry/',
include([
path(
'<int:entrypk>/',
include([
path(
'',
SelectionEntryDetail.as_view(),
name='api-selectionlistentry-detail',
)
]),
),
path(
'',
SelectionEntryList.as_view(),
name='api-selectionlistentry-list',
),
]),
),
path('', SelectionListDetail.as_view(), name='api-selectionlist-detail'),
]),
),
path('', SelectionListList.as_view(), name='api-selectionlist-list'),
]
# API URL patterns
settings_api_urls = [
# User settings
path(
@@ -1016,6 +1092,8 @@ common_api_urls = [
),
# Icons
path('icons/', IconList.as_view(), name='api-icon-list'),
# Selection lists
path('selection/', include(selection_urls)),
]
admin_api_urls = [
@@ -0,0 +1,191 @@
# Generated by Django 4.2.16 on 2024-11-24 12:41
import django.db.models.deletion
from django.db import migrations, models
import InvenTree.models
class Migration(migrations.Migration):
dependencies = [
('plugin', '0009_alter_pluginconfig_key'),
('common', '0031_auto_20241026_0024'),
]
operations = [
migrations.CreateModel(
name='SelectionList',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'metadata',
models.JSONField(
blank=True,
help_text='JSON metadata field, for use by external plugins',
null=True,
verbose_name='Plugin Metadata',
),
),
(
'name',
models.CharField(
help_text='Name of the selection list',
max_length=100,
unique=True,
verbose_name='Name',
),
),
(
'description',
models.CharField(
blank=True,
help_text='Description of the selection list',
max_length=250,
verbose_name='Description',
),
),
(
'locked',
models.BooleanField(
default=False,
help_text='Is this selection list locked?',
verbose_name='Locked',
),
),
(
'active',
models.BooleanField(
default=True,
help_text='Can this selection list be used?',
verbose_name='Active',
),
),
(
'source_string',
models.CharField(
blank=True,
help_text='Optional string identifying the source used for this list',
max_length=1000,
verbose_name='Source String',
),
),
(
'created',
models.DateTimeField(
auto_now_add=True,
help_text='Date and time that the selection list was created',
verbose_name='Created',
),
),
(
'last_updated',
models.DateTimeField(
auto_now=True,
help_text='Date and time that the selection list was last updated',
verbose_name='Last Updated',
),
),
],
options={
'verbose_name': 'Selection List',
'verbose_name_plural': 'Selection Lists',
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='SelectionListEntry',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'value',
models.CharField(
help_text='Value of the selection list entry',
max_length=255,
verbose_name='Value',
),
),
(
'label',
models.CharField(
help_text='Label for the selection list entry',
max_length=255,
verbose_name='Label',
),
),
(
'description',
models.CharField(
blank=True,
help_text='Description of the selection list entry',
max_length=250,
verbose_name='Description',
),
),
(
'active',
models.BooleanField(
default=True,
help_text='Is this selection list entry active?',
verbose_name='Active',
),
),
(
'list',
models.ForeignKey(
blank=True,
help_text='Selection list to which this entry belongs',
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='entries',
to='common.selectionlist',
verbose_name='Selection List',
),
),
],
options={
'verbose_name': 'Selection List Entry',
'verbose_name_plural': 'Selection List Entries',
'unique_together': {('list', 'value')},
},
),
migrations.AddField(
model_name='selectionlist',
name='default',
field=models.ForeignKey(
blank=True,
help_text='Default entry for this selection list',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='common.selectionlistentry',
verbose_name='Default Entry',
),
),
migrations.AddField(
model_name='selectionlist',
name='source_plugin',
field=models.ForeignKey(
blank=True,
help_text='Plugin which provides the selection list',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='plugin.pluginconfig',
verbose_name='Source Plugin',
),
),
]
+163
View File
@@ -3542,6 +3542,169 @@ class InvenTreeCustomUserStateModel(models.Model):
return cls
class SelectionList(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
"""Class which represents a list of selectable items for parameters.
A lists selection options can be either manually defined, or sourced from a plugin.
Attributes:
name: The name of the selection list
description: A description of the selection list
locked: Is this selection list locked (i.e. cannot be modified)?
active: Is this selection list active?
source_plugin: The plugin which provides the selection list
source_string: The string representation of the selection list
default: The default value for the selection list
created: The date/time that the selection list was created
last_updated: The date/time that the selection list was last updated
"""
class Meta:
"""Meta options for SelectionList."""
verbose_name = _('Selection List')
verbose_name_plural = _('Selection Lists')
name = models.CharField(
max_length=100,
verbose_name=_('Name'),
help_text=_('Name of the selection list'),
unique=True,
)
description = models.CharField(
max_length=250,
verbose_name=_('Description'),
help_text=_('Description of the selection list'),
blank=True,
)
locked = models.BooleanField(
default=False,
verbose_name=_('Locked'),
help_text=_('Is this selection list locked?'),
)
active = models.BooleanField(
default=True,
verbose_name=_('Active'),
help_text=_('Can this selection list be used?'),
)
source_plugin = models.ForeignKey(
'plugin.PluginConfig',
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_('Source Plugin'),
help_text=_('Plugin which provides the selection list'),
)
source_string = models.CharField(
max_length=1000,
verbose_name=_('Source String'),
help_text=_('Optional string identifying the source used for this list'),
blank=True,
)
default = models.ForeignKey(
'SelectionListEntry',
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_('Default Entry'),
help_text=_('Default entry for this selection list'),
)
created = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Created'),
help_text=_('Date and time that the selection list was created'),
)
last_updated = models.DateTimeField(
auto_now=True,
verbose_name=_('Last Updated'),
help_text=_('Date and time that the selection list was last updated'),
)
def __str__(self):
"""Return string representation of the selection list."""
if not self.active:
return f'{self.name} (Inactive)'
return self.name
@staticmethod
def get_api_url():
"""Return the API URL associated with the SelectionList model."""
return reverse('api-selectionlist-list')
def get_choices(self):
"""Return the choices for the selection list."""
choices = self.entries.filter(active=True)
return [c.value for c in choices]
class SelectionListEntry(models.Model):
"""Class which represents a single entry in a SelectionList.
Attributes:
list: The SelectionList to which this entry belongs
value: The value of the selection list entry
label: The label for the selection list entry
description: A description of the selection list entry
active: Is this selection list entry active?
"""
class Meta:
"""Meta options for SelectionListEntry."""
verbose_name = _('Selection List Entry')
verbose_name_plural = _('Selection List Entries')
unique_together = [['list', 'value']]
list = models.ForeignKey(
SelectionList,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='entries',
verbose_name=_('Selection List'),
help_text=_('Selection list to which this entry belongs'),
)
value = models.CharField(
max_length=255,
verbose_name=_('Value'),
help_text=_('Value of the selection list entry'),
)
label = models.CharField(
max_length=255,
verbose_name=_('Label'),
help_text=_('Label for the selection list entry'),
)
description = models.CharField(
max_length=250,
verbose_name=_('Description'),
help_text=_('Description of the selection list entry'),
blank=True,
)
active = models.BooleanField(
default=True,
verbose_name=_('Active'),
help_text=_('Is this selection list entry active?'),
)
def __str__(self):
"""Return string representation of the selection list entry."""
if not self.active:
return f'{self.label} (Inactive)'
return self.label
class BarcodeScanResult(InvenTree.models.InvenTreeModel):
"""Model for storing barcode scans results."""
+121 -1
View File
@@ -1,7 +1,7 @@
"""JSON serializers for common components."""
from django.contrib.contenttypes.models import ContentType
from django.db.models import OuterRef, Subquery
from django.db.models import Count, OuterRef, Subquery
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -639,3 +639,123 @@ class IconPackageSerializer(serializers.Serializer):
prefix = serializers.CharField()
fonts = serializers.DictField(child=serializers.CharField())
icons = serializers.DictField(child=IconSerializer())
class SelectionEntrySerializer(InvenTreeModelSerializer):
"""Serializer for a selection entry."""
class Meta:
"""Meta options for SelectionEntrySerializer."""
model = common_models.SelectionListEntry
fields = '__all__'
def validate(self, attrs):
"""Ensure that the selection list is not locked."""
ret = super().validate(attrs)
if self.instance and self.instance.list.locked:
raise serializers.ValidationError({'list': _('Selection list is locked')})
return ret
class SelectionListSerializer(InvenTreeModelSerializer):
"""Serializer for a selection list."""
_choices_validated: dict = {}
class Meta:
"""Meta options for SelectionListSerializer."""
model = common_models.SelectionList
fields = [
'pk',
'name',
'description',
'active',
'locked',
'source_plugin',
'source_string',
'default',
'created',
'last_updated',
'choices',
'entry_count',
]
default = SelectionEntrySerializer(read_only=True, many=False)
choices = SelectionEntrySerializer(source='entries', many=True, required=False)
entry_count = serializers.IntegerField(read_only=True)
@staticmethod
def annotate_queryset(queryset):
"""Add count of entries for each selection list."""
return queryset.annotate(entry_count=Count('entries'))
def is_valid(self, *, raise_exception=False):
"""Validate the selection list. Choices are validated separately."""
choices = (
self.initial_data.pop('choices')
if self.initial_data.get('choices') is not None
else []
)
# Validate the choices
_choices_validated = []
db_entries = (
{a.id: a for a in self.instance.entries.all()} if self.instance else {}
)
for choice in choices:
current_inst = db_entries.get(choice.get('id'))
serializer = SelectionEntrySerializer(
instance=current_inst,
data={'list': current_inst.list.pk if current_inst else None, **choice},
)
serializer.is_valid(raise_exception=raise_exception)
_choices_validated.append({
**serializer.validated_data,
'id': choice.get('id'),
})
self._choices_validated = _choices_validated
return super().is_valid(raise_exception=raise_exception)
def create(self, validated_data):
"""Create a new selection list. Save the choices separately."""
list_entry = common_models.SelectionList.objects.create(**validated_data)
for choice_data in self._choices_validated:
common_models.SelectionListEntry.objects.create(**{
**choice_data,
'list': list_entry,
})
return list_entry
def update(self, instance, validated_data):
"""Update an existing selection list. Save the choices separately."""
inst_mapping = {inst.id: inst for inst in instance.entries.all()}
exsising_ids = {a.get('id') for a in self._choices_validated}
# Perform creations and updates.
ret = []
for data in self._choices_validated:
list_inst = data.get('list', None)
inst = inst_mapping.get(data.get('id'))
if inst is None:
if list_inst is None:
data['list'] = instance
ret.append(SelectionEntrySerializer().create(data))
else:
ret.append(SelectionEntrySerializer().update(inst, data))
# Perform deletions.
for entry_id in inst_mapping.keys() - exsising_ids:
inst_mapping[entry_id].delete()
return super().update(instance, validated_data)
def validate(self, attrs):
"""Ensure that the selection list is not locked."""
ret = super().validate(attrs)
if self.instance and self.instance.locked:
raise serializers.ValidationError({'locked': _('Selection list is locked')})
return ret
+162 -3
View File
@@ -29,7 +29,7 @@ from InvenTree.unit_test import (
InvenTreeTestCase,
PluginMixin,
)
from part.models import Part
from part.models import Part, PartParameterTemplate
from plugin import registry
from plugin.models import NotificationUserSetting
@@ -45,6 +45,8 @@ from .models import (
NotificationEntry,
NotificationMessage,
ProjectCode,
SelectionList,
SelectionListEntry,
WebhookEndpoint,
WebhookMessage,
)
@@ -434,7 +436,7 @@ class SettingsTest(InvenTreeTestCase):
try:
InvenTreeSetting.set_setting(key, value, change_user=self.user)
except Exception as exc:
except Exception as exc: # pragma: no cover
print(f"test_defaults: Failed to set default value for setting '{key}'")
raise exc
@@ -1252,7 +1254,9 @@ class CurrencyAPITests(InvenTreeAPITestCase):
# Updating via the external exchange may not work every time
for _idx in range(5):
self.post(reverse('api-currency-refresh'), expected_code=200)
self.post(
reverse('api-currency-refresh'), expected_code=200, max_query_time=30
)
# There should be some new exchange rate objects now
if Rate.objects.all().exists():
@@ -1683,6 +1687,161 @@ class CustomStatusTest(TestCase):
)
class SelectionListTest(InvenTreeAPITestCase):
"""Tests for the SelectionList and SelectionListEntry model and API endpoints."""
fixtures = ['category', 'part', 'location', 'params', 'test_templates']
def setUp(self):
"""Setup for all tests."""
super().setUp()
self.list = SelectionList.objects.create(name='Test List')
self.entry1 = SelectionListEntry.objects.create(
list=self.list,
value='test1',
label='Test Entry',
description='Test Description',
)
self.entry2 = SelectionListEntry.objects.create(
list=self.list,
value='test2',
label='Test Entry 2',
description='Test Description 2',
active=False,
)
self.list2 = SelectionList.objects.create(name='Test List 2', active=False)
# Urls
self.list_url = reverse('api-selectionlist-detail', kwargs={'pk': self.list.pk})
self.entry_url = reverse(
'api-selectionlistentry-detail',
kwargs={'entrypk': self.entry1.pk, 'pk': self.list.pk},
)
def test_api(self):
"""Test the SelectionList and SelctionListEntry API endpoints."""
url = reverse('api-selectionlist-list')
response = self.get(url, expected_code=200)
self.assertEqual(len(response.data), 2)
response = self.get(self.list_url, expected_code=200)
self.assertEqual(response.data['name'], 'Test List')
self.assertEqual(len(response.data['choices']), 2)
self.assertEqual(response.data['choices'][0]['value'], 'test1')
self.assertEqual(response.data['choices'][0]['label'], 'Test Entry')
response = self.get(self.entry_url, expected_code=200)
self.assertEqual(response.data['value'], 'test1')
self.assertEqual(response.data['label'], 'Test Entry')
self.assertEqual(response.data['description'], 'Test Description')
def test_api_update(self):
"""Test adding and editing via the SelectionList."""
# Test adding a new list via the API
response = self.post(
reverse('api-selectionlist-list'),
{
'name': 'New List',
'active': True,
'choices': [{'value': '1', 'label': 'Test Entry'}],
},
expected_code=201,
)
list_pk = response.data['pk']
self.assertEqual(response.data['name'], 'New List')
self.assertTrue(response.data['active'])
self.assertEqual(len(response.data['choices']), 1)
self.assertEqual(response.data['choices'][0]['value'], '1')
# Test editing the list choices via the API (remove and add in same call)
response = self.patch(
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
{'choices': [{'value': '2', 'label': 'New Label'}]},
expected_code=200,
)
self.assertEqual(response.data['name'], 'New List')
self.assertTrue(response.data['active'])
self.assertEqual(len(response.data['choices']), 1)
self.assertEqual(response.data['choices'][0]['value'], '2')
self.assertEqual(response.data['choices'][0]['label'], 'New Label')
entry_id = response.data['choices'][0]['id']
# Test changing an entry via list API
response = self.patch(
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
{'choices': [{'id': entry_id, 'value': '2', 'label': 'New Label Text'}]},
expected_code=200,
)
self.assertEqual(response.data['name'], 'New List')
self.assertTrue(response.data['active'])
self.assertEqual(len(response.data['choices']), 1)
self.assertEqual(response.data['choices'][0]['value'], '2')
self.assertEqual(response.data['choices'][0]['label'], 'New Label Text')
def test_api_locked(self):
"""Test editing with locked/unlocked list."""
# Lock list
self.list.locked = True
self.list.save()
response = self.patch(self.entry_url, {'label': 'New Label'}, expected_code=400)
self.assertIn('Selection list is locked', response.data['list'])
response = self.patch(self.list_url, {'name': 'New Name'}, expected_code=400)
self.assertIn('Selection list is locked', response.data['locked'])
# Unlock the list
self.list.locked = False
self.list.save()
response = self.patch(self.entry_url, {'label': 'New Label'}, expected_code=200)
self.assertEqual(response.data['label'], 'New Label')
response = self.patch(self.list_url, {'name': 'New Name'}, expected_code=200)
self.assertEqual(response.data['name'], 'New Name')
def test_model_meta(self):
"""Test model meta functions."""
# Models str
self.assertEqual(str(self.list), 'Test List')
self.assertEqual(str(self.list2), 'Test List 2 (Inactive)')
self.assertEqual(str(self.entry1), 'Test Entry')
self.assertEqual(str(self.entry2), 'Test Entry 2 (Inactive)')
# API urls
self.assertEqual(self.list.get_api_url(), '/api/selection/')
def test_parameter(self):
"""Test the SelectionList parameter."""
self.assertEqual(self.list.get_choices(), ['test1'])
self.user.is_superuser = True
self.user.save()
# Add to parameter
part = Part.objects.get(pk=1)
template = PartParameterTemplate.objects.create(
name='test_parameter', units='', selectionlist=self.list
)
rsp = self.get(
reverse('api-part-parameter-template-detail', kwargs={'pk': template.pk})
)
self.assertEqual(rsp.data['name'], 'test_parameter')
self.assertEqual(rsp.data['choices'], '')
# Add to part
url = reverse('api-part-parameter-list')
response = self.post(
url,
{'part': part.pk, 'template': template.pk, 'data': 70},
expected_code=400,
)
self.assertIn('Invalid choice for parameter value', response.data['data'])
response = self.post(
url,
{'part': part.pk, 'template': template.pk, 'data': self.entry1.value},
expected_code=201,
)
self.assertEqual(response.data['data'], self.entry1.value)
class AdminTest(AdminTestCase):
"""Tests for the admin interface integration."""
+74 -27
View File
@@ -21,7 +21,11 @@ import company.models
from generic.states.api import StatusView
from importer.mixins import DataExportViewMixin
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.filters import (
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
)
from InvenTree.helpers import str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
@@ -140,6 +144,22 @@ class OrderFilter(rest_filters.FilterSet):
queryset=Owner.objects.all(), field_name='responsible', label=_('Responsible')
)
created_before = InvenTreeDateFilter(
label=_('Created Before'), field_name='creation_date', lookup_expr='lt'
)
created_after = InvenTreeDateFilter(
label=_('Created After'), field_name='creation_date', lookup_expr='gt'
)
target_date_before = InvenTreeDateFilter(
label=_('Target Date Before'), field_name='target_date', lookup_expr='lt'
)
target_date_after = InvenTreeDateFilter(
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
)
class LineItemFilter(rest_filters.FilterSet):
"""Base class for custom API filters for order line item list(s)."""
@@ -171,6 +191,41 @@ class PurchaseOrderFilter(OrderFilter):
model = models.PurchaseOrder
fields = ['supplier']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(),
field_name='part',
label=_('Part'),
method='filter_part',
)
def filter_part(self, queryset, name, part: Part):
"""Filter by provided Part instance."""
orders = part.purchase_orders()
return queryset.filter(pk__in=[o.pk for o in orders])
supplier_part = rest_filters.ModelChoiceFilter(
queryset=company.models.SupplierPart.objects.all(),
label=_('Supplier Part'),
method='filter_supplier_part',
)
def filter_supplier_part(
self, queryset, name, supplier_part: company.models.SupplierPart
):
"""Filter by provided SupplierPart instance."""
orders = supplier_part.purchase_orders()
return queryset.filter(pk__in=[o.pk for o in orders])
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
)
class PurchaseOrderMixin:
"""Mixin class for PurchaseOrder endpoints."""
@@ -221,32 +276,6 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
params = self.request.query_params
# Attempt to filter by part
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=part)
queryset = queryset.filter(
id__in=[p.id for p in part.purchase_orders()]
)
except (Part.DoesNotExist, ValueError):
pass
# Attempt to filter by supplier part
supplier_part = params.get('supplier_part', None)
if supplier_part is not None:
try:
supplier_part = company.models.SupplierPart.objects.get(
pk=supplier_part
)
queryset = queryset.filter(
id__in=[p.id for p in supplier_part.purchase_orders()]
)
except (ValueError, company.models.SupplierPart.DoesNotExist):
pass
# Filter by 'date range'
min_date = params.get('min_date', None)
max_date = params.get('max_date', None)
@@ -276,6 +305,7 @@ class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
'reference',
'supplier__name',
'target_date',
'complete_date',
'line_items',
'status',
'responsible',
@@ -648,6 +678,14 @@ class SalesOrderFilter(OrderFilter):
# Now we have a list of matching IDs, filter the queryset
return queryset.filter(pk__in=sales_orders)
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='shipment_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='shipment_date', lookup_expr='gt'
)
class SalesOrderMixin:
"""Mixin class for SalesOrder endpoints."""
@@ -1257,6 +1295,14 @@ class ReturnOrderFilter(OrderFilter):
# Now we have a list of matching IDs, filter the queryset
return queryset.filter(pk__in=return_orders)
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
)
class ReturnOrderMixin:
"""Mixin class for ReturnOrder endpoints."""
@@ -1325,6 +1371,7 @@ class ReturnOrderList(ReturnOrderMixin, DataExportViewMixin, ListCreateAPI):
'line_items',
'status',
'target_date',
'complete_date',
'project_code',
]
+23 -13
View File
@@ -2363,14 +2363,23 @@ class ReturnOrder(TotalPriceMixin, Order):
# endregion
@transaction.atomic
def receive_line_item(self, line, location, user, note='', **kwargs):
def receive_line_item(self, line, location, user, **kwargs):
"""Receive a line item against this ReturnOrder.
Rules:
- Transfers the StockItem to the specified location
- Marks the StockItem as "quarantined"
- Adds a tracking entry to the StockItem
- Removes the 'customer' reference from the StockItem
Arguments:
line: ReturnOrderLineItem to receive
location: StockLocation to receive the item to
user: User performing the action
Keyword Arguments:
note: Additional notes to add to the tracking entry
status: Status to set the StockItem to (default: StockStatus.QUARANTINED)
Performs the following actions:
- Transfers the StockItem to the specified location
- Marks the StockItem as "quarantined"
- Adds a tracking entry to the StockItem
- Removes the 'customer' reference from the StockItem
"""
# Prevent an item from being "received" multiple times
if line.received_date is not None:
@@ -2379,17 +2388,18 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item = line.item
deltas = {
'status': StockStatus.QUARANTINED.value,
'returnorder': self.pk,
'location': location.pk,
}
status = kwargs.get('status')
if status is None:
status = StockStatus.QUARANTINED.value
deltas = {'status': status, 'returnorder': self.pk, 'location': location.pk}
if stock_item.customer:
deltas['customer'] = stock_item.customer.pk
# Update the StockItem
stock_item.status = kwargs.get('status', StockStatus.QUARANTINED.value)
stock_item.status = status
stock_item.location = location
stock_item.customer = None
stock_item.sales_order = None
@@ -2400,7 +2410,7 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item.add_tracking_entry(
StockHistoryCode.RETURNED_AGAINST_RETURN_ORDER,
user,
notes=note,
notes=kwargs.get('note', ''),
deltas=deltas,
location=location,
returnorder=self,
+28 -3
View File
@@ -26,6 +26,7 @@ import part.filters as part_filters
import part.models as part_models
import stock.models
import stock.serializers
import stock.status_codes
from common.serializers import ProjectCodeSerializer
from company.serializers import (
AddressBriefSerializer,
@@ -1923,7 +1924,7 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['item']
fields = ['item', 'status']
item = serializers.PrimaryKeyRelatedField(
queryset=order.models.ReturnOrderLineItem.objects.all(),
@@ -1933,6 +1934,15 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
label=_('Return order line item'),
)
status = serializers.ChoiceField(
choices=stock.status_codes.StockStatus.items(),
default=None,
label=_('Status'),
help_text=_('Stock item status code'),
required=False,
allow_blank=True,
)
def validate_line_item(self, item):
"""Validation for a single line item."""
if item.order != self.context['order']:
@@ -1950,7 +1960,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['items', 'location']
fields = ['items', 'location', 'note']
items = ReturnOrderLineItemReceiveSerializer(many=True)
@@ -1963,6 +1973,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
help_text=_('Select destination location for received items'),
)
note = serializers.CharField(
label=_('Note'),
help_text=_('Additional note for incoming stock items'),
required=False,
default='',
allow_blank=True,
)
def validate(self, data):
"""Perform data validation for this serializer."""
order = self.context['order']
@@ -1993,7 +2011,14 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
with transaction.atomic():
for item in items:
line_item = item['item']
order.receive_line_item(line_item, location, request.user)
order.receive_line_item(
line_item,
location,
request.user,
note=data.get('note', ''),
status=item.get('status', None),
)
@register_importer()
+2 -2
View File
@@ -1159,10 +1159,10 @@ class PartFilter(rest_filters.FilterSet):
# Created date filters
created_before = InvenTreeDateFilter(
label='Updated before', field_name='creation_date', lookup_expr='lte'
label='Updated before', field_name='creation_date', lookup_expr='lt'
)
created_after = InvenTreeDateFilter(
label='Updated after', field_name='creation_date', lookup_expr='gte'
label='Updated after', field_name='creation_date', lookup_expr='gt'
)
@@ -0,0 +1,27 @@
# Generated by Django 4.2.16 on 2024-11-24 12:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0032_selectionlist_selectionlistentry_and_more'),
('part', '0131_partrelated_note'),
]
operations = [
migrations.AddField(
model_name='partparametertemplate',
name='selectionlist',
field=models.ForeignKey(
blank=True,
help_text='Selection list for this parameter',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='parameter_templates',
to='common.selectionlist',
verbose_name='Selection List',
),
)
]
+14
View File
@@ -3724,6 +3724,7 @@ class PartParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
description: Description of the parameter [string]
checkbox: Boolean flag to indicate whether the parameter is a checkbox [bool]
choices: List of valid choices for the parameter [string]
selectionlist: SelectionList that should be used for choices [selectionlist]
"""
class Meta:
@@ -3805,6 +3806,9 @@ class PartParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
def get_choices(self):
"""Return a list of choices for this parameter template."""
if self.selectionlist:
return self.selectionlist.get_choices()
if not self.choices:
return []
@@ -3845,6 +3849,16 @@ class PartParameterTemplate(InvenTree.models.InvenTreeMetadataModel):
blank=True,
)
selectionlist = models.ForeignKey(
common.models.SelectionList,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='parameter_templates',
verbose_name=_('Selection List'),
help_text=_('Selection list for this parameter'),
)
@receiver(
post_save,
+10 -1
View File
@@ -316,7 +316,16 @@ class PartParameterTemplateSerializer(
"""Metaclass defining serializer fields."""
model = PartParameterTemplate
fields = ['pk', 'name', 'units', 'description', 'parts', 'checkbox', 'choices']
fields = [
'pk',
'name',
'units',
'description',
'parts',
'checkbox',
'choices',
'selectionlist',
]
parts = serializers.IntegerField(
read_only=True,
+9
View File
@@ -281,6 +281,15 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
'A\t part\t category\t',
'A pa\rrt cat\r\r\regory',
'A part\u200e catego\u200fry\u202e',
'A\u0000 part\u0000 category',
'A part\u0007 category',
'A\u001f part category',
'A part\u007f category',
'\u0001A part category',
'A part\u0085 category',
'A part category\u200e',
'A part cat\u200fegory',
'A\u0006 part\u007f categ\nory\r',
]
for val in values:
@@ -1,4 +1,4 @@
"""The DigiKeyPlugin is meant to integrate the DigiKey API into Inventree.
"""The DigiKeyPlugin is meant to integrate the DigiKey API into InvenTree.
This plugin can currently only match DigiKey barcodes to supplier parts.
"""
@@ -10,7 +10,7 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
"""Plugin to integrate the DigiKey API into Inventree."""
"""Plugin to integrate the DigiKey API into InvenTree."""
NAME = 'DigiKeyPlugin'
TITLE = _('Supplier Integration - DigiKey')
@@ -1,4 +1,4 @@
"""The LCSCPlugin is meant to integrate the LCSC API into Inventree.
"""The LCSCPlugin is meant to integrate the LCSC API into InvenTree.
This plugin can currently only match LCSC barcodes to supplier parts.
"""
@@ -12,7 +12,7 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
"""Plugin to integrate the LCSC API into Inventree."""
"""Plugin to integrate the LCSC API into InvenTree."""
NAME = 'LCSCPlugin'
TITLE = _('Supplier Integration - LCSC')
@@ -1,4 +1,4 @@
"""The MouserPlugin is meant to integrate the Mouser API into Inventree.
"""The MouserPlugin is meant to integrate the Mouser API into InvenTree.
This plugin currently only match Mouser barcodes to supplier parts.
"""
@@ -10,7 +10,7 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
class MouserPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
"""Plugin to integrate the Mouser API into Inventree."""
"""Plugin to integrate the Mouser API into InvenTree."""
NAME = 'MouserPlugin'
TITLE = _('Supplier Integration - Mouser')
@@ -1,4 +1,4 @@
"""The TMEPlugin is meant to integrate the TME API into Inventree.
"""The TMEPlugin is meant to integrate the TME API into InvenTree.
This plugin can currently only match TME barcodes to supplier parts.
"""
@@ -12,7 +12,7 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
"""Plugin to integrate the TME API into Inventree."""
"""Plugin to integrate the TME API into InvenTree."""
NAME = 'TMEPlugin'
TITLE = _('Supplier Integration - TME')
@@ -8,8 +8,10 @@ from decimal import Decimal
from typing import Any, Optional
from django import template
from django.apps.registry import apps
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _
@@ -29,6 +31,50 @@ register = template.Library()
logger = logging.getLogger('inventree')
@register.simple_tag()
def filter_queryset(queryset: QuerySet, **kwargs) -> QuerySet:
"""Filter a database queryset based on the provided keyword arguments.
Arguments:
queryset: The queryset to filter
Keyword Arguments:
field (any): Filter the queryset based on the provided field
Example:
{% filter_queryset companies is_supplier=True as suppliers %}
"""
return queryset.filter(**kwargs)
@register.simple_tag()
def filter_db_model(model_name: str, **kwargs) -> QuerySet:
"""Filter a database model based on the provided keyword arguments.
Arguments:
model_name: The name of the Django model - including app name (e.g. 'part.partcategory')
Keyword Arguments:
field (any): Filter the queryset based on the provided field
Example:
{% filter_db_model 'part.partcategory' is_template=True as template_parts %}
"""
app_name, model_name = model_name.split('.')
try:
model = apps.get_model(app_name, model_name)
except Exception:
return None
if model is None:
return None
queryset = model.objects.all()
return filter_queryset(queryset, **kwargs)
@register.simple_tag()
def getindex(container: list, index: int) -> Any:
"""Return the value contained at the specified index of the list.
+15 -6
View File
@@ -807,19 +807,28 @@ class StockFilter(rest_filters.FilterSet):
# Update date filters
updated_before = InvenTreeDateFilter(
label='Updated before', field_name='updated', lookup_expr='lte'
label=_('Updated before'), field_name='updated', lookup_expr='lt'
)
updated_after = InvenTreeDateFilter(
label='Updated after', field_name='updated', lookup_expr='gte'
label=_('Updated after'), field_name='updated', lookup_expr='gt'
)
stocktake_before = InvenTreeDateFilter(
label=_('Stocktake Before'), field_name='stocktake_date', lookup_expr='lt'
)
stocktake_after = InvenTreeDateFilter(
label=_('Stocktake After'), field_name='stocktake_date', lookup_expr='gt'
)
# Stock "expiry" filters
expiry_date_lte = InvenTreeDateFilter(
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lte'
expiry_before = InvenTreeDateFilter(
label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lt'
)
expiry_date_gte = InvenTreeDateFilter(
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gte'
expiry_after = InvenTreeDateFilter(
label=_('Expiry date after'), field_name='expiry_date', lookup_expr='gt'
)
stale = rest_filters.BooleanFilter(label=_('Stale'), method='filter_stale')
+22 -9
View File
@@ -1217,8 +1217,16 @@ class StockItem(
def return_from_customer(self, location, user=None, **kwargs):
"""Return stock item from customer, back into the specified location.
Arguments:
location: The location to return the stock item to
user: The user performing the action
Keyword Arguments:
notes: Additional notes to add to the tracking entry
status: Optionally set the status of the stock item
If the selected location is the same as the parent, merge stock back into the parent.
Otherwise create the stock in the new location
Otherwise create the stock in the new location.
"""
notes = kwargs.get('notes', '')
@@ -1228,6 +1236,17 @@ class StockItem(
tracking_info['customer'] = self.customer.id
tracking_info['customer_name'] = self.customer.name
# Clear out allocation information for the stock item
self.customer = None
self.belongs_to = None
self.sales_order = None
self.location = location
self.clearAllocations()
if status := kwargs.get('status'):
self.status = status
tracking_info['status'] = status
self.add_tracking_entry(
StockHistoryCode.RETURNED_FROM_CUSTOMER,
user,
@@ -1236,13 +1255,6 @@ class StockItem(
location=location,
)
# Clear out allocation information for the stock item
self.customer = None
self.belongs_to = None
self.sales_order = None
self.location = location
self.clearAllocations()
trigger_event('stockitem.returnedfromcustomer', id=self.id)
"""If new location is the same as the parent location, merge this stock back in the parent"""
@@ -1403,6 +1415,7 @@ class StockItem(
# Assign the other stock item into this one
stock_item.belongs_to = self
stock_item.consumed_by = build
stock_item.location = None
stock_item.save(add_note=False)
deltas = {'stockitem': self.pk}
@@ -2420,7 +2433,7 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
"""Hook function to be executed after StockItem object is saved/updated."""
from part import tasks as part_tasks
if created and not InvenTree.ready.isImportingData():
if not InvenTree.ready.isImportingData():
if InvenTree.ready.canAppAccessDatabase(allow_test=True):
InvenTree.tasks.offload_task(
part_tasks.notify_low_stock_if_required,
+16 -3
View File
@@ -968,7 +968,7 @@ class ReturnStockItemSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ['location', 'note']
fields = ['location', 'status', 'notes']
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
@@ -979,6 +979,15 @@ class ReturnStockItemSerializer(serializers.Serializer):
help_text=_('Destination location for returned item'),
)
status = serializers.ChoiceField(
choices=stock.status_codes.StockStatus.items(),
default=None,
label=_('Status'),
help_text=_('Stock item status code'),
required=False,
allow_blank=True,
)
notes = serializers.CharField(
label=_('Notes'),
help_text=_('Add transaction note (optional)'),
@@ -994,9 +1003,13 @@ class ReturnStockItemSerializer(serializers.Serializer):
data = self.validated_data
location = data['location']
notes = data.get('notes', '')
item.return_from_customer(location, user=request.user, notes=notes)
item.return_from_customer(
location,
user=request.user,
notes=data.get('notes', ''),
status=data.get('status', None),
)
class StockChangeStatusSerializer(serializers.Serializer):
+39
View File
@@ -951,6 +951,45 @@ class StockTest(StockTestBase):
# Final purchase price should be the weighted average
self.assertAlmostEqual(s1.purchase_price.amount, 16.875, places=3)
def test_notify_low_stock(self):
"""Test that the 'notify_low_stock' task is triggered correctly."""
FUNC_NAME = 'part.tasks.notify_low_stock_if_required'
from django_q.models import OrmQ
# Start from a blank slate
OrmQ.objects.all().delete()
def check_func() -> bool:
"""Check that the 'notify_low_stock_if_required' task has been triggered."""
found = False
for task in OrmQ.objects.all():
if task.func() == FUNC_NAME:
found = True
break
# Clear the task queue (for the next test)
OrmQ.objects.all().delete()
return found
self.assertFalse(check_func())
part = Part.objects.first()
# Create a new stock item for this part
item = StockItem.objects.create(
part=part, quantity=100, location=StockLocation.objects.first()
)
self.assertTrue(check_func())
self.assertFalse(check_func())
# Re-count the stock item
item.stocktake(99, None)
self.assertTrue(check_func())
class StockBarcodeTest(StockTestBase):
"""Run barcode tests for the stock app."""
@@ -102,6 +102,8 @@ function getModelRenderer(model) {
return renderReportTemplate;
case 'pluginconfig':
return renderPluginConfig;
case 'selectionlist':
return renderSelectionList;
default:
// Un-handled model type
console.error(`Rendering not implemented for model '${model}'`);
@@ -589,3 +591,15 @@ function renderPluginConfig(data, parameters={}) {
parameters
);
}
// Render for "SelectionList" model
function renderSelectionList(data, parameters={}) {
return renderModel(
{
text: data.name,
textSecondary: data.description,
},
parameters
);
}
@@ -1356,6 +1356,19 @@ function partParameterFields(options={}) {
display_name: choice,
});
});
} else if (response.selectionlist) {
// Selection list - get choices from the API
inventreeGet(`{% url "api-selectionlist-list" %}${response.selectionlist}/`, {}, {
async: false,
success: function(data) {
data.choices.forEach(function(item) {
choices.push({
value: item.value,
display_name: item.label,
});
});
}
});
}
}
});
@@ -1576,6 +1589,7 @@ function partParameterTemplateFields() {
icon: 'fa-th-list',
},
checkbox: {},
selectionlist: {},
};
}
@@ -421,11 +421,11 @@ function getStockTableFilters() {
title: '{% trans "Has purchase price" %}',
description: '{% trans "Show stock items which have a purchase price set" %}',
},
expiry_date_lte: {
expiry_before: {
type: 'date',
title: '{% trans "Expiry Date before" %}',
},
expiry_date_gte: {
expiry_after: {
type: 'date',
title: '{% trans "Expiry Date after" %}',
},
+2
View File
@@ -347,6 +347,8 @@ class RuleSet(models.Model):
'common_webhookendpoint',
'common_webhookmessage',
'common_inventreecustomuserstatemodel',
'common_selectionlistentry',
'common_selectionlist',
'users_owner',
# Third-party tables
'error_report_error',
+124 -122
View File
@@ -198,98 +198,98 @@ click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via pip-tools
coverage[toml]==7.6.4 \
--hash=sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376 \
--hash=sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9 \
--hash=sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111 \
--hash=sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172 \
--hash=sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491 \
--hash=sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546 \
--hash=sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2 \
--hash=sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11 \
--hash=sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08 \
--hash=sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c \
--hash=sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2 \
--hash=sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963 \
--hash=sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613 \
--hash=sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0 \
--hash=sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db \
--hash=sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf \
--hash=sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73 \
--hash=sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117 \
--hash=sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1 \
--hash=sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e \
--hash=sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522 \
--hash=sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25 \
--hash=sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc \
--hash=sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea \
--hash=sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52 \
--hash=sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a \
--hash=sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07 \
--hash=sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06 \
--hash=sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa \
--hash=sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901 \
--hash=sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b \
--hash=sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17 \
--hash=sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0 \
--hash=sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21 \
--hash=sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19 \
--hash=sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5 \
--hash=sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51 \
--hash=sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3 \
--hash=sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3 \
--hash=sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f \
--hash=sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076 \
--hash=sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a \
--hash=sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718 \
--hash=sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba \
--hash=sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e \
--hash=sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27 \
--hash=sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e \
--hash=sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09 \
--hash=sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e \
--hash=sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70 \
--hash=sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f \
--hash=sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72 \
--hash=sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a \
--hash=sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef \
--hash=sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b \
--hash=sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b \
--hash=sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f \
--hash=sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806 \
--hash=sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b \
--hash=sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1 \
--hash=sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c \
--hash=sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858
coverage[toml]==7.6.8 \
--hash=sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5 \
--hash=sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf \
--hash=sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb \
--hash=sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638 \
--hash=sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4 \
--hash=sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc \
--hash=sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed \
--hash=sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a \
--hash=sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d \
--hash=sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649 \
--hash=sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c \
--hash=sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b \
--hash=sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4 \
--hash=sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443 \
--hash=sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83 \
--hash=sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee \
--hash=sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e \
--hash=sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e \
--hash=sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3 \
--hash=sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0 \
--hash=sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb \
--hash=sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076 \
--hash=sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb \
--hash=sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787 \
--hash=sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1 \
--hash=sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e \
--hash=sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce \
--hash=sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801 \
--hash=sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764 \
--hash=sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365 \
--hash=sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf \
--hash=sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6 \
--hash=sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71 \
--hash=sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002 \
--hash=sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4 \
--hash=sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c \
--hash=sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8 \
--hash=sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4 \
--hash=sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146 \
--hash=sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc \
--hash=sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea \
--hash=sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4 \
--hash=sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad \
--hash=sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28 \
--hash=sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451 \
--hash=sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50 \
--hash=sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779 \
--hash=sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63 \
--hash=sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e \
--hash=sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc \
--hash=sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022 \
--hash=sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d \
--hash=sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94 \
--hash=sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b \
--hash=sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d \
--hash=sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331 \
--hash=sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a \
--hash=sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0 \
--hash=sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee \
--hash=sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92 \
--hash=sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a \
--hash=sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9
# via -r src/backend/requirements-dev.in
cryptography==43.0.1 \
--hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \
--hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \
--hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \
--hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \
--hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \
--hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \
--hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \
--hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \
--hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \
--hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \
--hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \
--hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \
--hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \
--hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \
--hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \
--hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \
--hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \
--hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \
--hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \
--hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \
--hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \
--hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \
--hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \
--hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \
--hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \
--hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \
--hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289
cryptography==43.0.3 \
--hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \
--hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \
--hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \
--hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \
--hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \
--hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \
--hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \
--hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \
--hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \
--hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \
--hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \
--hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \
--hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \
--hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \
--hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \
--hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \
--hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \
--hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \
--hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \
--hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \
--hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \
--hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \
--hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \
--hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \
--hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \
--hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \
--hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7
# via
# -c src/backend/requirements.txt
# pdfminer-six
@@ -322,13 +322,13 @@ filelock==3.16.1 \
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
# via virtualenv
identify==2.6.1 \
--hash=sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0 \
--hash=sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98
identify==2.6.2 \
--hash=sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3 \
--hash=sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd
# via pre-commit
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
importlib-metadata==8.5.0 \
--hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \
--hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7
# via
# -c src/backend/requirements.txt
# build
@@ -340,9 +340,9 @@ nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
packaging==24.2 \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
# via
# -c src/backend/requirements.txt
# build
@@ -350,9 +350,9 @@ pdfminer-six==20240706 \
--hash=sha256:c631a46d5da957a9ffe4460c5dce21e8431dabb615fee5f9f4400603a58d95a6 \
--hash=sha256:f4f70e74174b4b3542fcb8406a210b6e2e27cd0f0b5fd04534a8cc0d8951e38c
# via -r src/backend/requirements-dev.in
pip==24.2 \
--hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \
--hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8
pip==24.3.1 \
--hash=sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed \
--hash=sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99
# via pip-tools
pip-tools==7.4.1 \
--hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \
@@ -361,7 +361,9 @@ pip-tools==7.4.1 \
platformdirs==4.3.6 \
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
# via virtualenv
# via
# -c src/backend/requirements.txt
# virtualenv
pre-commit==4.0.1 \
--hash=sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2 \
--hash=sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878
@@ -435,22 +437,22 @@ pyyaml==6.0.2 \
# via
# -c src/backend/requirements.txt
# pre-commit
setuptools==75.1.0 \
--hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \
--hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538
setuptools==75.6.0 \
--hash=sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6 \
--hash=sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements-dev.in
# pip-tools
sqlparse==0.5.1 \
--hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \
--hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e
sqlparse==0.5.2 \
--hash=sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f \
--hash=sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e
# via
# -c src/backend/requirements.txt
# django
tomli==2.0.2 \
--hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \
--hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed
tomli==2.1.0 \
--hash=sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8 \
--hash=sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391
# via
# -c src/backend/requirements.txt
# build
@@ -463,17 +465,17 @@ typing-extensions==4.12.2 \
# -c src/backend/requirements.txt
# asgiref
# django-test-migrations
virtualenv==20.27.0 \
--hash=sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2 \
--hash=sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655
virtualenv==20.27.1 \
--hash=sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba \
--hash=sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4
# via pre-commit
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
wheel==0.45.1 \
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
--hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248
# via pip-tools
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
zipp==3.21.0 \
--hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
--hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
# via
# -c src/backend/requirements.txt
# importlib-metadata
-1
View File
@@ -48,7 +48,6 @@ python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz # Fuzzy string matching
regex # Advanced regular expressions
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -5,8 +5,8 @@ export default defineConfig({
fullyParallel: true,
timeout: 90000,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 3 : undefined,
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
/* Configure projects for major browsers */
@@ -1,4 +1,4 @@
import { Button } from '@mantine/core';
import { Button, Tooltip } from '@mantine/core';
import {
IconBrandAzure,
IconBrandBitbucket,
@@ -14,6 +14,7 @@ import {
IconLogin
} from '@tabler/icons-react';
import { t } from '@lingui/macro';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
@@ -49,14 +50,23 @@ export function SsoButton({ provider }: Readonly<{ provider: Provider }>) {
}
return (
<Button
leftSection={getBrandIcon(provider)}
radius='xl'
component='a'
onClick={login}
<Tooltip
label={
provider.login
? t`You will be redirected to the provider for further actions.`
: t`This provider is not full set up.`
}
>
{provider.display_name}{' '}
</Button>
<Button
leftSection={getBrandIcon(provider)}
radius='xl'
component='a'
onClick={login}
disabled={!provider.login}
>
{provider.display_name}
</Button>
</Tooltip>
);
}
function getBrandIcon(provider: Provider) {
@@ -219,7 +219,7 @@ export function ApiForm({
// If the user has specified initial data, that overrides default values
// But, *only* for the fields we have specified
if (props.initialData) {
Object.keys(props.initialData).map((key) => {
Object.keys(props.initialData).forEach((key) => {
if (key in defaultValuesMap) {
defaultValuesMap[key] =
props?.initialData?.[key] ?? defaultValuesMap[key];
@@ -107,12 +107,14 @@ export function AuthenticationForm() {
<TextInput
required
label={t`Username`}
aria-label='login-username'
placeholder={t`Your username`}
{...classicForm.getInputProps('username')}
/>
<PasswordInput
required
label={t`Password`}
aria-label='login-password'
placeholder={t`Your password`}
{...classicForm.getInputProps('password')}
/>
@@ -228,12 +230,14 @@ export function RegistrationForm() {
<TextInput
required
label={t`Username`}
aria-label='register-username'
placeholder={t`Your username`}
{...registrationForm.getInputProps('username')}
/>
<TextInput
required
label={t`Email`}
aria-label='register-email'
description={t`This will be used for a confirmation`}
placeholder='email@example.org'
{...registrationForm.getInputProps('email')}
@@ -241,12 +245,14 @@ export function RegistrationForm() {
<PasswordInput
required
label={t`Password`}
aria-label='register-password'
placeholder={t`Your password`}
{...registrationForm.getInputProps('password1')}
/>
<PasswordInput
required
label={t`Password repeat`}
aria-label='register-password-repeat'
placeholder={t`Repeat password`}
{...registrationForm.getInputProps('password2')}
/>
@@ -49,6 +49,7 @@ export type ApiFormAdjustFilterType = {
* @param onValueChange : Callback function to call when the field value changes
* @param adjustFilters : Callback function to adjust the filters for a related field before a query is made
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param addRow : Callback function to add a new row to a table field
* @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter
*/
export type ApiFormFieldType = {
@@ -94,6 +95,7 @@ export type ApiFormFieldType = {
adjustValue?: (value: any) => any;
onValueChange?: (value: any, record?: any) => void;
adjustFilters?: (value: ApiFormAdjustFilterType) => any;
addRow?: () => any;
headers?: string[];
depends_on?: string[];
};
@@ -43,7 +43,7 @@ export default function DateField({
let dv: Date | null = null;
if (field.value) {
dv = new Date(field.value) ?? null;
dv = new Date(field.value);
}
// Ensure that the date is valid
@@ -6,6 +6,7 @@ import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { identifierString } from '../../../functions/conversion';
import { InvenTreeIcon } from '../../../functions/icons';
import { AddItemButton } from '../../buttons/AddItemButton';
import { StandaloneField } from '../StandaloneField';
import type { ApiFormFieldType } from './ApiFormField';
@@ -109,6 +110,17 @@ export function TableField({
field.onChange(val);
};
const fieldDefinition = useMemo(() => {
return {
...definition,
modelRenderer: undefined,
onValueChange: undefined,
adjustFilters: undefined,
read_only: undefined,
addRow: undefined
};
}, [definition]);
// Extract errors associated with the current row
const rowErrors: any = useCallback(
(idx: number) => {
@@ -134,6 +146,7 @@ export function TableField({
})}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{value.length > 0 ? (
value.map((item: any, idx: number) => {
@@ -170,6 +183,26 @@ export function TableField({
</Table.Tr>
)}
</Table.Tbody>
{definition.addRow && (
<Table.Tfoot>
<Table.Tr>
<Table.Td colSpan={definition.headers?.length}>
<AddItemButton
tooltip={t`Add new row`}
onClick={() => {
if (definition.addRow === undefined) return;
const ret = definition.addRow();
if (ret) {
const val = field.value;
val.push(ret);
field.onChange(val);
}
}}
/>
</Table.Td>
</Table.Tr>
</Table.Tfoot>
)}
</Table>
);
}
@@ -70,7 +70,6 @@ export default function ImporterDrawer({
// Map from import steps to stepper steps
const currentStep = useMemo(() => {
switch (session.status) {
default:
case importSessionStatus.INITIAL:
return 0;
case importSessionStatus.MAPPING:
@@ -81,6 +80,8 @@ export default function ImporterDrawer({
return 3;
case importSessionStatus.COMPLETE:
return 4;
default:
return 0;
}
}, [session.status]);
@@ -84,7 +84,7 @@ export const getPluginTemplateEditor = (
useEffect(() => {
(async () => {
try {
await func({
func({
ref: elRef.current!,
registerHandlers: ({ getCode, setCode }) => {
setCodeRef.current = setCode;
@@ -136,7 +136,7 @@ export const getPluginTemplatePreview = (
useEffect(() => {
(async () => {
try {
await func({
func({
ref: elRef.current!,
registerHandlers: ({ updatePreview }) => {
updatePreviewRef.current = updatePreview;
@@ -34,3 +34,16 @@ export function RenderImportSession({
}): ReactNode {
return instance && <RenderInlineModel primary={instance.data_file} />;
}
export function RenderSelectionList({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
instance && (
<RenderInlineModel
primary={instance.name}
secondary={instance.description}
/>
)
);
}
@@ -20,7 +20,8 @@ import {
RenderContentType,
RenderError,
RenderImportSession,
RenderProjectCode
RenderProjectCode,
RenderSelectionList
} from './Generic';
import { ModelInformationDict } from './ModelType';
import {
@@ -94,6 +95,7 @@ const RendererLookup: EnumDictionary<
[ModelType.labeltemplate]: RenderLabelTemplate,
[ModelType.pluginconfig]: RenderPlugin,
[ModelType.contenttype]: RenderContentType,
[ModelType.selectionlist]: RenderSelectionList,
[ModelType.error]: RenderError
};
@@ -286,6 +286,12 @@ export const ModelInformationDict: ModelDict = {
api_endpoint: ApiEndpoints.content_type_list,
icon: 'list_details'
},
selectionlist: {
label: () => t`Selection List`,
label_multiple: () => t`Selection Lists`,
api_endpoint: ApiEndpoints.selectionlist_list,
icon: 'list_details'
},
error: {
label: () => t`Error`,
label_multiple: () => t`Errors`,
+1 -1
View File
@@ -38,7 +38,7 @@ export function getActions(navigate: NavigateFunction) {
{
id: 'server-info',
label: t`Server Information`,
description: t`About this Inventree instance`,
description: t`About this InvenTree instance`,
onClick: () => serverInfo(),
leftSection: <IconLink size='1.2rem' />
},
+1 -1
View File
@@ -115,7 +115,7 @@ export function AboutLinks(): MenuLinkItem[] {
{
id: 'instance',
title: t`System Information`,
description: t`About this Inventree instance`,
description: t`About this InvenTree instance`,
icon: 'info',
action: serverInfo
},
+2
View File
@@ -49,6 +49,8 @@ export enum ApiEndpoints {
owner_list = 'user/owner/',
content_type_list = 'contenttype/',
icons = 'icons/',
selectionlist_list = 'selection/',
selectionlist_detail = 'selection/:id/',
// Barcode API endpoints
barcode = 'barcode/',
+1
View File
@@ -33,5 +33,6 @@ export enum ModelType {
labeltemplate = 'labeltemplate',
pluginconfig = 'pluginconfig',
contenttype = 'contenttype',
selectionlist = 'selectionlist',
error = 'error'
}
+20 -1
View File
@@ -2,7 +2,10 @@ import { t } from '@lingui/macro';
import { IconPackages } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { api } from '../App';
import type { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState';
/**
@@ -204,7 +207,7 @@ export function usePartParameterFields({
setChoices(
_choices.map((choice) => {
return {
label: choice.trim(),
display_name: choice.trim(),
value: choice.trim()
};
})
@@ -214,6 +217,22 @@ export function usePartParameterFields({
setChoices([]);
setFieldType('string');
}
} else if (record?.selectionlist) {
api
.get(
apiUrl(ApiEndpoints.selectionlist_detail, record.selectionlist)
)
.then((res) => {
setChoices(
res.data.choices.map((item: any) => {
return {
value: item.value,
display_name: item.label
};
})
);
setFieldType('choice');
});
} else {
setChoices([]);
setFieldType('string');
+30 -2
View File
@@ -4,6 +4,7 @@ import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
import { useMemo } from 'react';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import type {
ApiFormAdjustFilterType,
ApiFormFieldSet
@@ -11,8 +12,10 @@ import type {
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { useCreateApiFormModal } from '../hooks/UseForm';
import { apiUrl } from '../states/ApiState';
import { StatusFilterOptions } from '../tables/Filter';
export function useReturnOrderFields({
duplicateOrderId
@@ -133,6 +136,17 @@ function ReturnOrderLineItemFormRow({
props: TableFieldRowProps;
record: any;
}>) {
const statusOptions = useMemo(() => {
return (
StatusFilterOptions(ModelType.stockitem)()?.map((choice) => {
return {
value: choice.value,
display_name: choice.label
};
}) ?? []
);
}, []);
return (
<>
<Table.Tr>
@@ -146,7 +160,21 @@ function ReturnOrderLineItemFormRow({
<div>{record.part_detail.name}</div>
</Flex>
</Table.Td>
<Table.Td>{record.item_detail.serial}</Table.Td>
<Table.Td># {record.item_detail.serial}</Table.Td>
<Table.Td>
<StandaloneField
fieldDefinition={{
field_type: 'choice',
label: t`Status`,
choices: statusOptions,
onValueChange: (value) => {
props.changeFn(props.idx, 'status', value);
}
}}
defaultValue={record.item_detail?.status}
error={props.rowErrors?.status?.message}
/>
</Table.Td>
<Table.Td>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
@@ -181,7 +209,7 @@ export function useReceiveReturnOrderLineItems(
/>
);
},
headers: [t`Part`, t`Serial Number`]
headers: [t`Part`, t`Stock Item`, t`Status`]
},
location: {
filters: {
+27 -10
View File
@@ -159,7 +159,7 @@ export function useStockFields({
hidden:
create ||
partInstance.trackable == false ||
(!stockItem?.quantity != undefined && stockItem?.quantity != 1)
(stockItem?.quantity != undefined && stockItem?.quantity != 1)
},
batch: {
placeholder: nextBatchCode
@@ -870,6 +870,7 @@ function stockOperationModal({
endpoint,
filters,
title,
successMessage,
modalFunc = useCreateApiFormModal
}: {
items?: object;
@@ -880,6 +881,7 @@ function stockOperationModal({
fieldGenerator: (items: any[]) => ApiFormFieldSet;
endpoint: ApiEndpoints;
title: string;
successMessage?: string;
modalFunc?: apiModalFunc;
}) {
const baseParams: any = {
@@ -932,12 +934,13 @@ function stockOperationModal({
fields: fields,
title: title,
size: '80%',
successMessage: successMessage,
onFormSuccess: () => refresh()
});
}
export type StockOperationProps = {
items?: object;
items?: any[];
pk?: number;
filters?: any;
model: ModelType.stockitem | 'location' | ModelType.part;
@@ -949,7 +952,8 @@ export function useAddStockItem(props: StockOperationProps) {
...props,
fieldGenerator: stockAddFields,
endpoint: ApiEndpoints.stock_add,
title: t`Add Stock`
title: t`Add Stock`,
successMessage: t`Stock added`
});
}
@@ -958,7 +962,8 @@ export function useRemoveStockItem(props: StockOperationProps) {
...props,
fieldGenerator: stockRemoveFields,
endpoint: ApiEndpoints.stock_remove,
title: t`Remove Stock`
title: t`Remove Stock`,
successMessage: t`Stock removed`
});
}
@@ -967,7 +972,8 @@ export function useTransferStockItem(props: StockOperationProps) {
...props,
fieldGenerator: stockTransferFields,
endpoint: ApiEndpoints.stock_transfer,
title: t`Transfer Stock`
title: t`Transfer Stock`,
successMessage: t`Stock transferred`
});
}
@@ -976,7 +982,8 @@ export function useCountStockItem(props: StockOperationProps) {
...props,
fieldGenerator: stockCountFields,
endpoint: ApiEndpoints.stock_count,
title: t`Count Stock`
title: t`Count Stock`,
successMessage: t`Stock counted`
});
}
@@ -985,7 +992,8 @@ export function useChangeStockStatus(props: StockOperationProps) {
...props,
fieldGenerator: stockChangeStatusFields,
endpoint: ApiEndpoints.stock_change_status,
title: t`Change Stock Status`
title: t`Change Stock Status`,
successMessage: t`Stock status changed`
});
}
@@ -994,16 +1002,24 @@ export function useMergeStockItem(props: StockOperationProps) {
...props,
fieldGenerator: stockMergeFields,
endpoint: ApiEndpoints.stock_merge,
title: t`Merge Stock`
title: t`Merge Stock`,
successMessage: t`Stock merged`
});
}
export function useAssignStockItem(props: StockOperationProps) {
// Filter items - only allow 'salable' items
const items = useMemo(() => {
return props.items?.filter((item) => item?.part_detail?.salable);
}, [props.items]);
return stockOperationModal({
...props,
items: items,
fieldGenerator: stockAssignFields,
endpoint: ApiEndpoints.stock_assign,
title: t`Assign Stock to Customer`
title: t`Assign Stock to Customer`,
successMessage: t`Stock assigned to customer`
});
}
@@ -1013,7 +1029,8 @@ export function useDeleteStockItem(props: StockOperationProps) {
fieldGenerator: stockDeleteFields,
endpoint: ApiEndpoints.stock_item_list,
modalFunc: useDeleteApiFormModal,
title: t`Delete Stock Items`
title: t`Delete Stock Items`,
successMessage: t`Stock deleted`
});
}
@@ -0,0 +1,117 @@
import { t } from '@lingui/macro';
import { Table } from '@mantine/core';
import { useMemo } from 'react';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import type {
ApiFormFieldSet,
ApiFormFieldType
} from '../components/forms/fields/ApiFormField';
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
function BuildAllocateLineRow({
props
}: Readonly<{
props: TableFieldRowProps;
}>) {
const valueField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'value',
required: true,
value: props.item.value,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'value', value);
}
};
}, [props]);
const labelField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'label',
required: true,
value: props.item.label,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'label', value);
}
};
}, [props]);
const descriptionField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'description',
required: true,
value: props.item.description,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'description', value);
}
};
}, [props]);
const activeField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'boolean',
name: 'active',
required: true,
value: props.item.active,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'active', value);
}
};
}, [props]);
return (
<Table.Tr key={`table-row-${props.item.pk}`}>
<Table.Td>
<StandaloneField fieldName='value' fieldDefinition={valueField} />
</Table.Td>
<Table.Td>
<StandaloneField fieldName='label' fieldDefinition={labelField} />
</Table.Td>
<Table.Td>
<StandaloneField
fieldName='description'
fieldDefinition={descriptionField}
/>
</Table.Td>
<Table.Td>
<StandaloneField fieldName='active' fieldDefinition={activeField} />
</Table.Td>
<Table.Td>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
</Table.Tr>
);
}
export function selectionListFields(): ApiFormFieldSet {
return {
name: {},
description: {},
active: {},
locked: {},
source_plugin: {},
source_string: {},
choices: {
label: t`Entries`,
description: t`List of entries to choose from`,
field_type: 'table',
value: [],
headers: [t`Value`, t`Label`, t`Description`, t`Active`],
modelRenderer: (row: TableFieldRowProps) => (
<BuildAllocateLineRow props={row} />
),
addRow: () => {
return {
value: '',
label: '',
description: '',
active: true
};
}
}
};
}
@@ -66,6 +66,8 @@ const MachineManagementPanel = Loadable(
lazy(() => import('./MachineManagementPanel'))
);
const PartParameterPanel = Loadable(lazy(() => import('./PartParameterPanel')));
const ErrorReportTable = Loadable(
lazy(() => import('../../../../tables/settings/ErrorTable'))
);
@@ -86,6 +88,10 @@ const CustomStateTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomStateTable'))
);
const CustomUnitsTable = Loadable(
lazy(() => import('../../../../tables/settings/CustomUnitsTable'))
);
const PartParameterTemplateTable = Loadable(
lazy(() => import('../../../../tables/part/PartParameterTemplateTable'))
);
@@ -169,7 +175,7 @@ export default function AdminCenter() {
name: 'part-parameters',
label: t`Part Parameters`,
icon: <IconList />,
content: <PartParameterTemplateTable />
content: <PartParameterPanel />
},
{
name: 'category-parameters',
@@ -0,0 +1,29 @@
import { t } from '@lingui/macro';
import { Accordion } from '@mantine/core';
import { StylishText } from '../../../../components/items/StylishText';
import PartParameterTemplateTable from '../../../../tables/part/PartParameterTemplateTable';
import SelectionListTable from '../../../../tables/part/SelectionListTable';
export default function PartParameterPanel() {
return (
<Accordion defaultValue='parametertemplate'>
<Accordion.Item value='parametertemplate' key='parametertemplate'>
<Accordion.Control>
<StylishText size='lg'>{t`Part Parameter Template`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<PartParameterTemplateTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='selectionlist' key='selectionlist'>
<Accordion.Control>
<StylishText size='lg'>{t`Selection Lists`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SelectionListTable />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
@@ -3,8 +3,6 @@ import { Group, Skeleton, Stack, Text } from '@mantine/core';
import { IconInfoCircle, IconPackages, IconSitemap } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ActionButton } from '../../components/buttons/ActionButton';
import AdminButton from '../../components/buttons/AdminButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import {
@@ -278,12 +276,6 @@ export default function Stock() {
() => [
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
<LocateItemButton locationId={location.pk} />,
<ActionButton
icon={<InvenTreeIcon icon='stocktake' />}
onClick={notYetImplemented}
variant='outline'
size='lg'
/>,
location.pk ? (
<BarcodeActionDropdown
model={ModelType.stocklocation}
+19 -3
View File
@@ -48,6 +48,7 @@ import { UserRoles } from '../../enums/Roles';
import {
type StockOperationProps,
useAddStockItem,
useAssignStockItem,
useCountStockItem,
useRemoveStockItem,
useStockFields,
@@ -591,7 +592,7 @@ export default function StockDetail() {
const stockActionProps: StockOperationProps = useMemo(() => {
return {
items: stockitem,
items: [stockitem],
model: ModelType.stockitem,
refresh: refreshInstance,
filters: {
@@ -604,6 +605,7 @@ export default function StockDetail() {
const addStockItem = useAddStockItem(stockActionProps);
const removeStockItem = useRemoveStockItem(stockActionProps);
const transferStockItem = useTransferStockItem(stockActionProps);
const assignToCustomer = useAssignStockItem(stockActionProps);
const serializeStockFields = useStockItemSerializeFields({
partId: stockitem.part,
@@ -640,10 +642,12 @@ export default function StockDetail() {
),
fields: {
location: {},
status: {},
notes: {}
},
initialData: {
location: stockitem.location ?? stockitem.part_detail?.default_location
location: stockitem.location ?? stockitem.part_detail?.default_location,
status: stockitem.status_custom_key ?? stockitem.status
},
successMessage: t`Item returned to stock`,
onFormSuccess: () => {
@@ -734,7 +738,7 @@ export default function StockDetail() {
{
name: t`Return`,
tooltip: t`Return from customer`,
hidden: !stockitem.sales_order,
hidden: !stockitem.customer,
icon: (
<InvenTreeIcon
icon='return_orders'
@@ -744,6 +748,17 @@ export default function StockDetail() {
onClick: () => {
stockitem.pk && returnStockItem.open();
}
},
{
name: t`Assign to Customer`,
tooltip: t`Assign to a customer`,
hidden: !!stockitem.customer,
icon: (
<InvenTreeIcon icon='customer' iconProps={{ color: 'blue' }} />
),
onClick: () => {
stockitem.pk && assignToCustomer.open();
}
}
]}
/>,
@@ -876,6 +891,7 @@ export default function StockDetail() {
{transferStockItem.modal}
{serializeStockItem.modal}
{returnStockItem.modal}
{assignToCustomer.modal}
</Stack>
</InstanceDetail>
);
+1 -1
View File
@@ -92,7 +92,7 @@ export interface SettingChoice {
}
export enum SettingTyp {
Inventree = 'inventree',
InvenTree = 'inventree',
Plugin = 'plugin',
User = 'user',
Notification = 'notification'
@@ -242,6 +242,14 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn {
});
}
export function CompletionDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({
accessor: 'completion_date',
title: t`Completion Date`,
...props
});
}
export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({
accessor: 'shipment_date',
+56 -1
View File
@@ -21,8 +21,9 @@ export type TableFilterChoice = {
* boolean: A simple true/false filter
* choice: A filter which allows selection from a list of (supplied)
* date: A filter which allows selection from a date input
* text: A filter which allows raw text input
*/
export type TableFilterType = 'boolean' | 'choice' | 'date';
export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text';
/**
* Interface for the table filter type. Provides a number of options for selecting filter value:
@@ -142,6 +143,60 @@ export function MaxDateFilter(): TableFilter {
};
}
export function CreatedBeforeFilter(): TableFilter {
return {
name: 'created_before',
label: t`Created Before`,
description: t`Show items created before this date`,
type: 'date'
};
}
export function CreatedAfterFilter(): TableFilter {
return {
name: 'created_after',
label: t`Created After`,
description: t`Show items created after this date`,
type: 'date'
};
}
export function TargetDateBeforeFilter(): TableFilter {
return {
name: 'target_date_before',
label: t`Target Date Before`,
description: t`Show items with a target date before this date`,
type: 'date'
};
}
export function TargetDateAfterFilter(): TableFilter {
return {
name: 'target_date_after',
label: t`Target Date After`,
description: t`Show items with a target date after this date`,
type: 'date'
};
}
export function CompletedBeforeFilter(): TableFilter {
return {
name: 'completed_before',
label: t`Completed Before`,
description: t`Show items completed before this date`,
type: 'date'
};
}
export function CompletedAfterFilter(): TableFilter {
return {
name: 'completed_after',
label: t`Completed After`,
description: t`Show items completed after this date`,
type: 'date'
};
}
export function HasProjectCodeFilter(): TableFilter {
return {
name: 'has_project_code',
+85 -29
View File
@@ -1,5 +1,6 @@
import { t } from '@lingui/macro';
import {
ActionIcon,
Badge,
Button,
CloseButton,
@@ -10,12 +11,14 @@ import {
Select,
Stack,
Text,
TextInput,
Tooltip
} from '@mantine/core';
import { DateInput, type DateValue } from '@mantine/dates';
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IconCheck } from '@tabler/icons-react';
import { StylishText } from '../components/items/StylishText';
import type { TableState } from '../hooks/UseTable';
import {
@@ -60,6 +63,77 @@ function FilterItem({
);
}
function FilterElement({
filterType,
valueOptions,
onValueChange
}: {
filterType: TableFilterType;
valueOptions: TableFilterChoice[];
onValueChange: (value: string | null) => void;
}) {
const setDateValue = useCallback(
(value: DateValue) => {
if (value) {
const date = value.toString();
onValueChange(dayjs(date).format('YYYY-MM-DD'));
} else {
onValueChange('');
}
},
[onValueChange]
);
const [textValue, setTextValue] = useState<string>('');
switch (filterType) {
case 'text':
return (
<TextInput
label={t`Value`}
value={textValue}
placeholder={t`Enter filter value`}
rightSection={
<ActionIcon
aria-label='apply-text-filter'
variant='transparent'
onClick={() => onValueChange(textValue)}
>
<IconCheck />
</ActionIcon>
}
onChange={(e) => setTextValue(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onValueChange(textValue);
}
}}
/>
);
case 'date':
return (
<DateInput
label={t`Value`}
placeholder={t`Select date value`}
onChange={setDateValue}
/>
);
case 'choice':
case 'boolean':
default:
return (
<Select
data={valueOptions}
searchable={filterType != 'boolean'}
label={t`Value`}
placeholder={t`Select filter value`}
onChange={(value: string | null) => onValueChange(value)}
maxDropdownHeight={800}
/>
);
}
}
function FilterAddGroup({
tableState,
availableFilters
@@ -79,6 +153,7 @@ function FilterAddGroup({
return (
availableFilters
?.filter((flt) => !activeFilterNames.includes(flt.name))
?.sort((a, b) => a.label.localeCompare(b.label))
?.map((flt) => ({
value: flt.name,
label: flt.label,
@@ -133,22 +208,13 @@ function FilterAddGroup({
};
tableState.setActiveFilters([...filters, newFilter]);
// Clear selected filter
setSelectedFilter(null);
},
[selectedFilter]
);
const setDateValue = useCallback(
(value: DateValue) => {
if (value) {
const date = value.toString();
setSelectedValue(dayjs(date).format('YYYY-MM-DD'));
} else {
setSelectedValue('');
}
},
[setSelectedValue]
);
return (
<Stack gap='xs'>
<Divider />
@@ -160,23 +226,13 @@ function FilterAddGroup({
onChange={(value: string | null) => setSelectedFilter(value)}
maxDropdownHeight={800}
/>
{selectedFilter &&
(filterType === 'date' ? (
<DateInput
label={t`Value`}
placeholder={t`Select date value`}
onChange={setDateValue}
/>
) : (
<Select
data={valueOptions}
label={t`Value`}
searchable={true}
placeholder={t`Select filter value`}
onChange={(value: string | null) => setSelectedValue(value)}
maxDropdownHeight={800}
/>
))}
{selectedFilter && (
<FilterElement
filterType={filterType}
valueOptions={valueOptions}
onValueChange={setSelectedValue}
/>
)}
</Stack>
);
}
@@ -25,12 +25,18 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -130,6 +136,12 @@ export function BuildOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@@ -283,12 +283,10 @@ export default function ParametricPartTable({
if (column?.accessor?.toString()?.startsWith('parameter_')) {
const col = column as any;
onParameterClick(col.extra.template, record);
} else {
} else if (record?.pk) {
// Navigate through to the part detail page
if (record?.pk) {
const url = getDetailUrl(ModelType.part, record.pk);
navigateToLink(url, navigate, event);
}
const url = getDetailUrl(ModelType.part, record.pk);
navigateToLink(url, navigate, event);
}
}
}}
@@ -76,7 +76,8 @@ export default function PartParameterTemplateTable() {
description: {},
units: {},
choices: {},
checkbox: {}
checkbox: {},
selectionlist: {}
};
}, []);
@@ -0,0 +1,134 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import { selectionListFields } from '../../forms/selectionListFields';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import type { TableColumn } from '../Column';
import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Table for displaying list of selectionlist items
*/
export default function SelectionListTable() {
const table = useTable('selectionlist');
const user = useUserState();
const columns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'name',
sortable: true
},
{
accessor: 'description',
sortable: true
},
BooleanColumn({
accessor: 'active'
}),
BooleanColumn({
accessor: 'locked'
}),
{
accessor: 'source_plugin',
sortable: true
},
{
accessor: 'source_string',
sortable: true
},
{
accessor: 'entry_count'
}
];
}, []);
const newSelectionList = useCreateApiFormModal({
url: ApiEndpoints.selectionlist_list,
title: t`Add Selection List`,
fields: selectionListFields(),
table: table
});
const [selectedSelectionList, setSelectedSelectionList] = useState<
number | undefined
>(undefined);
const editSelectionList = useEditApiFormModal({
url: ApiEndpoints.selectionlist_list,
pk: selectedSelectionList,
title: t`Edit Selection List`,
fields: selectionListFields(),
table: table
});
const deleteSelectionList = useDeleteApiFormModal({
url: ApiEndpoints.selectionlist_list,
pk: selectedSelectionList,
title: t`Delete Selection List`,
table: table
});
const rowActions = useCallback(
(record: any): RowAction[] => {
return [
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.admin),
onClick: () => {
setSelectedSelectionList(record.pk);
editSelectionList.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.admin),
onClick: () => {
setSelectedSelectionList(record.pk);
deleteSelectionList.open();
}
})
];
},
[user]
);
const tableActions = useMemo(() => {
return [
<AddItemButton
key='add-selection-list'
onClick={() => newSelectionList.open()}
tooltip={t`Add Selection List`}
/>
];
}, []);
return (
<>
{newSelectionList.modal}
{editSelectionList.modal}
{deleteSelectionList.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.selectionlist_list)}
tableState={table}
columns={columns}
props={{
rowActions: rowActions,
tableActions: tableActions,
enableDownload: true
}}
/>
</>
);
}
@@ -14,6 +14,7 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import {
CompletionDateColumn,
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
@@ -25,13 +26,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -64,6 +71,12 @@ export function PurchaseOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@@ -108,6 +121,9 @@ export function PurchaseOrderTable({
ProjectCodeColumn({}),
CreationDateColumn({}),
TargetDateColumn({}),
CompletionDateColumn({
accessor: 'complete_date'
}),
{
accessor: 'total_price',
title: t`Total Price`,
@@ -14,8 +14,8 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import {
CompletionDateColumn,
CreationDateColumn,
DateColumn,
DescriptionColumn,
LineItemsProgressColumn,
ProjectCodeColumn,
@@ -26,13 +26,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -62,6 +68,12 @@ export function ReturnOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@@ -117,9 +129,8 @@ export function ReturnOrderTable({
ProjectCodeColumn({}),
CreationDateColumn({}),
TargetDateColumn({}),
DateColumn({
accessor: 'complete_date',
title: t`Completion Date`
CompletionDateColumn({
accessor: 'complete_date'
}),
ResponsibleColumn({}),
{
@@ -141,6 +152,9 @@ export function ReturnOrderTable({
url: ApiEndpoints.return_order_list,
title: t`Add Return Order`,
fields: returnOrderFields,
initialData: {
customer: customerId
},
follow: true,
modelType: ModelType.returnorder
});
@@ -27,13 +27,19 @@ import {
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
CompletedAfterFilter,
CompletedBeforeFilter,
CreatedAfterFilter,
CreatedBeforeFilter,
HasProjectCodeFilter,
MaxDateFilter,
MinDateFilter,
OutstandingFilter,
OverdueFilter,
StatusFilterOptions,
type TableFilter
type TableFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -63,6 +69,12 @@ export function SalesOrderTable({
AssignedToMeFilter(),
MinDateFilter(),
MaxDateFilter(),
CreatedBeforeFilter(),
CreatedAfterFilter(),
TargetDateBeforeFilter(),
TargetDateAfterFilter(),
CompletedBeforeFilter(),
CompletedAfterFilter(),
{
name: 'project_code',
label: t`Project Code`,
@@ -236,7 +236,7 @@ export function TemplateTable({
sortable: false,
switchable: true
},
...Object.entries(additionalFormFields || {})?.map(([key, field]) => ({
...Object.entries(additionalFormFields || {}).map(([key, field]) => ({
accessor: key,
...field,
title: field.label,
@@ -286,7 +286,11 @@ function stockItemTableColumns(): TableColumn[] {
/**
* Construct a list of available filters for the stock item table
*/
function stockItemTableFilters(): TableFilter[] {
function stockItemTableFilters({
enableExpiry
}: {
enableExpiry: boolean;
}): TableFilter[] {
return [
{
name: 'active',
@@ -354,15 +358,35 @@ function stockItemTableFilters(): TableFilter[] {
label: t`Is Serialized`,
description: t`Show items which have a serial number`
},
// TODO: serial
// TODO: serial_gte
// TODO: serial_lte
{
name: 'batch',
label: t`Batch Code`,
description: t`Filter items by batch code`,
type: 'text'
},
{
name: 'serial',
label: t`Serial Number`,
description: t`Filter items by serial number`,
type: 'text'
},
{
name: 'serial_lte',
label: t`Serial Number LTE`,
description: t`Show items with serial numbers less than or equal to a given value`,
type: 'text'
},
{
name: 'serial_gte',
label: t`Serial Number GTE`,
description: t`Show items with serial numbers greater than or equal to a given value`,
type: 'text'
},
{
name: 'has_batch',
label: t`Has Batch Code`,
description: t`Show items which have a batch code`
},
// TODO: batch
{
name: 'tracked',
label: t`Tracked`,
@@ -373,10 +397,56 @@ function stockItemTableFilters(): TableFilter[] {
label: t`Has Purchase Price`,
description: t`Show items which have a purchase price`
},
// TODO: Expired
// TODO: stale
// TODO: expiry_date_lte
// TODO: expiry_date_gte
{
name: 'expired',
label: t`Expired`,
description: t`Show items which have expired`,
active: enableExpiry
},
{
name: 'stale',
label: t`Stale`,
description: t`Show items which are stale`,
active: enableExpiry
},
{
name: 'expiry_before',
label: t`Expired Before`,
description: t`Show items which expired before this date`,
type: 'date',
active: enableExpiry
},
{
name: 'expiry_after',
label: t`Expired After`,
description: t`Show items which expired after this date`,
type: 'date',
active: enableExpiry
},
{
name: 'updated_before',
label: t`Updated Before`,
description: t`Show items updated before this date`,
type: 'date'
},
{
name: 'updated_after',
label: t`Updated After`,
description: t`Show items updated after this date`,
type: 'date'
},
{
name: 'stocktake_before',
label: t`Stocktake Before`,
description: t`Show items counted before this date`,
type: 'date'
},
{
name: 'stocktake_after',
label: t`Stocktake After`,
description: t`Show items counted after this date`,
type: 'date'
},
{
name: 'external',
label: t`External Location`,
@@ -397,12 +467,25 @@ export function StockItemTable({
allowAdd?: boolean;
tableName: string;
}>) {
const tableColumns = useMemo(() => stockItemTableColumns(), []);
const tableFilters = useMemo(() => stockItemTableFilters(), []);
const table = useTable(tableName);
const user = useUserState();
const settings = useGlobalSettingsState();
const stockExpiryEnabled = useMemo(
() => settings.isSet('STOCK_ENABLE_EXPIRY'),
[settings]
);
const tableColumns = useMemo(() => stockItemTableColumns(), []);
const tableFilters = useMemo(
() =>
stockItemTableFilters({
enableExpiry: stockExpiryEnabled
}),
[stockExpiryEnabled]
);
const tableActionParams: StockOperationProps = useMemo(() => {
return {
items: table.selectedRecords,
@@ -521,7 +604,7 @@ export function StockItemTable({
{
name: t`Assign to customer`,
icon: <InvenTreeIcon icon='customer' />,
tooltip: t`Order new stock`,
tooltip: t`Assign items to a customer`,
disabled: !can_add_stock,
onClick: () => {
assignStock.open();
+46
View File
@@ -0,0 +1,46 @@
/**
* Open the filter drawer for the currently visible table
* @param page - The page object
*/
export const openFilterDrawer = async (page) => {
await page.getByLabel('table-select-filters').click();
};
/**
* Close the filter drawer for the currently visible table
* @param page - The page object
*/
export const closeFilterDrawer = async (page) => {
await page.getByLabel('filter-drawer-close').click();
};
/**
* Click the specified button (if it is visible)
* @param page - The page object
* @param name - The name of the button to click
*/
export const clickButtonIfVisible = async (page, name, timeout = 500) => {
await page.waitForTimeout(timeout);
if (await page.getByRole('button', { name }).isVisible()) {
await page.getByRole('button', { name }).click();
}
};
/**
* Clear all filters from the currently visible table
* @param page - The page object
*/
export const clearTableFilters = async (page) => {
await openFilterDrawer(page);
await clickButtonIfVisible(page, 'Clear Filters');
await page.getByLabel('filter-drawer-close').click();
};
/**
* Return the parent 'row' element for a given 'cell' element
* @param cell - The cell element
*/
export const getRowFromCell = async (cell) => {
return cell.locator('xpath=ancestor::tr').first();
};
+1 -1
View File
@@ -8,7 +8,7 @@ test('Modals as admin', async ({ page }) => {
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', {
name: 'Server Information About this Inventree instance'
name: 'Server Information About this InvenTree instance'
})
.click();
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
+23 -7
View File
@@ -1,8 +1,13 @@
import { test } from '../baseFixtures.ts';
import { baseUrl } from '../defaults.ts';
import {
clickButtonIfVisible,
getRowFromCell,
openFilterDrawer
} from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
test('Pages - Build Order', async ({ page }) => {
test('Build Order - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
@@ -82,7 +87,7 @@ test('Pages - Build Order', async ({ page }) => {
.waitFor();
});
test('Pages - Build Order - Build Outputs', async ({ page }) => {
test('Build Order - Build Outputs', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/`);
@@ -140,7 +145,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
// Cancel one of the newly created outputs
const cell = await page.getByRole('cell', { name: `# ${sn}` });
const row = await cell.locator('xpath=ancestor::tr').first();
const row = await getRowFromCell(cell);
await row.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Cancel' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
@@ -148,7 +153,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
// Complete the other output
const cell2 = await page.getByRole('cell', { name: `# ${sn + 1}` });
const row2 = await cell2.locator('xpath=ancestor::tr').first();
const row2 = await getRowFromCell(cell2);
await row2.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Complete' }).click();
await page.getByLabel('related-field-location').click();
@@ -158,7 +163,7 @@ test('Pages - Build Order - Build Outputs', async ({ page }) => {
await page.getByText('Build outputs have been completed').waitFor();
});
test('Pages - Build Order - Allocation', async ({ page }) => {
test('Build Order - Allocation', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/manufacturing/build-order/1/line-items`);
@@ -170,7 +175,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
// The capacitor stock should be fully allocated
const cell = await page.getByRole('cell', { name: /C_1uF_0805/ });
const row = await cell.locator('xpath=ancestor::tr').first();
const row = await getRowFromCell(cell);
await row.getByText(/150 \/ 150/).waitFor();
@@ -237,7 +242,7 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
const item = data[idx];
const cell = await page.getByRole('cell', { name: item.name });
const row = await cell.locator('xpath=ancestor::tr').first();
const row = await getRowFromCell(cell);
const progress = `${item.allocated} / ${item.required}`;
await row.getByRole('cell', { name: item.ipn }).first().waitFor();
@@ -257,3 +262,14 @@ test('Pages - Build Order - Allocation', async ({ page }) => {
.getByRole('menuitem', { name: 'Deallocate Stock', exact: true })
.waitFor();
});
test('Build Order - Filters', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/manufacturing/index/buildorders`);
await openFilterDrawer(page);
await clickButtonIfVisible(page, 'Clear Filters');
await page.waitForTimeout(2500);
});
@@ -2,7 +2,7 @@ import { test } from '../baseFixtures.js';
import { doQuickLogin } from '../login.js';
import { setPluginState } from '../settings.js';
test('Pages - Dashboard - Basic', async ({ page }) => {
test('Dashboard - Basic', async ({ page }) => {
await doQuickLogin(page);
await page.getByText('Use the menu to add widgets').waitFor();
@@ -35,7 +35,7 @@ test('Pages - Dashboard - Basic', async ({ page }) => {
await page.getByLabel('dashboard-accept-layout').click();
});
test('Pages - Dashboard - Plugins', async ({ page, request }) => {
test('Dashboard - Plugins', async ({ page, request }) => {
// Ensure that the "SampleUI" plugin is enabled
await setPluginState({
request,
+3 -6
View File
@@ -1,5 +1,6 @@
import { test } from '../baseFixtures';
import { baseUrl } from '../defaults';
import { getRowFromCell } from '../helpers';
import { doQuickLogin } from '../login';
/**
@@ -129,9 +130,7 @@ test('Parts - Allocations', async ({ page }) => {
// Check "progress" bar of BO0001
const build_order_cell = await page.getByRole('cell', { name: 'BO0001' });
const build_order_row = await build_order_cell
.locator('xpath=ancestor::tr')
.first();
const build_order_row = await getRowFromCell(build_order_cell);
await build_order_row.getByText('11 / 75').waitFor();
// Expand allocations against BO0001
@@ -147,9 +146,7 @@ test('Parts - Allocations', async ({ page }) => {
// Check "progress" bar of SO0025
const sales_order_cell = await page.getByRole('cell', { name: 'SO0025' });
const sales_order_row = await sales_order_cell
.locator('xpath=ancestor::tr')
.first();
const sales_order_row = await getRowFromCell(sales_order_cell);
await sales_order_row.getByText('3 / 10').waitFor();
// Expand allocations against SO0025
@@ -1,4 +1,5 @@
import { test } from '../baseFixtures.ts';
import { clickButtonIfVisible, openFilterDrawer } from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
test('Purchase Orders - General', async ({ page }) => {
@@ -51,6 +52,30 @@ test('Purchase Orders - General', async ({ page }) => {
await page.getByRole('tab', { name: 'Details' }).waitFor();
});
test('Purchase Orders - Filters', async ({ page }) => {
await doQuickLogin(page, 'reader', 'readonly');
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
// Open filters drawer
await openFilterDrawer(page);
await clickButtonIfVisible(page, 'Clear Filters');
await page.getByRole('button', { name: 'Add Filter' }).click();
// Check for expected filter options
await page.getByPlaceholder('Select filter').fill('before');
await page.getByRole('option', { name: 'Created Before' }).waitFor();
await page.getByRole('option', { name: 'Completed Before' }).waitFor();
await page.getByRole('option', { name: 'Target Date Before' }).waitFor();
await page.getByPlaceholder('Select filter').fill('after');
await page.getByRole('option', { name: 'Created After' }).waitFor();
await page.getByRole('option', { name: 'Completed After' }).waitFor();
await page.getByRole('option', { name: 'Target Date After' }).waitFor();
});
/**
* Tests for receiving items against a purchase order
*/

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