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:
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+191
@@ -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',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
)
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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" %}',
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+818
-886
File diff suppressed because it is too large
Load Diff
@@ -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`,
|
||||
|
||||
@@ -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' />
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -33,5 +33,6 @@ export enum ModelType {
|
||||
labeltemplate = 'labeltemplate',
|
||||
pluginconfig = 'pluginconfig',
|
||||
contenttype = 'contenttype',
|
||||
selectionlist = 'selectionlist',
|
||||
error = 'error'
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user