mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into switch-pytz
This commit is contained in:
commit
3e7a3b6d3a
@ -1,5 +1,3 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13
|
||||
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -18,7 +18,7 @@ updates:
|
||||
directories:
|
||||
- /contrib/container
|
||||
- /docs
|
||||
- /.github
|
||||
- /contrib/dev_reqs
|
||||
- /src/backend
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
3
.github/scripts/version_check.py
vendored
3
.github/scripts/version_check.py
vendored
@ -97,6 +97,9 @@ if __name__ == '__main__':
|
||||
)
|
||||
text = version_file.read_text()
|
||||
results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text)
|
||||
# If 2. args is true lower the version number by 1
|
||||
if len(sys.argv) > 2 and sys.argv[2] == 'true':
|
||||
results[0] = str(int(results[0]) - 1)
|
||||
print(results[0])
|
||||
exit(0)
|
||||
# GITHUB_REF_TYPE may be either 'branch' or 'tag'
|
||||
|
12
.github/workflows/docker.yaml
vendored
12
.github/workflows/docker.yaml
vendored
@ -68,7 +68,7 @@ jobs:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
|
||||
- name: Set Up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
|
||||
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Version Check
|
||||
@ -124,10 +124,10 @@ jobs:
|
||||
rm -rf InvenTree/_testfolder
|
||||
- name: Set up QEMU
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # pin@v3.1.0
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # pin@v3.4.0
|
||||
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # pin@v3.5.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # pin@v3.5.0
|
||||
@ -141,14 +141,14 @@ jobs:
|
||||
fi
|
||||
- name: Login to Dockerhub
|
||||
if: github.event_name != 'pull_request' && steps.docker_login.outputs.skip_dockerhub_login != 'true'
|
||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # pin@v3.2.0
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # pin@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log into registry ghcr.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # pin@v3.2.0
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # pin@v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@ -166,7 +166,7 @@ jobs:
|
||||
- name: Push Docker Images
|
||||
id: push-docker
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # pin@v6.3.0
|
||||
uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # pin@v6.5.0
|
||||
with:
|
||||
context: .
|
||||
file: ./contrib/container/Dockerfile
|
||||
|
26
.github/workflows/qc_checks.yaml
vendored
26
.github/workflows/qc_checks.yaml
vendored
@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
python_version: 3.9
|
||||
node_version: 18
|
||||
node_version: 20
|
||||
# The OS version must be set per job
|
||||
server_start_sleep: 60
|
||||
|
||||
@ -94,7 +94,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
|
||||
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: "pip"
|
||||
@ -115,7 +115,7 @@ jobs:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
|
||||
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Check Config
|
||||
@ -164,15 +164,27 @@ jobs:
|
||||
name: schema.yml
|
||||
path: src/backend/InvenTree/schema.yml
|
||||
- name: Download public schema
|
||||
if: needs.paths-filter.outputs.api == 'false'
|
||||
run: |
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
|
||||
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
|
||||
version="$(python3 .github/scripts/version_check.py only_version ${{ needs.paths-filter.outputs.api }} 2>&1)"
|
||||
echo "Version: $version"
|
||||
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
|
||||
echo "URL: $url"
|
||||
curl -s -o api.yaml $url
|
||||
code=$(curl -s -o api.yaml $url --write-out '%{http_code}' --silent)
|
||||
if [ "$code" != "200" ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloaded api.yaml"
|
||||
- name: Running OpenAPI Spec diff action
|
||||
id: breaking_changes
|
||||
uses: oasdiff/oasdiff-action/diff@205ce7e2c5ae1511e720cbd307cae79fd7d4a909 # pin@main
|
||||
with:
|
||||
base: 'api.yaml'
|
||||
revision: 'src/backend/InvenTree/schema.yml'
|
||||
format: 'html'
|
||||
- name: Echoing diff to step
|
||||
run: echo "${{ steps.breaking_changes.outputs.diff }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check for differences in API Schema
|
||||
if: needs.paths-filter.outputs.api == 'false'
|
||||
run: |
|
||||
@ -555,6 +567,8 @@ jobs:
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && yarn run compile && yarn run build
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Zip frontend
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -43,6 +43,10 @@ jobs:
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Write version file - TAG
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "${{ github.ref_name }}" > tag.txt
|
||||
- name: Zip frontend
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static/web
|
||||
|
2
.github/workflows/scorecard.yaml
vendored
2
.github/workflows/scorecard.yaml
vendored
@ -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@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
|
||||
uses: github/codeql-action/upload-sarif@2d790406f505036ef40ecba973cc774a50395aac # v3.25.13
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
python_version: 3.9
|
||||
node_version: 18
|
||||
node_version: 20
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
@ -14,8 +14,10 @@ env:
|
||||
- INVENTREE_BACKUP_DIR=/opt/inventree/backup
|
||||
- INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt
|
||||
- INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
|
||||
- APP_REPO=inventree/InvenTree
|
||||
before_install: contrib/packager.io/preinstall.sh
|
||||
after_install: contrib/packager.io/postinstall.sh
|
||||
before_remove: contrib/packager.io/preinstall.sh
|
||||
before:
|
||||
- contrib/packager.io/before.sh
|
||||
dependencies:
|
||||
@ -33,7 +35,7 @@ dependencies:
|
||||
- gettext
|
||||
- nginx
|
||||
- jq
|
||||
- libffi7
|
||||
- "libffi7 | libffi8"
|
||||
targets:
|
||||
ubuntu-20.04: true
|
||||
debian-11: true
|
||||
|
@ -17,7 +17,7 @@ repos:
|
||||
- id: check-yaml
|
||||
- id: mixed-line-ending
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.1
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
args: [--preview]
|
||||
@ -27,23 +27,23 @@ repos:
|
||||
--preview
|
||||
]
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 0.1.35
|
||||
rev: 0.2.13
|
||||
hooks:
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements-dev.in
|
||||
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt]
|
||||
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --no-strip-extras, --generate-hashes]
|
||||
files: src/backend/requirements-dev\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
args: [src/backend/requirements.in, -o, src/backend/requirements.txt]
|
||||
args: [src/backend/requirements.in, -o, src/backend/requirements.txt, --no-strip-extras, --generate-hashes]
|
||||
files: src/backend/requirements\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt]
|
||||
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt, --no-strip-extras, --generate-hashes]
|
||||
files: contrib/dev_reqs/requirements\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
args: [docs/requirements.in, -o, docs/requirements.txt]
|
||||
args: [docs/requirements.in, -o, docs/requirements.txt, --no-strip-extras, --generate-hashes]
|
||||
files: docs/requirements\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
@ -54,9 +54,11 @@ repos:
|
||||
hooks:
|
||||
- id: djlint-django
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
exclude: >
|
||||
(?x)^(
|
||||
docs/docs/stylesheets/.*|
|
||||
@ -75,7 +77,7 @@ repos:
|
||||
- "prettier@^2.4.1"
|
||||
- "@trivago/prettier-plugin-sort-imports"
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: "v9.4.0"
|
||||
rev: "v9.6.0"
|
||||
hooks:
|
||||
- id: eslint
|
||||
additional_dependencies:
|
||||
@ -87,7 +89,7 @@ repos:
|
||||
- "@typescript-eslint/parser"
|
||||
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.18.3
|
||||
rev: v8.18.4
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
#- repo: https://github.com/jumanjihouse/pre-commit-hooks
|
||||
|
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@ -6,19 +6,37 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "InvenTree Server",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
|
||||
"args": ["runserver"],
|
||||
"args": [
|
||||
"runserver",
|
||||
// "0.0.0.0:8000", // expose server in network (useful for testing with mobile app)
|
||||
// "--noreload" // disable auto-reload
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "InvenTree Server - Tests",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
|
||||
"args": [
|
||||
"test",
|
||||
// "part.test_api.PartCategoryAPITest", // run only a specific test
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "InvenTree Server - 3rd party",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
|
||||
"args": ["runserver"],
|
||||
"args": [
|
||||
"runserver"
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": false
|
||||
},
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
# Docker compose recipe for InvenTree development server
|
||||
# - Runs PostgreSQL as the database backend
|
||||
# - Uses built-in django webserver
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
# Docker compose recipe for a production-ready InvenTree setup, with the following containers:
|
||||
# - PostgreSQL as the database backend
|
||||
# - gunicorn as the InvenTree web server
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/bin/ash
|
||||
|
||||
# Install system packages required for building InvenTree python libraries
|
||||
# Note that for postgreslql, we use the 13 version, which matches the version used in the InvenTree docker image
|
||||
# Note that for postgreslql, we use the version 13, which matches the version used in the InvenTree docker image
|
||||
|
||||
apk add gcc g++ musl-dev openssl-dev libffi-dev cargo python3-dev openldap-dev \
|
||||
libstdc++ build-base linux-headers py3-grpcio \
|
||||
|
@ -11,12 +11,15 @@ django==4.2.14 \
|
||||
django-auth-ldap==4.8.0 \
|
||||
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
|
||||
--hash=sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738
|
||||
# via -r contrib/container/requirements.in
|
||||
gunicorn==22.0.0 \
|
||||
--hash=sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9 \
|
||||
--hash=sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63
|
||||
# via -r contrib/container/requirements.in
|
||||
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 \
|
||||
@ -29,6 +32,7 @@ mariadb==1.1.10 \
|
||||
--hash=sha256:a332893e3ef7ceb7970ab4bd7c844bcb4bd68a051ca51313566f9808d7411f2d \
|
||||
--hash=sha256:d7b09ec4abd02ed235257feb769f90cd4066e8f536b55b92f5166103d5b66a63 \
|
||||
--hash=sha256:dff8b28ce4044574870d7bdd2d9f9f5da8e5f95a7ff6d226185db733060d1a93
|
||||
# via -r contrib/container/requirements.in
|
||||
mysqlclient==2.2.4 \
|
||||
--hash=sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711 \
|
||||
--hash=sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41 \
|
||||
@ -39,6 +43,7 @@ mysqlclient==2.2.4 \
|
||||
--hash=sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5 \
|
||||
--hash=sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54 \
|
||||
--hash=sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab
|
||||
# via -r contrib/container/requirements.in
|
||||
packaging==24.0 \
|
||||
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
|
||||
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
|
||||
@ -48,6 +53,7 @@ packaging==24.0 \
|
||||
psycopg[binary, pool]==3.1.18 \
|
||||
--hash=sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b \
|
||||
--hash=sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e
|
||||
# via -r contrib/container/requirements.in
|
||||
psycopg-binary==3.1.18 \
|
||||
--hash=sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7 \
|
||||
--hash=sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c \
|
||||
@ -131,7 +137,9 @@ pyasn1-modules==0.4.0 \
|
||||
# via python-ldap
|
||||
python-ldap==3.4.4 \
|
||||
--hash=sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828
|
||||
# via django-auth-ldap
|
||||
# via
|
||||
# -r contrib/container/requirements.in
|
||||
# django-auth-ldap
|
||||
pyyaml==6.0.1 \
|
||||
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
|
||||
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
|
||||
@ -184,9 +192,11 @@ pyyaml==6.0.1 \
|
||||
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
|
||||
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
|
||||
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
|
||||
setuptools==69.5.1 \
|
||||
--hash=sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987 \
|
||||
--hash=sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32
|
||||
# via -r contrib/container/requirements.in
|
||||
setuptools==70.3.0 \
|
||||
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
|
||||
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
|
||||
# via -r contrib/container/requirements.in
|
||||
sqlparse==0.5.0 \
|
||||
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
|
||||
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
|
||||
@ -215,6 +225,8 @@ uv==0.1.38 \
|
||||
--hash=sha256:b0b15e51a0f8240969bc412ed0dd60cfe3f664b30173139ef263d71c596d631f \
|
||||
--hash=sha256:ea44c07605d1359a7d82bf42706dd86d341f15f4ca2e1f36e51626a7111c2ad5 \
|
||||
--hash=sha256:f87c9711493c53d32012a96b49c4d53aabdf7ed666cbf2c3fb55dd402a6b31a8
|
||||
# via -r contrib/container/requirements.in
|
||||
wheel==0.43.0 \
|
||||
--hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \
|
||||
--hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81
|
||||
# via -r contrib/container/requirements.in
|
||||
|
@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt
|
||||
# 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
|
||||
@ -103,6 +103,7 @@ idna==3.7 \
|
||||
jc==1.25.2 \
|
||||
--hash=sha256:26e412a65a478f9da3097653db6277f915cfae5c0f0a3f42026b405936abd358 \
|
||||
--hash=sha256:97ada193495f79550f06fe0cbfb119ff470bcca57c1cc593a5cdb0008720e0b3
|
||||
# via -r contrib/dev_reqs/requirements.in
|
||||
pygments==2.17.2 \
|
||||
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
|
||||
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
|
||||
@ -159,9 +160,11 @@ pyyaml==6.0.1 \
|
||||
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
|
||||
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
|
||||
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
|
||||
# via -r contrib/dev_reqs/requirements.in
|
||||
requests==2.32.2 \
|
||||
--hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \
|
||||
--hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c
|
||||
# via -r contrib/dev_reqs/requirements.in
|
||||
ruamel-yaml==0.18.6 \
|
||||
--hash=sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636 \
|
||||
--hash=sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b
|
||||
|
@ -75,6 +75,7 @@ root_command() {
|
||||
;;
|
||||
"Debian GNU/Linux" | "debian gnu/linux" | Raspbian)
|
||||
if [[ $VER == "12" ]]; then
|
||||
DIST_VER="11"
|
||||
SUPPORTED=true
|
||||
elif [[ $VER == "11" ]]; then
|
||||
SUPPORTED=true
|
||||
|
@ -5,33 +5,40 @@
|
||||
|
||||
set -eu
|
||||
|
||||
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
|
||||
echo "Setting VERSION information to $VERSION"
|
||||
echo "$VERSION" > VERSION
|
||||
|
||||
# The sha is the second element in APP_PKG_ITERATION
|
||||
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
|
||||
SHA=$(echo $APP_PKG_ITERATION | cut -d'.' -f2)
|
||||
|
||||
# Download info
|
||||
echo "Getting info from github for commit $SHA"
|
||||
curl -L \
|
||||
echo "INFO collection | Getting info from github for commit $SHA"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA > commit.json
|
||||
curl -L \
|
||||
https://api.github.com/repos/$APP_REPO/commits/$SHA > commit.json
|
||||
echo "INFO collection | Got commit.json with size $(wc -c commit.json)"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA/branches-where-head > branches.json
|
||||
https://api.github.com/repos/$APP_REPO/commits/$SHA/branches-where-head > branches.json
|
||||
echo "INFO collection | Got branches.json with size $(wc -c branches.json)"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/$APP_REPO/commits/$APP_PKG_VERSION > tag.json
|
||||
echo "INFO collection | Got tag.json with size $(wc -c tag.json)"
|
||||
|
||||
# Extract info
|
||||
echo "Extracting info from github"
|
||||
echo "INFO extract | Extracting info from github"
|
||||
DATE=$(jq -r '.commit.committer.date' commit.json)
|
||||
BRANCH=$(jq -r '.[].name' branches.json)
|
||||
NODE_ID=$(jq -r '.node_id' commit.json)
|
||||
SIGNATURE=$(jq -r '.commit.verification.signature' commit.json)
|
||||
FULL_SHA=$(jq -r '.sha' commit.json)
|
||||
|
||||
echo "Write VERSION information"
|
||||
echo "INFO write | Write VERSION information"
|
||||
echo "$VERSION" > VERSION
|
||||
echo "INVENTREE_COMMIT_HASH='$SHA'" >> VERSION
|
||||
echo "INVENTREE_COMMIT_SHA='$FULL_SHA'" >> VERSION
|
||||
echo "INVENTREE_COMMIT_DATE='$DATE'" >> VERSION
|
||||
echo "INVENTREE_PKG_INSTALLER='PKG'" >> VERSION
|
||||
echo "INVENTREE_PKG_BRANCH='$BRANCH'" >> VERSION
|
||||
@ -39,5 +46,22 @@ echo "INVENTREE_PKG_TARGET='$TARGET'" >> VERSION
|
||||
echo "NODE_ID='$NODE_ID'" >> VERSION
|
||||
echo "SIGNATURE='$SIGNATURE'" >> VERSION
|
||||
|
||||
echo "Written VERSION information"
|
||||
echo "INFO write | Written VERSION information"
|
||||
echo "### VERSION ###"
|
||||
cat VERSION
|
||||
echo "### VERSION ###"
|
||||
|
||||
# Try to get frontend
|
||||
echo "INFO frontend | Trying to get frontend"
|
||||
# Check if tag sha is the same as the commit sha
|
||||
TAG_SHA=$(jq -r '.sha' tag.json)
|
||||
if [ "$TAG_SHA" != "$FULL_SHA" ]; then
|
||||
echo "INFO frontend | Tag sha '$TAG_SHA' is not the same as commit sha $FULL_SHA, can not download frontend"
|
||||
else
|
||||
echo "INFO frontend | Getting frontend from github via tag"
|
||||
curl https://github.com/$APP_REPO/releases/download/$APP_PKG_VERSION/frontend-build.zip -L -O -f
|
||||
mkdir -p src/backend/InvenTree/web/static
|
||||
echo "INFO frontend | Unzipping frontend"
|
||||
unzip -qq frontend-build.zip -d src/backend/InvenTree/web/static/web
|
||||
echo "INFO frontend | Unzipped frontend"
|
||||
fi
|
||||
|
@ -60,7 +60,7 @@ function detect_python() {
|
||||
fi
|
||||
|
||||
# Try to detect a python between 3.9 and 3.12 in reverse order
|
||||
if [ -z "${SETUP_PYTHON}" ]; then
|
||||
if [ -z "$(which ${SETUP_PYTHON})" ]; then
|
||||
echo "# Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
|
||||
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
|
||||
echo "# Checking for python3.${i}"
|
||||
@ -318,17 +318,17 @@ function set_env() {
|
||||
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE}
|
||||
|
||||
# Database engine
|
||||
sed -i s=#ENGINE:\ sampleengine=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ ENGINE:\ Database\ engine.\ Selection\ from:=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
|
||||
# Database name
|
||||
sed -i s=#NAME:\ \'/path/to/database\'=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ NAME:\ Database\ name=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
|
||||
# Database user
|
||||
sed -i s=#USER:\ sampleuser=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ USER:\ Database\ username\ \(if\ required\)=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
|
||||
# Database password
|
||||
sed -i s=#PASSWORD:\ samplepassword=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ PASSWORD:\ Database\ password\ \(if\ required\)=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
|
||||
# Database host
|
||||
sed -i s=#HOST:\ samplehost=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ HOST:\ Database\ host\ address\ \(if\ required\)=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
|
||||
# Database port
|
||||
sed -i s=#PORT:\ 123456=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
|
||||
sed -i s=#\ PORT:\ Database\ host\ port\ \(if\ required\)=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
|
||||
|
||||
# Fixing the permissions
|
||||
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# packager.io preinstall script
|
||||
# packager.io preinstall/preremove script
|
||||
#
|
||||
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
|
||||
|
||||
|
@ -4,7 +4,11 @@ title: Internal Barcodes
|
||||
|
||||
## Internal Barcodes
|
||||
|
||||
InvenTree defines an internal format for generating barcodes for various items. This format uses a simple JSON-style string to uniquely identify an item in the database.
|
||||
InvenTree ships with two integrated internal formats for generating barcodes for various items which are available through the built-in InvenTree Barcode plugin. The used format can be selected through the plugin settings of the InvenTree Barcode plugin.
|
||||
|
||||
### 1. JSON-based QR Codes
|
||||
|
||||
This format uses a simple JSON-style string to uniquely identify an item in the database.
|
||||
|
||||
Some simple examples of this format are shown below:
|
||||
|
||||
@ -12,10 +16,49 @@ Some simple examples of this format are shown below:
|
||||
| --- | --- |
|
||||
| Part | `{% raw %}{"part": 10}{% endraw %}` |
|
||||
| Stock Item | `{% raw %}{"stockitem": 123}{% endraw %}` |
|
||||
| Stock Location | `{% raw %}{"stocklocation": 1}{% endraw %}` |
|
||||
| Supplier Part | `{% raw %}{"supplierpart": 99}{% endraw %}` |
|
||||
|
||||
The numerical ID value used is the *Primary Key* (PK) of the particular object in the database.
|
||||
|
||||
#### Downsides
|
||||
|
||||
1. The JSON format includes binary only characters (`{% raw %}{{% endraw %}` and `{% raw %}"{% endraw %}`) which requires unnecessary use of the binary QR code encoding which means fewer amount of chars can be encoded with the same version of QR code.
|
||||
2. The model name key has not a fixed length. Some model names are longer than others. E.g. a part QR code with the shortest possible id requires 11 chars, while a stock location QR code with the same id would already require 20 chars, which already requires QR code version 2 and quickly version 3.
|
||||
|
||||
!!! info "QR code versions"
|
||||
There are 40 different qr code versions from 1-40. They all can encode more data than the previous version, but require more "squares". E.g. a V1 QR codes has 21x21 "squares" while a V2 already has 25x25. For more information see [QR code comparison](https://www.qrcode.com/en/about/version.html).
|
||||
|
||||
For a more detailed size analysis of the JSON-based QR codes refer to [this issue](https://github.com/inventree/InvenTree/issues/6612).
|
||||
|
||||
### 2. Short alphanumeric QR Codes
|
||||
|
||||
While JSON-based QR Codes encode all necessary information, they come with the described downsides. This new, short, alphanumeric only format is build to improve those downsides. The basic format uses an alphanumeric string: `INV-??x`
|
||||
|
||||
- `INV-` is a constant prefix. This is configurable in the InvenTree Barcode plugins settings per instance to support environments that use multiple instances.
|
||||
- `??` is a two character alphanumeric (`0-9A-Z $%*+-./:` (45 chars)) code, individual to each model.
|
||||
- `x` the actual pk of the model.
|
||||
|
||||
Now with an overhead of 6 chars for every model, this format supports the following amount of model instances using the described QR code modes:
|
||||
|
||||
| QR code mode | Alphanumeric mode | Mixed mode |
|
||||
| --- | --- | --- |
|
||||
| v1 M ECL (15%) | `10**14` items (~3.170 items per sec for 1000 years) | `10**20` items (~3.170.979.198 items per sec for 1000 years) |
|
||||
| v1 Q ECL (25%) | `10**10` items (~0.317 items per sec for 1000 years) | `10**13` items (~317 items per sec for 1000 years) |
|
||||
| v1 H ECL (30%) | `10**4` items (~100 items per day for 100 days) | `10**3` items (~100 items per day for 10 days (*even worse*)) |
|
||||
|
||||
!!! info "QR code mixed mode"
|
||||
Normally the QR code data is encoded only in one format (binary, alphanumeric, numeric). But the data can also be split into multiple chunks using different formats. This is especially useful with long model ids, because the first 6 chars can be encoded using the alphanumeric mode and the id using the more efficient numeric mode. Mixed mode is used by default, because the `qrcode` template tag uses a default value for optimize of 1.
|
||||
|
||||
Some simple examples of this format are shown below:
|
||||
|
||||
| Model Type | Example Barcode |
|
||||
| --- | --- |
|
||||
| Part | `INV-PA10` |
|
||||
| Stock Item | `INV-SI123` |
|
||||
| Stock Location | `INV-SL1` |
|
||||
| Supplier Part | `INV-SP99` |
|
||||
|
||||
## Report Integration
|
||||
|
||||
This barcode format can be used to generate 1D or 2D barcodes (e.g. for [labels and reports](../report/barcodes.md))
|
||||
|
@ -125,7 +125,7 @@ The core software modules are targeting the following versions:
|
||||
| Python | {{ config.extra.min_python_version }} | Minimum required version |
|
||||
| Invoke | {{ config.extra.min_invoke_version }} | Minimum required version |
|
||||
| Django | {{ config.extra.django_version }} | Pinned version |
|
||||
| Node | 18 | Only needed for frontend development |
|
||||
| Node | 20 | Only needed for frontend development |
|
||||
|
||||
Any other software dependencies are handled by the project package config.
|
||||
|
||||
|
@ -67,7 +67,7 @@ If you need to process your queue with background workers, run the `worker` task
|
||||
You can either only run InvenTree or use the integrated debugger for debugging. Goto the `Run and debug` side panel make sure `InvenTree Server` is selected. Click on the play button on the left.
|
||||
|
||||
!!! tip "Debug with 3rd party"
|
||||
Sometimes you need to debug also some 3rd party packages. Just select `InvenTree Servre - 3rd party`
|
||||
Sometimes you need to debug also some 3rd party packages. Just select `InvenTree Server - 3rd party`
|
||||
|
||||
You can now set breakpoints and vscode will automatically pause execution if that point is hit. You can see all variables available in that context and evaluate some code with the debugger console at the bottom. Use the play or step buttons to continue execution.
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: Barcode Mixin
|
||||
|
||||
## Barcode Plugins
|
||||
|
||||
InvenTree supports decoding of arbitrary barcode data via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
|
||||
InvenTree supports decoding of arbitrary barcode data and generation of internal barcode formats via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
|
||||
|
||||
InvenTree can generate native QR codes to represent database objects (e.g. a single StockItem). This barcode can then be used to perform quick lookup of a stock item or location in the database. A client application (for example the InvenTree mobile app) scans a barcode, and sends the barcode data to the InvenTree server. The server then uses the **InvenTreeBarcodePlugin** (found at `src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py`) to decode the supplied barcode data.
|
||||
|
||||
@ -26,7 +26,7 @@ POST {
|
||||
|
||||
### Builtin Plugin
|
||||
|
||||
The InvenTree server includes a builtin barcode plugin which can decode QR codes generated by the server. This plugin is enabled by default.
|
||||
The InvenTree server includes a builtin barcode plugin which can generate and decode the QR codes. This plugin is enabled by default.
|
||||
|
||||
::: plugin.builtin.barcodes.inventree_barcode.InvenTreeInternalBarcodePlugin
|
||||
options:
|
||||
@ -39,14 +39,12 @@ The InvenTree server includes a builtin barcode plugin which can decode QR codes
|
||||
|
||||
### Example Plugin
|
||||
|
||||
Please find below a very simple example that is executed each time a barcode is scanned.
|
||||
Please find below a very simple example that is used to return a part if the barcode starts with `PART-`
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from InvenTree.models import InvenTreeBarcodeMixin
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import BarcodeMixin
|
||||
from part.models import Part
|
||||
|
||||
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
|
||||
@ -56,16 +54,39 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
VERSION = "0.0.1"
|
||||
AUTHOR = "Michael"
|
||||
|
||||
status = 0
|
||||
|
||||
def scan(self, barcode_data):
|
||||
if barcode_data.startswith("PART-"):
|
||||
try:
|
||||
pk = int(barcode_data.split("PART-")[1])
|
||||
instance = Part.objects.get(pk=pk)
|
||||
label = Part.barcode_model_type()
|
||||
|
||||
self.status = self.status+1
|
||||
print('Started barcode plugin', self.status)
|
||||
print(barcode_data)
|
||||
response = {}
|
||||
return response
|
||||
|
||||
return {label: instance.format_matched_response()}
|
||||
except Part.DoesNotExist:
|
||||
pass
|
||||
```
|
||||
|
||||
To try it just copy the file to src/InvenTree/plugins and restart the server. Open the scan barcode window and start to scan codes or type in text manually. Each time the timeout is hit the plugin will execute and printout the result. The timeout can be changed in `Settings->Barcode Support->Barcode Input Delay`.
|
||||
|
||||
### Custom Internal Format
|
||||
|
||||
To implement a custom internal barcode format, the `generate(...)` method from the Barcode Mixin needs to be overridden. Then the plugin can be selected at `System Settings > Barcodes > Barcode Generation Plugin`.
|
||||
|
||||
```python
|
||||
from InvenTree.models import InvenTreeBarcodeMixin
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import BarcodeMixin
|
||||
|
||||
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
|
||||
NAME = "MyInternalBarcode"
|
||||
TITLE = "My Internal Barcodes"
|
||||
DESCRIPTION = "support for custom internal barcodes"
|
||||
VERSION = "0.0.1"
|
||||
AUTHOR = "InvenTree contributors"
|
||||
|
||||
def generate(self, model_instance: InvenTreeBarcodeMixin):
|
||||
return f'{model_instance.barcode_model_type()}: {model_instance.pk}'
|
||||
```
|
||||
|
||||
!!! info "Scanning implementation required"
|
||||
The parsing of the custom format needs to be implemented too, so that the scanning of the generated QR codes resolves to the correct part.
|
||||
|
19
docs/docs/extend/plugins/icon.md
Normal file
19
docs/docs/extend/plugins/icon.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Icon Pack Mixin
|
||||
---
|
||||
|
||||
## IconPackMixin
|
||||
|
||||
The IconPackMixin class provides basic functionality for letting plugins expose custom icon packs that are available in the InvenTree UI. This is especially useful to provide a custom crafted icon pack with icons for different location types, e.g. different sizes and styles of drawers, bags, ESD bags, ... which are not available in the standard tabler icons library.
|
||||
|
||||
### Sample Plugin
|
||||
|
||||
The following example demonstrates how to use the `IconPackMixin` class to add a custom icon pack:
|
||||
|
||||
::: plugin.samples.icons.icon_sample.SampleIconPlugin
|
||||
options:
|
||||
show_bases: False
|
||||
show_root_heading: False
|
||||
show_root_toc_entry: False
|
||||
show_source: True
|
||||
members: []
|
@ -259,6 +259,31 @@ A shortcut function is provided for rendering an image associated with a Company
|
||||
|
||||
*Preview* and *thumbnail* image variations can be rendered for the `company_image` tag, in a similar manner to [part image variations](#image-variations)
|
||||
|
||||
## Icons
|
||||
|
||||
Some models (e.g. part categories and locations) allow to specify a custom icon. To render these icons in a report, there is a `{% raw %}{% icon location.icon %}{% endraw %}` template tag from the report template library available.
|
||||
|
||||
This tag renders the required html for the icon.
|
||||
|
||||
!!! info "Loading fonts"
|
||||
Additionally the icon fonts need to be loaded into the template. This can be done using the `{% raw %}{% include_icon_fonts %}{% endraw %}` template tag inside of a style block
|
||||
|
||||
!!! tip "Custom classes for styling the icon further"
|
||||
The icon template tag accepts an optional `class` argument which can be used to apply a custom class to the rendered icon used to style the icon further e.g. positioning it, changing it's size, ... `{% raw %}{% icon location.icon class="my-class" %}{% endraw %}`.
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
{% load report %}
|
||||
|
||||
{% block style %}
|
||||
{% include_icon_fonts %}
|
||||
{% endblock style %}
|
||||
|
||||
{% icon location.icon %}
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
## InvenTree Logo
|
||||
|
||||
A template tag is provided to load the InvenTree logo image into a report. You can render the logo using the `{% raw %}{% logo_image %}{% endraw %}` tag:
|
||||
|
@ -297,6 +297,10 @@ Alternatively this location can be specified with the `INVENTREE_BACKUP_DIR` env
|
||||
|
||||
InvenTree provides allowance for additional sign-in options. The following options are not enabled by default, and care must be taken by the system administrator when configuring these settings.
|
||||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_MFA_ENABLED | mfa_enabled | Enable or disable multi-factor authentication support for the InvenTree server | True |
|
||||
|
||||
### Single Sign On
|
||||
|
||||
Single Sign On (SSO) allows users to sign in to InvenTree using a third-party authentication provider. This functionality is provided by the [django-allauth](https://docs.allauth.org/en/latest/) package.
|
||||
|
@ -212,3 +212,86 @@ To start afresh (and completely remove the existing database), run the following
|
||||
```
|
||||
docker compose run --rm inventree-server invoke delete-data
|
||||
```
|
||||
|
||||
## Install custom packages
|
||||
|
||||
To install custom packages to your docker image, a custom docker image can be built and used automatically each time when updating. The following changes need to be applied to the docker compose file:
|
||||
|
||||
<details><summary>docker-compose.yml changes</summary>
|
||||
|
||||
```diff
|
||||
diff --git a/docker-compose.yml b/docker-compose.yml
|
||||
index 8adee63..dc3993c 100644
|
||||
--- a/docker-compose.yml
|
||||
+++ b/docker-compose.yml
|
||||
@@ -69,7 +69,14 @@ services:
|
||||
# Uses gunicorn as the web server
|
||||
inventree-server:
|
||||
# If you wish to specify a particular InvenTree version, do so here
|
||||
- image: inventree/inventree:${INVENTREE_TAG:-stable}
|
||||
+ image: inventree/inventree:${INVENTREE_TAG:-stable}-custom
|
||||
+ pull_policy: never
|
||||
+ build:
|
||||
+ context: .
|
||||
+ dockerfile: Dockerfile
|
||||
+ target: production
|
||||
+ args:
|
||||
+ INVENTREE_TAG: ${INVENTREE_TAG:-stable}
|
||||
# Only change this port if you understand the stack.
|
||||
# If you change this you have to change:
|
||||
# - the proxy settings (on two lines)
|
||||
@@ -88,7 +95,8 @@ services:
|
||||
# Background worker process handles long-running or periodic tasks
|
||||
inventree-worker:
|
||||
# If you wish to specify a particular InvenTree version, do so here
|
||||
- image: inventree/inventree:${INVENTREE_TAG:-stable}
|
||||
+ image: inventree/inventree:${INVENTREE_TAG:-stable}-custom
|
||||
+ pull_policy: never
|
||||
command: invoke worker
|
||||
depends_on:
|
||||
- inventree-server
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
And the following `Dockerfile` needs to be created:
|
||||
|
||||
<details><summary>Dockerfile</summary>
|
||||
|
||||
```dockerfile
|
||||
ARG INVENTREE_TAG
|
||||
|
||||
FROM inventree/inventree:${INVENTREE_TAG} as production
|
||||
|
||||
# Install whatever dependency is needed here (e.g. git)
|
||||
RUN apk add --no-cache git
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
And if additional, development packages are needed e.g. just for building a wheel for a pip package, a multi stage build can be used with the following `Dockerfile`:
|
||||
|
||||
<details><summary>Dockerfile</summary>
|
||||
|
||||
```dockerfile
|
||||
ARG INVENTREE_TAG
|
||||
|
||||
# prebuild stage - needs a lot of build dependencies
|
||||
# make sure, the alpine and python version matches the version used in the inventree base image
|
||||
FROM python:3.11-alpine3.18 as prebuild
|
||||
|
||||
# Install whatever development dependency is needed (e.g. cups-dev, gcc, the musl-dev build tools and the pip pycups package)
|
||||
RUN apk add --no-cache cups-dev gcc musl-dev && \
|
||||
pip install --user --no-cache-dir pycups
|
||||
|
||||
# production image - only install the cups shared library
|
||||
FROM inventree/inventree:${INVENTREE_TAG} as production
|
||||
|
||||
# Install e.g. shared library later available in the final image
|
||||
RUN apk add --no-cache cups-libs
|
||||
|
||||
# Copy the pip wheels from the build stage in the production stage
|
||||
COPY --from=prebuild /root/.local /root/.local
|
||||
```
|
||||
|
||||
</details>
|
||||
|
@ -6,6 +6,18 @@ title: Stock
|
||||
|
||||
A stock location represents a physical real-world location where *Stock Items* are stored. Locations are arranged in a cascading manner and each location may contain multiple sub-locations, or stock, or both.
|
||||
|
||||
### Icons
|
||||
|
||||
Stock locations can be assigned custom icons (either directly or through [Stock Location Types](#stock-location-type)). When using PUI there is a custom icon picker component available that can help to select the right icon. However in CUI the icon needs to be entered manually.
|
||||
|
||||
By default, the tabler icons package (with prefix: `ti`) is available. To manually select an item, search on the [tabler icons](https://tabler.io/icons) page for an icon and copy its name e.g. `bookmark`. Some icons have a filled and an outline version (if no variants are specified, it's an outline variant). Now these values can be put into the format: `<package-prefix>:<icon-name>:<variant>`. E.g. `ti:bookmark:outline` or `ti:bookmark:filled`.
|
||||
|
||||
If there are some icons missing in the tabler icons package, users can even install their own custom icon packs through a plugin. See [`IconPackMixin`](../extend/plugins/icon.md).
|
||||
|
||||
## Stock Location Type
|
||||
|
||||
A stock location type represents a specific type of location (e.g. one specific size of drawer, shelf, ... or box) which can be assigned to multiple stock locations. In the first place, it is used to specify an icon and having the icon in sync for all locations that use this location type, but it also serves as a data field to quickly see what type of location this is. It is planned to add e.g. drawer dimension information to the location type to add a "find a matching, empty stock location" tool.
|
||||
|
||||
## Stock Item
|
||||
|
||||
A *Stock Item* is an actual instance of a [*Part*](../part/part.md) item. It represents a physical quantity of the *Part* in a specific location.
|
||||
|
23
docs/main.py
23
docs/main.py
@ -2,7 +2,6 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
@ -54,11 +53,23 @@ def check_link(url) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_build_enviroment() -> str:
|
||||
"""Returns the branch we are currently building on, based on the environment variables of the various CI platforms."""
|
||||
# Check if we are in ReadTheDocs
|
||||
if os.environ.get('READTHEDOCS') == 'True':
|
||||
return os.environ.get('READTHEDOCS_GIT_IDENTIFIER')
|
||||
# We are in GitHub Actions
|
||||
elif os.environ.get('GITHUB_ACTIONS') == 'true':
|
||||
return os.environ.get('GITHUB_REF')
|
||||
else:
|
||||
return 'master'
|
||||
|
||||
|
||||
def define_env(env):
|
||||
"""Define custom environment variables for the documentation build process."""
|
||||
|
||||
@env.macro
|
||||
def sourcedir(dirname, branch='master'):
|
||||
def sourcedir(dirname, branch=None):
|
||||
"""Return a link to a directory within the source code repository.
|
||||
|
||||
Arguments:
|
||||
@ -70,6 +81,9 @@ def define_env(env):
|
||||
Raises:
|
||||
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_enviroment()
|
||||
|
||||
if dirname.startswith('/'):
|
||||
dirname = dirname[1:]
|
||||
|
||||
@ -94,7 +108,7 @@ def define_env(env):
|
||||
return url
|
||||
|
||||
@env.macro
|
||||
def sourcefile(filename, branch='master', raw=False):
|
||||
def sourcefile(filename, branch=None, raw=False):
|
||||
"""Return a link to a file within the source code repository.
|
||||
|
||||
Arguments:
|
||||
@ -106,6 +120,9 @@ def define_env(env):
|
||||
Raises:
|
||||
- FileNotFoundError: If the file does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_enviroment()
|
||||
|
||||
if filename.startswith('/'):
|
||||
filename = filename[1:]
|
||||
|
||||
|
@ -203,6 +203,7 @@ nav:
|
||||
- Barcode Mixin: extend/plugins/barcode.md
|
||||
- Currency Mixin: extend/plugins/currency.md
|
||||
- Event Mixin: extend/plugins/event.md
|
||||
- Icon Pack Mixin: extend/plugins/icon.md
|
||||
- Label Printing Mixin: extend/plugins/label.md
|
||||
- Locate Mixin: extend/plugins/locate.md
|
||||
- Navigation Mixin: extend/plugins/navigation.md
|
||||
|
@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile docs/requirements.in -o docs/requirements.txt
|
||||
# uv pip compile docs/requirements.in -o docs/requirements.txt --no-strip-extras --generate-hashes
|
||||
anyio==4.3.0 \
|
||||
--hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \
|
||||
--hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6
|
||||
@ -284,6 +284,7 @@ mkdocs==1.6.0 \
|
||||
--hash=sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7 \
|
||||
--hash=sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512
|
||||
# via
|
||||
# -r docs/requirements.in
|
||||
# mkdocs-autorefs
|
||||
# mkdocs-git-revision-date-localized-plugin
|
||||
# mkdocs-include-markdown-plugin
|
||||
@ -303,15 +304,19 @@ mkdocs-get-deps==0.2.0 \
|
||||
mkdocs-git-revision-date-localized-plugin==1.2.5 \
|
||||
--hash=sha256:0c439816d9d0dba48e027d9d074b2b9f1d7cd179f74ba46b51e4da7bb3dc4b9b \
|
||||
--hash=sha256:d796a18b07cfcdb154c133e3ec099d2bb5f38389e4fd54d3eb516a8a736815b8
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-include-markdown-plugin==6.0.6 \
|
||||
--hash=sha256:7c80258b2928563c75cc057a7b9a0014701c40804b1b6aa290f3b4032518b43c \
|
||||
--hash=sha256:7ccafbaa412c1e5d3510c4aff46d1fe64c7a810c01dace4c636253d1aa5bc193
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-macros-plugin==1.0.5 \
|
||||
--hash=sha256:f60e26f711f5a830ddf1e7980865bf5c0f1180db56109803cdd280073c1a050a \
|
||||
--hash=sha256:fe348d75f01c911f362b6d998c57b3d85b505876dde69db924f2c512c395c328
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material==9.5.24 \
|
||||
--hash=sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34 \
|
||||
--hash=sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material-extensions==1.3.1 \
|
||||
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
|
||||
--hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
|
||||
@ -319,10 +324,13 @@ mkdocs-material-extensions==1.3.1 \
|
||||
mkdocs-simple-hooks==0.1.5 \
|
||||
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
|
||||
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
|
||||
# via -r docs/requirements.in
|
||||
mkdocstrings[python]==0.25.1 \
|
||||
--hash=sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf \
|
||||
--hash=sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51
|
||||
# via mkdocstrings-python
|
||||
# via
|
||||
# -r docs/requirements.in
|
||||
# mkdocstrings-python
|
||||
mkdocstrings-python==1.10.2 \
|
||||
--hash=sha256:38a4fd41953defb458a107033440c229c7e9f98f35a24e84d888789c97da5a63 \
|
||||
--hash=sha256:e8e596b37f45c09b67bec253e035fe18988af5bbbbf44e0ccd711742eed750e5
|
||||
@ -330,6 +338,7 @@ mkdocstrings-python==1.10.2 \
|
||||
neoteroi-mkdocs==1.0.5 \
|
||||
--hash=sha256:1f3b372dee79269157361733c0f45b3a89189077078e0e3224d829a144ef3579 \
|
||||
--hash=sha256:29875ef444b08aec5619a384142e16f1b4e851465cab4e380fb2b8ae730fe046
|
||||
# via -r docs/requirements.in
|
||||
packaging==24.0 \
|
||||
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
|
||||
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
|
||||
|
@ -16,7 +16,7 @@ exclude = [
|
||||
src = ["src/backend/InvenTree"]
|
||||
# line-length = 120
|
||||
|
||||
[tool.ruff.extend-per-file-ignores]
|
||||
[tool.ruff.lint.extend-per-file-ignores]
|
||||
"__init__.py" = ["D104"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
@ -1,12 +1,39 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 220
|
||||
INVENTREE_API_VERSION = 228
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
v228 - 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
|
||||
- Adds "icon" field to the PartCategory.path and StockLocation.path API
|
||||
- Adds icon packages API endpoint
|
||||
|
||||
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
|
||||
- Adds endpoints to list and revoke the tokens issued to the current user
|
||||
|
||||
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
|
||||
- Adds barcode generation API endpoint
|
||||
|
||||
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
|
||||
- Adds "filters" field to DataImportSession API
|
||||
|
||||
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
|
||||
- Add notes field to ManufacturerPart and SupplierPart API endpoints
|
||||
|
||||
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
|
||||
- Allow adjustment of "packaging" field when receiving items against a purchase order
|
||||
|
||||
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
|
||||
- Adjust the BomItem API endpoint to improve data import process
|
||||
|
||||
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
|
||||
- Adds missing fields from StockItemBriefSerializer
|
||||
- Adds missing fields from PartBriefSerializer
|
||||
- Adds extra exportable fields to BuildItemSerializer
|
||||
|
||||
v220 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
|
||||
- Adds "revision_of" field to Part serializer
|
||||
- Adds new API filters for "revision" status
|
||||
|
@ -15,6 +15,7 @@ from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth_2fa.adapter import OTPAdapter
|
||||
from allauth_2fa.forms import TOTPDeviceForm
|
||||
from allauth_2fa.utils import user_has_valid_totp_device
|
||||
from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText
|
||||
from crispy_forms.helper import FormHelper
|
||||
@ -211,6 +212,16 @@ class CustomSignupForm(SignupForm):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class CustomTOTPDeviceForm(TOTPDeviceForm):
|
||||
"""Ensure that db registration is enabled."""
|
||||
|
||||
def __init__(self, user, metadata=None, **kwargs):
|
||||
"""Override to check if registration is open."""
|
||||
if not settings.MFA_ENABLED:
|
||||
raise forms.ValidationError(_('MFA Registration is disabled.'))
|
||||
super().__init__(user, metadata, **kwargs)
|
||||
|
||||
|
||||
def registration_enabled():
|
||||
"""Determine whether user registration is enabled."""
|
||||
if get_global_setting('LOGIN_ENABLE_REG') or InvenTree.sso.registration_enabled():
|
||||
|
@ -396,38 +396,6 @@ def WrapWithQuotes(text, quote='"'):
|
||||
return text
|
||||
|
||||
|
||||
def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
|
||||
"""Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||
|
||||
Args:
|
||||
cls_name: string describing the object type e.g. 'StockItem'
|
||||
object_pk (int): ID (Primary Key) of the object in the database
|
||||
object_data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
|
||||
|
||||
Returns:
|
||||
json string of the supplied data plus some other data
|
||||
"""
|
||||
if object_data is None:
|
||||
object_data = {}
|
||||
|
||||
brief = kwargs.get('brief', True)
|
||||
|
||||
data = {}
|
||||
|
||||
if brief:
|
||||
data[cls_name] = object_pk
|
||||
else:
|
||||
data['tool'] = 'InvenTree'
|
||||
data['version'] = InvenTree.version.inventreeVersion()
|
||||
data['instance'] = InvenTree.version.inventreeInstanceName()
|
||||
|
||||
# Ensure PK is included
|
||||
object_data['id'] = object_pk
|
||||
data[cls_name] = object_data
|
||||
|
||||
return str(json.dumps(data, sort_keys=True))
|
||||
|
||||
|
||||
def GetExportFormats():
|
||||
"""Return a list of allowable file formats for importing or exporting tabular data."""
|
||||
return ['csv', 'xlsx', 'tsv', 'json']
|
||||
|
@ -15,9 +15,6 @@ from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
import InvenTree
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.version
|
||||
from common.notifications import (
|
||||
InvenTreeNotificationBodies,
|
||||
NotificationBody,
|
||||
@ -331,9 +328,7 @@ def notify_users(
|
||||
'instance': instance,
|
||||
'name': content.name.format(**content_context),
|
||||
'message': content.message.format(**content_context),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
instance.get_absolute_url()
|
||||
),
|
||||
'link': construct_absolute_url(instance.get_absolute_url()),
|
||||
'template': {'subject': content.name.format(**content_context)},
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ LOCALES = [
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('et', _('Estonian')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fi', _('Finnish')),
|
||||
('fr', _('French')),
|
||||
|
@ -0,0 +1,192 @@
|
||||
"""Custom management command to migrate the old FontAwesome icons."""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import models
|
||||
|
||||
from common.icons import validate_icon
|
||||
from part.models import PartCategory
|
||||
from stock.models import StockLocation, StockLocationType
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Generate an icon map from the FontAwesome library to the new icon library."""
|
||||
|
||||
help = """Helper command to migrate the old FontAwesome icons to the new icon library."""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add the arguments."""
|
||||
parser.add_argument(
|
||||
'--output-file',
|
||||
type=str,
|
||||
help='Path to file to write generated icon map to',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--input-file', type=str, help='Path to file to read icon map from'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--include-items',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Include referenced inventree items in the output icon map (optional)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--import-now',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='CAUTION: If this flag is set, the icon map will be imported and the database will be touched',
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Generate an icon map from the FontAwesome library to the new icon library."""
|
||||
# Check for invalid combinations of arguments
|
||||
if kwargs['output_file'] and kwargs['input_file']:
|
||||
raise CommandError('Cannot specify both --input-file and --output-file')
|
||||
|
||||
if not kwargs['output_file'] and not kwargs['input_file']:
|
||||
raise CommandError('Must specify either --input-file or --output-file')
|
||||
|
||||
if kwargs['include_items'] and not kwargs['output_file']:
|
||||
raise CommandError(
|
||||
'--include-items can only be used with an --output-file specified'
|
||||
)
|
||||
|
||||
if kwargs['output_file'] and kwargs['import_now']:
|
||||
raise CommandError(
|
||||
'--import-now can only be used with an --input-file specified'
|
||||
)
|
||||
|
||||
ICON_MODELS = [
|
||||
(StockLocation, 'custom_icon'),
|
||||
(StockLocationType, 'icon'),
|
||||
(PartCategory, '_icon'),
|
||||
]
|
||||
|
||||
def get_model_items_with_icons(model: models.Model, icon_field: str):
|
||||
"""Return a list of models with icon fields."""
|
||||
return model.objects.exclude(**{f'{icon_field}__isnull': True}).exclude(**{
|
||||
f'{icon_field}__exact': ''
|
||||
})
|
||||
|
||||
# Generate output icon map file
|
||||
if kwargs['output_file']:
|
||||
icons = {}
|
||||
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if icon not in icons:
|
||||
icons[icon] = {
|
||||
**({'items': []} if kwargs['include_items'] else {}),
|
||||
'new_icon': '',
|
||||
}
|
||||
|
||||
if kwargs['include_items']:
|
||||
icons[icon]['items'].append({
|
||||
'model': model.__name__.lower(),
|
||||
'id': item.id, # type: ignore
|
||||
})
|
||||
|
||||
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')
|
||||
with open(kwargs['output_file'], 'w') as f:
|
||||
json.dump(icons, f, indent=2)
|
||||
|
||||
self.stdout.write(f'Icon map written to {kwargs["output_file"]}')
|
||||
|
||||
# Import icon map file
|
||||
if kwargs['input_file']:
|
||||
with open(kwargs['input_file'], 'r') as f:
|
||||
icons = json.load(f)
|
||||
|
||||
self.stdout.write(f'Loaded icon map for {len(icons.keys())} icons')
|
||||
|
||||
self.stdout.write('Verifying icon map')
|
||||
has_errors = False
|
||||
|
||||
# Verify that all new icons are valid icons
|
||||
for old_icon, data in icons.items():
|
||||
try:
|
||||
validate_icon(data.get('new_icon', ''))
|
||||
except ValidationError:
|
||||
self.stdout.write(
|
||||
f'[ERR] Invalid icon: "{old_icon}" -> "{data.get("new_icon", "")}'
|
||||
)
|
||||
has_errors = True
|
||||
|
||||
# Verify that all required items are provided in the icon map
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
if icon not in icons:
|
||||
self.stdout.write(
|
||||
f' [ERR] Icon "{icon}" not found in icon map'
|
||||
)
|
||||
has_errors = True
|
||||
|
||||
# If there are errors, stop here
|
||||
if has_errors:
|
||||
self.stdout.write(
|
||||
'[ERR] Icon map has errors, please fix them before continuing with importing'
|
||||
)
|
||||
return
|
||||
|
||||
# Import the icon map into the database if the flag is set
|
||||
if kwargs['import_now']:
|
||||
self.stdout.write('Start importing icons and updating database...')
|
||||
cnt = 0
|
||||
|
||||
for model, icon_name in ICON_MODELS:
|
||||
self.stdout.write(
|
||||
f'Processing model {model.__name__} with icon field {icon_name}'
|
||||
)
|
||||
items = get_model_items_with_icons(model, icon_name)
|
||||
|
||||
for item in items:
|
||||
icon = getattr(item, icon_name)
|
||||
|
||||
try:
|
||||
validate_icon(icon)
|
||||
continue # Skip if the icon is already valid
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
setattr(item, icon_name, icons[icon]['new_icon'])
|
||||
cnt += 1
|
||||
item.save()
|
||||
|
||||
self.stdout.write(
|
||||
f'Icon map successfully imported - changed {cnt} items'
|
||||
)
|
||||
self.stdout.write('Icons are now migrated')
|
||||
else:
|
||||
self.stdout.write('Icon map is valid and ready to be imported')
|
||||
self.stdout.write(
|
||||
'Run the command with --import-now to import the icon map and update the database'
|
||||
)
|
@ -3,9 +3,7 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
@ -577,6 +575,9 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
# e.g. for StockLocation, this value is 'location'
|
||||
ITEM_PARENT_KEY = None
|
||||
|
||||
# Extra fields to include in the get_path result. E.g. icon
|
||||
EXTRA_PATH_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines extra model properties."""
|
||||
|
||||
@ -870,7 +871,14 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
|
||||
name: <name>,
|
||||
}
|
||||
"""
|
||||
return [{'pk': item.pk, 'name': item.name} for item in self.path]
|
||||
return [
|
||||
{
|
||||
'pk': item.pk,
|
||||
'name': item.name,
|
||||
**{k: getattr(item, k, None) for k in self.EXTRA_PATH_FIELDS},
|
||||
}
|
||||
for item in self.path
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""String representation of a category is the full path to that category."""
|
||||
@ -934,6 +942,8 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
|
||||
- barcode_data : Raw data associated with an assigned barcode
|
||||
- barcode_hash : A 'hash' of the assigned barcode data used to improve matching
|
||||
|
||||
The barcode_model_type_code() classmethod must be implemented in the model class.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@ -964,11 +974,25 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
# By default, use the name of the class
|
||||
return cls.__name__.lower()
|
||||
|
||||
@classmethod
|
||||
def barcode_model_type_code(cls):
|
||||
r"""Return a 'short' code for the model type.
|
||||
|
||||
This is used to generate a efficient QR code for the model type.
|
||||
It is expected to match this pattern: [0-9A-Z $%*+-.\/:]{2}
|
||||
|
||||
Note: Due to the shape constrains (45**2=2025 different allowed codes)
|
||||
this needs to be explicitly implemented in the model class to avoid collisions.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'barcode_model_type_code() must be implemented in the model class'
|
||||
)
|
||||
|
||||
def format_barcode(self, **kwargs):
|
||||
"""Return a JSON string for formatting a QR code for this model instance."""
|
||||
return InvenTree.helpers.MakeBarcode(
|
||||
self.__class__.barcode_model_type(), self.pk, **kwargs
|
||||
)
|
||||
from plugin.base.barcodes.helper import generate_barcode
|
||||
|
||||
return generate_barcode(self)
|
||||
|
||||
def format_matched_response(self):
|
||||
"""Format a standard response for a matched barcode."""
|
||||
@ -986,7 +1010,7 @@ class InvenTreeBarcodeMixin(models.Model):
|
||||
@property
|
||||
def barcode(self):
|
||||
"""Format a minimal barcode string (e.g. for label printing)."""
|
||||
return self.format_barcode(brief=True)
|
||||
return self.format_barcode()
|
||||
|
||||
@classmethod
|
||||
def lookup_barcode(cls, barcode_hash):
|
||||
|
@ -115,6 +115,7 @@ def canAppAccessDatabase(
|
||||
'makemessages',
|
||||
'compilemessages',
|
||||
'spectactular',
|
||||
'collectstatic',
|
||||
]
|
||||
|
||||
if not allow_shell:
|
||||
@ -125,7 +126,7 @@ def canAppAccessDatabase(
|
||||
excluded_commands.append('test')
|
||||
|
||||
if not allow_plugins:
|
||||
excluded_commands.extend(['collectstatic', 'collectplugins'])
|
||||
excluded_commands.extend(['collectplugins'])
|
||||
|
||||
for cmd in excluded_commands:
|
||||
if cmd in sys.argv:
|
||||
|
@ -1210,6 +1210,9 @@ ACCOUNT_FORMS = {
|
||||
'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
|
||||
'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
|
||||
}
|
||||
ALLAUTH_2FA_FORMS = {'setup': 'InvenTree.forms.CustomTOTPDeviceForm'}
|
||||
# Determine if multi-factor authentication is enabled for this server (default = True)
|
||||
MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True)
|
||||
|
||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
|
||||
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
|
||||
|
@ -3,6 +3,7 @@
|
||||
import logging
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import NoReverseMatch, include, path, reverse
|
||||
|
||||
from allauth.account.models import EmailAddress
|
||||
@ -177,7 +178,9 @@ class SocialProviderListView(ListAPI):
|
||||
data = {
|
||||
'sso_enabled': InvenTree.sso.login_enabled(),
|
||||
'sso_registration': InvenTree.sso.registration_enabled(),
|
||||
'mfa_required': get_global_setting('LOGIN_ENFORCE_MFA'),
|
||||
'mfa_required': settings.MFA_ENABLED
|
||||
and get_global_setting('LOGIN_ENFORCE_MFA'),
|
||||
'mfa_enabled': settings.MFA_ENABLED,
|
||||
'providers': provider_list,
|
||||
'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'),
|
||||
'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'),
|
||||
|
@ -1101,3 +1101,14 @@ a {
|
||||
.large-treeview-icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.api-icon {
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
/* Better font rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
21
src/backend/InvenTree/InvenTree/static/tabler-icons/LICENSE
Normal file
21
src/backend/InvenTree/InvenTree/static/tabler-icons/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2024 Paweł Kuna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -62,6 +62,7 @@ class APITests(InvenTreeAPITestCase):
|
||||
"""Tests for the InvenTree API."""
|
||||
|
||||
fixtures = ['location', 'category', 'part', 'stock']
|
||||
roles = ['part.view']
|
||||
token = None
|
||||
auto_login = False
|
||||
|
||||
@ -132,6 +133,7 @@ class APITests(InvenTreeAPITestCase):
|
||||
|
||||
# Now log in!
|
||||
self.basicAuth()
|
||||
self.assignRole('part.view')
|
||||
|
||||
response = self.get(url)
|
||||
|
||||
@ -147,12 +149,17 @@ class APITests(InvenTreeAPITestCase):
|
||||
|
||||
role_names = roles.keys()
|
||||
|
||||
# By default, 'view' permissions are provided
|
||||
# By default, no permissions are provided
|
||||
for rule in RuleSet.RULESET_NAMES:
|
||||
self.assertIn(rule, role_names)
|
||||
|
||||
self.assertIn('view', roles[rule])
|
||||
if roles[rule] is None:
|
||||
continue
|
||||
|
||||
if rule == 'part':
|
||||
self.assertIn('view', roles[rule])
|
||||
else:
|
||||
self.assertNotIn('view', roles[rule])
|
||||
self.assertNotIn('add', roles[rule])
|
||||
self.assertNotIn('change', roles[rule])
|
||||
self.assertNotIn('delete', roles[rule])
|
||||
@ -297,6 +304,7 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
'order',
|
||||
'sales_order',
|
||||
]
|
||||
roles = ['build.view', 'part.view']
|
||||
|
||||
def test_empty(self):
|
||||
"""Test empty request."""
|
||||
@ -331,6 +339,19 @@ class SearchTests(InvenTreeAPITestCase):
|
||||
{'search': '01', 'limit': 2, 'purchaseorder': {}, 'salesorder': {}},
|
||||
expected_code=200,
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['purchaseorder'],
|
||||
{'error': 'User does not have permission to view this model'},
|
||||
)
|
||||
|
||||
# Add permissions and try again
|
||||
self.assignRole('purchase_order.view')
|
||||
self.assignRole('sales_order.view')
|
||||
response = self.post(
|
||||
reverse('api-search'),
|
||||
{'search': '01', 'limit': 2, 'purchaseorder': {}, 'salesorder': {}},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
self.assertEqual(response.data['purchaseorder']['count'], 1)
|
||||
self.assertEqual(response.data['salesorder']['count'], 0)
|
||||
|
@ -71,6 +71,7 @@ class MiddlewareTests(InvenTreeTestCase):
|
||||
|
||||
def test_error_exceptions(self):
|
||||
"""Test that ignored errors are not logged."""
|
||||
self.assignRole('part.view')
|
||||
|
||||
def check(excpected_nbr=0):
|
||||
# Check that errors are empty
|
||||
|
@ -789,33 +789,6 @@ class TestIncrement(TestCase):
|
||||
self.assertEqual(result, b)
|
||||
|
||||
|
||||
class TestMakeBarcode(TestCase):
|
||||
"""Tests for barcode string creation."""
|
||||
|
||||
def test_barcode_extended(self):
|
||||
"""Test creation of barcode with extended data."""
|
||||
bc = helpers.MakeBarcode(
|
||||
'part', 3, {'id': 3, 'url': 'www.google.com'}, brief=False
|
||||
)
|
||||
|
||||
self.assertIn('part', bc)
|
||||
self.assertIn('tool', bc)
|
||||
self.assertIn('"tool": "InvenTree"', bc)
|
||||
|
||||
data = json.loads(bc)
|
||||
|
||||
self.assertEqual(data['part']['id'], 3)
|
||||
self.assertEqual(data['part']['url'], 'www.google.com')
|
||||
|
||||
def test_barcode_brief(self):
|
||||
"""Test creation of simple barcode."""
|
||||
bc = helpers.MakeBarcode('stockitem', 7)
|
||||
|
||||
data = json.loads(bc)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data['stockitem'], 7)
|
||||
|
||||
|
||||
class TestDownloadFile(TestCase):
|
||||
"""Tests for DownloadFile."""
|
||||
|
||||
|
@ -204,6 +204,7 @@ class UserMixin:
|
||||
ruleset.can_add = True
|
||||
|
||||
ruleset.save()
|
||||
if not assign_all:
|
||||
break
|
||||
|
||||
|
||||
|
@ -115,11 +115,18 @@ class Build(
|
||||
|
||||
return defaults
|
||||
|
||||
@classmethod
|
||||
def barcode_model_type_code(cls):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return "BO"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method for the BuildOrder model"""
|
||||
self.validate_reference_field(self.reference)
|
||||
self.reference_int = self.rebuild_reference_field(self.reference)
|
||||
|
||||
# Check part when initially creating the build order
|
||||
if not self.pk or self.has_field_changed('part'):
|
||||
if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'):
|
||||
# Check that the BOM is valid
|
||||
if not self.part.is_bom_valid():
|
||||
|
@ -1072,6 +1072,8 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
'part_name',
|
||||
'part_ipn',
|
||||
'available_quantity',
|
||||
'item_batch_code',
|
||||
'item_serial',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
@ -1103,6 +1105,8 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
'part_name',
|
||||
'part_ipn',
|
||||
'available_quantity',
|
||||
'item_batch_code',
|
||||
'item_serial_number',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1138,6 +1142,9 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
||||
part_name = serializers.CharField(source='stock_item.part.name', label=_('Part Name'), read_only=True)
|
||||
part_ipn = serializers.CharField(source='stock_item.part.IPN', label=_('Part IPN'), read_only=True)
|
||||
|
||||
item_batch_code = serializers.CharField(source='stock_item.batch', label=_('Batch Code'), read_only=True)
|
||||
item_serial_number = serializers.CharField(source='stock_item.serial', label=_('Serial Number'), read_only=True)
|
||||
|
||||
# Annotated fields
|
||||
build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True)
|
||||
|
||||
|
@ -277,7 +277,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
$('#show-qr-code').click(function() {
|
||||
showQRDialog(
|
||||
'{% trans "Build Order QR Code" escape %}',
|
||||
'{"build": {{ build.pk }} }'
|
||||
'{{ build.barcode }}'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1015,7 +1015,7 @@ class BuildOverallocationTest(BuildAPITest):
|
||||
'accept_overallocated': 'trim',
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=550, # TODO: Come back and refactor this
|
||||
max_query_count=555, # TODO: Come back and refactor this
|
||||
)
|
||||
|
||||
self.build.refresh_from_db()
|
||||
|
@ -9,6 +9,7 @@ from django.http.response import HttpResponse
|
||||
from django.urls import include, path, re_path
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import django_q.models
|
||||
@ -25,6 +26,7 @@ from rest_framework.views import APIView
|
||||
|
||||
import common.models
|
||||
import common.serializers
|
||||
from common.icons import get_icon_packs
|
||||
from common.settings import get_global_setting
|
||||
from generic.states.api import AllStatusViews, StatusView
|
||||
from importer.mixins import DataExportViewMixin
|
||||
@ -743,6 +745,18 @@ class AttachmentDetail(RetrieveUpdateDestroyAPI):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@method_decorator(cache_control(public=True, max_age=86400), name='dispatch')
|
||||
class IconList(ListAPI):
|
||||
"""List view for available icon packages."""
|
||||
|
||||
serializer_class = common.serializers.IconPackageSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return a list of all available icon packages."""
|
||||
return get_icon_packs().values()
|
||||
|
||||
|
||||
settings_api_urls = [
|
||||
# User settings
|
||||
path(
|
||||
@ -957,6 +971,8 @@ common_api_urls = [
|
||||
path('', ContentTypeList.as_view(), name='api-contenttype-list'),
|
||||
]),
|
||||
),
|
||||
# Icons
|
||||
path('icons/', IconList.as_view(), name='api-icon-list'),
|
||||
]
|
||||
|
||||
admin_api_urls = [
|
||||
|
114
src/backend/InvenTree/common/icons.py
Normal file
114
src/backend/InvenTree/common/icons.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Icon utilities for InvenTree."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TypedDict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.templatetags.static import static
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
_icon_packs = None
|
||||
|
||||
|
||||
class Icon(TypedDict):
|
||||
"""Dict type for an icon.
|
||||
|
||||
Attributes:
|
||||
name: The name of the icon.
|
||||
category: The category of the icon.
|
||||
tags: A list of tags for the icon (used for search).
|
||||
variants: A dictionary of variants for the icon, where the key is the variant name and the value is the variant's unicode hex character.
|
||||
"""
|
||||
|
||||
name: str
|
||||
category: str
|
||||
tags: list[str]
|
||||
variants: dict[str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class IconPack:
|
||||
"""Dataclass for an icon pack.
|
||||
|
||||
Attributes:
|
||||
name: The name of the icon pack.
|
||||
prefix: The prefix used for the icon pack.
|
||||
fonts: A dictionary of different font file formats for the icon pack, where the key is the css format and the value a path to the font file.
|
||||
icons: A dictionary of icons in the icon pack, where the key is the icon name and the value is a dictionary of the icon's variants.
|
||||
"""
|
||||
|
||||
name: str
|
||||
prefix: str
|
||||
fonts: dict[str, str]
|
||||
icons: dict[str, Icon]
|
||||
|
||||
|
||||
def get_icon_packs():
|
||||
"""Return a dictionary of available icon packs including their icons."""
|
||||
global _icon_packs
|
||||
|
||||
if _icon_packs is None:
|
||||
tabler_icons_path = Path(__file__).parent.parent.joinpath(
|
||||
'InvenTree/static/tabler-icons/icons.json'
|
||||
)
|
||||
with open(tabler_icons_path, 'r') as tabler_icons_file:
|
||||
tabler_icons = json.load(tabler_icons_file)
|
||||
|
||||
icon_packs = [
|
||||
IconPack(
|
||||
name='Tabler Icons',
|
||||
prefix='ti',
|
||||
fonts={
|
||||
'woff2': static('tabler-icons/tabler-icons.woff2'),
|
||||
'woff': static('tabler-icons/tabler-icons.woff'),
|
||||
'truetype': static('tabler-icons/tabler-icons.ttf'),
|
||||
},
|
||||
icons=tabler_icons,
|
||||
)
|
||||
]
|
||||
|
||||
from plugin import registry
|
||||
|
||||
for plugin in registry.with_mixin('icon_pack', active=True):
|
||||
try:
|
||||
icon_packs.extend(plugin.icon_packs())
|
||||
except Exception as e:
|
||||
logger.warning('Error loading icon pack from plugin %s: %s', plugin, e)
|
||||
|
||||
_icon_packs = {pack.prefix: pack for pack in icon_packs}
|
||||
|
||||
return _icon_packs
|
||||
|
||||
|
||||
def reload_icon_packs():
|
||||
"""Reload the icon packs."""
|
||||
global _icon_packs
|
||||
_icon_packs = None
|
||||
get_icon_packs()
|
||||
|
||||
|
||||
def validate_icon(icon: str):
|
||||
"""Validate an icon string in the format pack:name:variant."""
|
||||
try:
|
||||
pack, name, variant = icon.split(':')
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
f'Invalid icon format: {icon}, expected: pack:name:variant'
|
||||
)
|
||||
|
||||
packs = get_icon_packs()
|
||||
|
||||
if pack not in packs:
|
||||
raise ValidationError(f'Invalid icon pack: {pack}')
|
||||
|
||||
if name not in packs[pack].icons:
|
||||
raise ValidationError(f'Invalid icon name: {name}')
|
||||
|
||||
if variant not in packs[pack].icons[name]['variants']:
|
||||
raise ValidationError(f'Invalid icon variant: {variant}')
|
||||
|
||||
return packs[pack], packs[pack].icons[name], variant
|
@ -9,12 +9,13 @@ import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import timedelta, timezone
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from secrets import compare_digest
|
||||
from typing import Any, Callable, Collection, TypedDict, Union
|
||||
from typing import Any, Callable, TypedDict, Union
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings as django_settings
|
||||
@ -49,6 +50,7 @@ import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import InvenTree.validators
|
||||
import order.validators
|
||||
import plugin.base.barcodes.helper
|
||||
import report.helpers
|
||||
import users.models
|
||||
from InvenTree.sanitizer import sanitize_svg
|
||||
@ -56,6 +58,17 @@ from plugin import registry
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import NotRequired
|
||||
else:
|
||||
|
||||
class NotRequired: # pragma: no cover
|
||||
"""NotRequired type helper is only supported with Python 3.11+."""
|
||||
|
||||
def __class_getitem__(cls, item):
|
||||
"""Return the item."""
|
||||
return item
|
||||
|
||||
|
||||
class MetaMixin(models.Model):
|
||||
"""A base class for InvenTree models to include shared meta fields.
|
||||
@ -1167,7 +1180,7 @@ class InvenTreeSettingsKeyType(SettingsKeyType):
|
||||
requires_restart: If True, a server restart is required after changing the setting
|
||||
"""
|
||||
|
||||
requires_restart: bool
|
||||
requires_restart: NotRequired[bool]
|
||||
|
||||
|
||||
class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
@ -1402,6 +1415,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'BARCODE_GENERATION_PLUGIN': {
|
||||
'name': _('Barcode Generation Plugin'),
|
||||
'description': _('Plugin to use for internal barcode data generation'),
|
||||
'choices': plugin.base.barcodes.helper.barcode_plugins,
|
||||
'default': 'inventreebarcode',
|
||||
},
|
||||
'PART_ENABLE_REVISION': {
|
||||
'name': _('Part Revisions'),
|
||||
'description': _('Enable revision field for Part'),
|
||||
@ -1539,6 +1558,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'name': _('Part Category Default Icon'),
|
||||
'description': _('Part category default icon (empty means no icon)'),
|
||||
'default': '',
|
||||
'validator': common.validators.validate_icon,
|
||||
},
|
||||
'PART_PARAMETER_ENFORCE_UNITS': {
|
||||
'name': _('Enforce Parameter Units'),
|
||||
@ -1760,6 +1780,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'name': _('Stock Location Default Icon'),
|
||||
'description': _('Stock location default icon (empty means no icon)'),
|
||||
'default': '',
|
||||
'validator': common.validators.validate_icon,
|
||||
},
|
||||
'STOCK_SHOW_INSTALLED_ITEMS': {
|
||||
'name': _('Show Installed Stock Items'),
|
||||
@ -2589,14 +2610,22 @@ class ColorTheme(models.Model):
|
||||
@classmethod
|
||||
def get_color_themes_choices(cls):
|
||||
"""Get all color themes from static folder."""
|
||||
if not django_settings.STATIC_COLOR_THEMES_DIR.exists():
|
||||
logger.error('Theme directory does not exist')
|
||||
color_theme_dir = (
|
||||
django_settings.STATIC_COLOR_THEMES_DIR
|
||||
if django_settings.STATIC_COLOR_THEMES_DIR.exists()
|
||||
else django_settings.BASE_DIR.joinpath(
|
||||
'InvenTree', 'static', 'css', 'color-themes'
|
||||
)
|
||||
)
|
||||
|
||||
if not color_theme_dir.exists():
|
||||
logger.error(f'Theme directory "{color_theme_dir}" does not exist')
|
||||
return []
|
||||
|
||||
# Get files list from css/color-themes/ folder
|
||||
files_list = []
|
||||
|
||||
for file in django_settings.STATIC_COLOR_THEMES_DIR.iterdir():
|
||||
for file in color_theme_dir.iterdir():
|
||||
files_list.append([file.stem, file.suffix])
|
||||
|
||||
# Get color themes choices (CSS sheets)
|
||||
|
@ -565,3 +565,21 @@ class AttachmentSerializer(InvenTreeModelSerializer):
|
||||
)
|
||||
|
||||
return super().save()
|
||||
|
||||
|
||||
class IconSerializer(serializers.Serializer):
|
||||
"""Serializer for an icon."""
|
||||
|
||||
name = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
tags = serializers.ListField(child=serializers.CharField())
|
||||
variants = serializers.DictField(child=serializers.CharField())
|
||||
|
||||
|
||||
class IconPackageSerializer(serializers.Serializer):
|
||||
"""Serializer for a list of icons."""
|
||||
|
||||
name = serializers.CharField()
|
||||
prefix = serializers.CharField()
|
||||
fonts = serializers.DictField(child=serializers.CharField())
|
||||
icons = serializers.DictField(child=IconSerializer())
|
||||
|
@ -20,6 +20,7 @@ from django.urls import reverse
|
||||
|
||||
import PIL
|
||||
|
||||
import common.validators
|
||||
from common.settings import get_global_setting, set_global_setting
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase, PluginMixin
|
||||
@ -1199,20 +1200,10 @@ class ColorThemeTest(TestCase):
|
||||
def test_choices(self):
|
||||
"""Test that default choices are returned."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip due to directories not being set up
|
||||
if not result:
|
||||
return # pragma: no cover
|
||||
self.assertIn(('default', 'Default'), result)
|
||||
|
||||
def test_valid_choice(self):
|
||||
"""Check that is_valid_choice works correctly."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip due to directories not being set up
|
||||
if not result:
|
||||
return # pragma: no cover
|
||||
|
||||
# check wrong reference
|
||||
self.assertFalse(ColorTheme.is_valid_choice('abcdd'))
|
||||
|
||||
@ -1534,3 +1525,44 @@ class ContentTypeAPITest(InvenTreeAPITestCase):
|
||||
reverse('api-contenttype-detail-modelname', kwargs={'model': None}),
|
||||
expected_code=404,
|
||||
)
|
||||
|
||||
|
||||
class IconAPITest(InvenTreeAPITestCase):
|
||||
"""Unit tests for the Icons API."""
|
||||
|
||||
def test_list(self):
|
||||
"""Test API list functionality."""
|
||||
response = self.get(reverse('api-icon-list'), expected_code=200)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
|
||||
self.assertEqual(response.data[0]['prefix'], 'ti')
|
||||
self.assertEqual(response.data[0]['name'], 'Tabler Icons')
|
||||
for font_format in ['woff2', 'woff', 'truetype']:
|
||||
self.assertIn(font_format, response.data[0]['fonts'])
|
||||
|
||||
self.assertGreater(len(response.data[0]['icons']), 1000)
|
||||
|
||||
|
||||
class ValidatorsTest(TestCase):
|
||||
"""Unit tests for the custom validators."""
|
||||
|
||||
def test_validate_icon(self):
|
||||
"""Test the validate_icon function."""
|
||||
common.validators.validate_icon('')
|
||||
common.validators.validate_icon(None)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
common.validators.validate_icon('invalid')
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
common.validators.validate_icon('my:package:non-existing')
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
common.validators.validate_icon(
|
||||
'ti:my-non-existing-icon:non-existing-variant'
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
common.validators.validate_icon('ti:package:non-existing-variant')
|
||||
|
||||
common.validators.validate_icon('ti:package:outline')
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Validation helpers for common models."""
|
||||
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import common.icons
|
||||
from common.settings import get_global_setting
|
||||
|
||||
|
||||
@ -103,3 +105,11 @@ def validate_email_domains(setting):
|
||||
raise ValidationError(_('An empty domain is not allowed.'))
|
||||
if not re.match(r'^@[a-zA-Z0-9\.\-_]+$', domain):
|
||||
raise ValidationError(_(f'Invalid domain name: {domain}'))
|
||||
|
||||
|
||||
def validate_icon(name: Union[str, None]):
|
||||
"""Validate the provided icon name, and ignore if empty."""
|
||||
if name == '' or name is None:
|
||||
return
|
||||
|
||||
common.icons.validate_icon(name)
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.11 on 2024-07-16 12:58
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0070_remove_manufacturerpartattachment_manufacturer_part_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='manufacturerpart',
|
||||
name='notes',
|
||||
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supplierpart',
|
||||
name='notes',
|
||||
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
|
||||
),
|
||||
]
|
@ -451,6 +451,7 @@ class Address(InvenTree.models.InvenTreeModel):
|
||||
class ManufacturerPart(
|
||||
InvenTree.models.InvenTreeAttachmentMixin,
|
||||
InvenTree.models.InvenTreeBarcodeMixin,
|
||||
InvenTree.models.InvenTreeNotesMixin,
|
||||
InvenTree.models.InvenTreeMetadataModel,
|
||||
):
|
||||
"""Represents a unique part as provided by a Manufacturer Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) Each ManufacturerPart is also linked to a Part object. A Part may be available from multiple manufacturers.
|
||||
@ -474,6 +475,11 @@ class ManufacturerPart(
|
||||
"""Return the API URL associated with the ManufacturerPart instance."""
|
||||
return reverse('api-manufacturer-part-list')
|
||||
|
||||
@classmethod
|
||||
def barcode_model_type_code(cls):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'MP'
|
||||
|
||||
part = models.ForeignKey(
|
||||
'part.Part',
|
||||
on_delete=models.CASCADE,
|
||||
@ -624,6 +630,7 @@ class SupplierPartManager(models.Manager):
|
||||
class SupplierPart(
|
||||
InvenTree.models.MetadataMixin,
|
||||
InvenTree.models.InvenTreeBarcodeMixin,
|
||||
InvenTree.models.InvenTreeNotesMixin,
|
||||
common.models.MetaMixin,
|
||||
InvenTree.models.InvenTreeModel,
|
||||
):
|
||||
@ -676,6 +683,11 @@ class SupplierPart(
|
||||
"""Return custom API filters for this particular instance."""
|
||||
return {'manufacturer_part': {'part': self.part.pk}}
|
||||
|
||||
@classmethod
|
||||
def barcode_model_type_code(cls):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'SP'
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean action for the SupplierPart model.
|
||||
|
||||
|
@ -213,7 +213,7 @@ class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerialize
|
||||
|
||||
@register_importer()
|
||||
class ManufacturerPartSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
|
||||
):
|
||||
"""Serializer for ManufacturerPart object."""
|
||||
|
||||
@ -232,6 +232,7 @@ class ManufacturerPartSerializer(
|
||||
'MPN',
|
||||
'link',
|
||||
'barcode_hash',
|
||||
'notes',
|
||||
'tags',
|
||||
]
|
||||
|
||||
@ -305,7 +306,7 @@ class ManufacturerPartParameterSerializer(
|
||||
|
||||
@register_importer()
|
||||
class SupplierPartSerializer(
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer
|
||||
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
|
||||
):
|
||||
"""Serializer for SupplierPart object."""
|
||||
|
||||
@ -340,6 +341,7 @@ class SupplierPartSerializer(
|
||||
'supplier_detail',
|
||||
'url',
|
||||
'updated',
|
||||
'notes',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
@ -171,11 +171,40 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-manufacturer-part-notes'>
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Manufacturer Part Notes" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% include "notes_buttons.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<textarea id='manufacturer-part-notes'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
// Load the "notes" tab
|
||||
onPanelLoad('manufacturer-part-notes', function() {
|
||||
|
||||
setupNotesField(
|
||||
'manufacturer-part-notes',
|
||||
'{% url "api-manufacturer-part-detail" part.pk %}',
|
||||
{
|
||||
model_type: "manufacturerpart",
|
||||
model_id: {{ part.pk }},
|
||||
editable: {% js_bool roles.purchase_order.change %},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onPanelLoad("attachments", function() {
|
||||
loadAttachmentTable('manufacturerpart', {{ part.pk }});
|
||||
});
|
||||
|
@ -8,3 +8,5 @@
|
||||
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
||||
{% trans "Attachments" as text %}
|
||||
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
||||
{% trans "Notes" as text %}
|
||||
{% include "sidebar_item.html" with label="manufacturer-part-notes" text=text icon="fa-clipboard" %}
|
||||
|
@ -264,17 +264,46 @@ src="{% static 'img/blank_image.png' %}"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-hidden' id='panel-supplier-part-notes'>
|
||||
<div class='panel-heading'>
|
||||
<div class='d-flex flex-wrap'>
|
||||
<h4>{% trans "Supplier Part Notes" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% include "notes_buttons.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<textarea id='supplier-part-notes'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
// Load the "notes" tab
|
||||
onPanelLoad('supplier-part-notes', function() {
|
||||
|
||||
setupNotesField(
|
||||
'supplier-part-notes',
|
||||
'{% url "api-supplier-part-detail" part.pk %}',
|
||||
{
|
||||
model_type: "supplierpart",
|
||||
model_id: {{ part.pk }},
|
||||
editable: {% js_bool roles.purchase_order.change %},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% if barcodes %}
|
||||
|
||||
$("#show-qr-code").click(function() {
|
||||
showQRDialog(
|
||||
'{% trans "Supplier Part QR Code" escape %}',
|
||||
'{"supplierpart": {{ part.pk }} }'
|
||||
'{{ part.barcode }}'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -8,3 +8,5 @@
|
||||
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
|
||||
{% trans "Supplier Part Pricing" as text %}
|
||||
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}
|
||||
{% trans "Notes" as text %}
|
||||
{% include "sidebar_item.html" with label="supplier-part-notes" text=text icon="fa-clipboard" %}
|
||||
|
@ -160,7 +160,7 @@ class CompanyTest(InvenTreeAPITestCase):
|
||||
class ContactTest(InvenTreeAPITestCase):
|
||||
"""Tests for the Contact models."""
|
||||
|
||||
roles = []
|
||||
roles = ['purchase_order.view']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -266,7 +266,7 @@ class ContactTest(InvenTreeAPITestCase):
|
||||
class AddressTest(InvenTreeAPITestCase):
|
||||
"""Test cases for Address API endpoints."""
|
||||
|
||||
roles = []
|
||||
roles = ['purchase_order.view']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -11,6 +11,8 @@
|
||||
# Note: Database configuration options can also be specified from environmental variables,
|
||||
# with the prefix INVENTREE_DB_
|
||||
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
||||
# Do not change this section if you are using the package - use `inventree config` instead
|
||||
# TO MAINTAINERS: Do not change database strings
|
||||
database:
|
||||
# --- Available options: ---
|
||||
# ENGINE: Database engine. Selection from:
|
||||
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.14 on 2024-07-12 03:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import importer.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('importer', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dataimportsession',
|
||||
name='field_overrides',
|
||||
field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Overrides'),
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.14 on 2024-07-16 03:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import importer.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('importer', '0002_dataimportsession_field_overrides'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dataimportsession',
|
||||
name='field_filters',
|
||||
field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Filters'),
|
||||
),
|
||||
]
|
@ -1,5 +1,6 @@
|
||||
"""Model definitions for the 'importer' app."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@ -31,7 +32,9 @@ class DataImportSession(models.Model):
|
||||
data_file: FileField for the data file to import
|
||||
status: IntegerField for the status of the import session
|
||||
user: ForeignKey to the User who initiated the import
|
||||
field_defaults: JSONField for field default values
|
||||
field_defaults: JSONField for field default values - provides a backup value for a field
|
||||
field_overrides: JSONField for field override values - used to force a value for a field
|
||||
field_filters: JSONField for field filter values - optional field API filters
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@ -92,6 +95,20 @@ class DataImportSession(models.Model):
|
||||
validators=[importer.validators.validate_field_defaults],
|
||||
)
|
||||
|
||||
field_overrides = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Field Overrides'),
|
||||
validators=[importer.validators.validate_field_defaults],
|
||||
)
|
||||
|
||||
field_filters = models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Field Filters'),
|
||||
validators=[importer.validators.validate_field_defaults],
|
||||
)
|
||||
|
||||
@property
|
||||
def field_mapping(self):
|
||||
"""Construct a dict of field mappings for this import session.
|
||||
@ -132,8 +149,15 @@ class DataImportSession(models.Model):
|
||||
|
||||
matched_columns = set()
|
||||
|
||||
field_overrides = self.field_overrides or {}
|
||||
|
||||
# Create a default mapping for each available field in the database
|
||||
for field, field_def in serializer_fields.items():
|
||||
# If an override value is provided for the field,
|
||||
# skip creating a mapping for this field
|
||||
if field in field_overrides:
|
||||
continue
|
||||
|
||||
# Generate a list of possible column names for this field
|
||||
field_options = [
|
||||
field,
|
||||
@ -181,10 +205,15 @@ class DataImportSession(models.Model):
|
||||
required_fields = self.required_fields()
|
||||
|
||||
field_defaults = self.field_defaults or {}
|
||||
field_overrides = self.field_overrides or {}
|
||||
|
||||
missing_fields = []
|
||||
|
||||
for field in required_fields.keys():
|
||||
# An override value exists
|
||||
if field in field_overrides:
|
||||
continue
|
||||
|
||||
# A default value exists
|
||||
if field in field_defaults and field_defaults[field]:
|
||||
continue
|
||||
@ -265,6 +294,18 @@ class DataImportSession(models.Model):
|
||||
self.status = DataImportStatusCode.PROCESSING.value
|
||||
self.save()
|
||||
|
||||
def check_complete(self) -> bool:
|
||||
"""Check if the import session is complete."""
|
||||
if self.completed_row_count < self.row_count:
|
||||
return False
|
||||
|
||||
# Update the status of this session
|
||||
if self.status != DataImportStatusCode.COMPLETE.value:
|
||||
self.status = DataImportStatusCode.COMPLETE.value
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def row_count(self):
|
||||
"""Return the number of rows in the import session."""
|
||||
@ -467,6 +508,34 @@ class DataImportRow(models.Model):
|
||||
|
||||
complete = models.BooleanField(default=False, verbose_name=_('Complete'))
|
||||
|
||||
@property
|
||||
def default_values(self) -> dict:
|
||||
"""Return a dict object of the 'default' values for this row."""
|
||||
defaults = self.session.field_defaults or {}
|
||||
|
||||
if type(defaults) is not dict:
|
||||
try:
|
||||
defaults = json.loads(str(defaults))
|
||||
except json.JSONDecodeError:
|
||||
logger.warning('Failed to parse default values for import row')
|
||||
defaults = {}
|
||||
|
||||
return defaults
|
||||
|
||||
@property
|
||||
def override_values(self) -> dict:
|
||||
"""Return a dict object of the 'override' values for this row."""
|
||||
overrides = self.session.field_overrides or {}
|
||||
|
||||
if type(overrides) is not dict:
|
||||
try:
|
||||
overrides = json.loads(str(overrides))
|
||||
except json.JSONDecodeError:
|
||||
logger.warning('Failed to parse override values for import row')
|
||||
overrides = {}
|
||||
|
||||
return overrides
|
||||
|
||||
def extract_data(
|
||||
self, available_fields: dict = None, field_mapping: dict = None, commit=True
|
||||
):
|
||||
@ -477,14 +546,24 @@ class DataImportRow(models.Model):
|
||||
if not available_fields:
|
||||
available_fields = self.session.available_fields()
|
||||
|
||||
default_values = self.session.field_defaults or {}
|
||||
overrride_values = self.override_values
|
||||
default_values = self.default_values
|
||||
|
||||
data = {}
|
||||
|
||||
# We have mapped column (file) to field (serializer) already
|
||||
for field, col in field_mapping.items():
|
||||
# Data override (force value and skip any further checks)
|
||||
if field in overrride_values:
|
||||
data[field] = overrride_values[field]
|
||||
continue
|
||||
|
||||
# Default value (if provided)
|
||||
if field in default_values:
|
||||
data[field] = default_values[field]
|
||||
|
||||
# If this field is *not* mapped to any column, skip
|
||||
if not col:
|
||||
if not col or col not in self.row_data:
|
||||
continue
|
||||
|
||||
# Extract field type
|
||||
@ -516,11 +595,14 @@ class DataImportRow(models.Model):
|
||||
- If available, we use the "default" values provided by the import session
|
||||
- If available, we use the "override" values provided by the import session
|
||||
"""
|
||||
data = self.session.field_defaults or {}
|
||||
data = self.default_values
|
||||
|
||||
if self.data:
|
||||
data.update(self.data)
|
||||
|
||||
# Override values take priority, if present
|
||||
data.update(self.override_values)
|
||||
|
||||
return data
|
||||
|
||||
def construct_serializer(self):
|
||||
@ -568,6 +650,8 @@ class DataImportRow(models.Model):
|
||||
self.complete = True
|
||||
self.save()
|
||||
|
||||
self.session.check_complete()
|
||||
|
||||
except Exception as e:
|
||||
self.errors = {'non_field_errors': str(e)}
|
||||
result = False
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""API serializers for the importer app."""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -47,6 +49,8 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
|
||||
'columns',
|
||||
'column_mappings',
|
||||
'field_defaults',
|
||||
'field_overrides',
|
||||
'field_filters',
|
||||
'row_count',
|
||||
'completed_row_count',
|
||||
]
|
||||
@ -75,6 +79,45 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
|
||||
|
||||
user_detail = UserSerializer(source='user', read_only=True, many=False)
|
||||
|
||||
def validate_field_defaults(self, defaults):
|
||||
"""De-stringify the field defaults."""
|
||||
if defaults is None:
|
||||
return None
|
||||
|
||||
if type(defaults) is not dict:
|
||||
try:
|
||||
defaults = json.loads(str(defaults))
|
||||
except:
|
||||
raise ValidationError(_('Invalid field defaults'))
|
||||
|
||||
return defaults
|
||||
|
||||
def validate_field_overrides(self, overrides):
|
||||
"""De-stringify the field overrides."""
|
||||
if overrides is None:
|
||||
return None
|
||||
|
||||
if type(overrides) is not dict:
|
||||
try:
|
||||
overrides = json.loads(str(overrides))
|
||||
except:
|
||||
raise ValidationError(_('Invalid field overrides'))
|
||||
|
||||
return overrides
|
||||
|
||||
def validate_field_filters(self, filters):
|
||||
"""De-stringify the field filters."""
|
||||
if filters is None:
|
||||
return None
|
||||
|
||||
if type(filters) is not dict:
|
||||
try:
|
||||
filters = json.loads(str(filters))
|
||||
except:
|
||||
raise ValidationError(_('Invalid field filters'))
|
||||
|
||||
return filters
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Override create method for this serializer.
|
||||
|
||||
@ -167,4 +210,7 @@ class DataImportAcceptRowSerializer(serializers.Serializer):
|
||||
for row in rows:
|
||||
row.validate(commit=True)
|
||||
|
||||
if session := self.context.get('session', None):
|
||||
session.check_complete()
|
||||
|
||||
return rows
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Custom validation routines for the 'importer' app."""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -46,4 +46,8 @@ def validate_field_defaults(value):
|
||||
return
|
||||
|
||||
if type(value) is not dict:
|
||||
# OK if we can parse it as JSON
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
raise ValidationError(_('Value must be a valid dictionary object'))
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
15062
src/backend/InvenTree/locale/et/LC_MESSAGES/django.po
Normal file
15062
src/backend/InvenTree/locale/et/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user