Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0a9a8b1c54 |
|
|
@ -1,7 +1,9 @@
|
|||
# Dockerfile for the InvenTree devcontainer
|
||||
# This container is used for development of the InvenTree project, and includes all necessary dependencies for both backend and frontend development.
|
||||
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.12-trixie@sha256:5440cb68898d190ad6c6e8a4634ce89d0645bea47f9c8beb75612bb8e3983711
|
||||
# In contrast with the "production" image (which is based on an Alpine image)
|
||||
# we use a Debian-based image for the devcontainer
|
||||
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.11-bookworm@sha256:e754c29c4e3ffcf6c794c1020e36a0812341d88ec9569a34704b975fa89e8848
|
||||
|
||||
# InvenTree paths
|
||||
ENV INVENTREE_HOME="/home/inventree"
|
||||
|
|
@ -23,10 +25,10 @@ RUN chmod +x init.sh
|
|||
|
||||
# Install required base packages
|
||||
RUN apt update && apt install -y \
|
||||
python3-dev python3-venv \
|
||||
python3.11-dev python3.11-venv \
|
||||
postgresql-client \
|
||||
libldap2-dev libsasl2-dev \
|
||||
libpango-1.0-0 libcairo2 \
|
||||
libpango1.0-0 libcairo2 \
|
||||
poppler-utils weasyprint
|
||||
|
||||
# Install packages required for frontend development
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- stable
|
||||
- refs/tags/*
|
||||
paths:
|
||||
include:
|
||||
- src/backend
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
Python39:
|
||||
PYTHON_VERSION: '3.11'
|
||||
maxParallel: 3
|
||||
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '$(PYTHON_VERSION)'
|
||||
architecture: 'x64'
|
||||
|
||||
- task: PythonScript@0
|
||||
displayName: 'Export project path'
|
||||
inputs:
|
||||
scriptSource: 'inline'
|
||||
script: |
|
||||
"""Search all subdirectories for `manage.py`."""
|
||||
from glob import iglob
|
||||
from os import path
|
||||
# Python >= 3.5
|
||||
manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None)
|
||||
if not manage_py:
|
||||
raise SystemExit('Could not find a Django project')
|
||||
project_location = path.dirname(path.abspath(manage_py))
|
||||
print('Found Django project in', project_location)
|
||||
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
|
||||
|
||||
- script: |
|
||||
python -m pip install --upgrade pip setuptools wheel uv
|
||||
uv pip install --require-hashes -r src/backend/requirements.txt
|
||||
uv pip install --require-hashes -r src/backend/requirements-dev.txt
|
||||
sudo apt-get install poppler-utils
|
||||
sudo apt-get install libpoppler-dev
|
||||
uv pip install unittest-xml-reporting coverage invoke
|
||||
displayName: 'Install prerequisites'
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
|
||||
- script: |
|
||||
pushd '$(projectRoot)'
|
||||
invoke update --uv
|
||||
coverage run manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
|
||||
coverage xml -i
|
||||
displayName: 'Run tests'
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
INVENTREE_BACKUP_DIR: ./backup
|
||||
INVENTREE_SITE_URL: http://localhost:8000
|
||||
INVENTREE_PLUGINS_ENABLED: true
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
INVENTREE_DEBUG: true
|
||||
INVENTREE_LOG_LEVEL: INFO
|
||||
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFiles: "**/TEST-*.xml"
|
||||
testRunTitle: 'Python $(PYTHON_VERSION)'
|
||||
condition: succeededOrFailed()
|
||||
|
||||
- task: PublishCodeCoverageResults@2
|
||||
inputs:
|
||||
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
|
||||
|
|
@ -39,14 +39,14 @@ runs:
|
|||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Python installs
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
if: ${{ inputs.python == 'true' && env.python_version != '3.14' }}
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # pin@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: pip
|
||||
|
|
@ -57,7 +57,7 @@ runs:
|
|||
contrib/dev_reqs/requirements.txt
|
||||
- name: Setup Python 3.14
|
||||
if: ${{ inputs.python == 'true' && env.python_version == '3.14' }}
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
|
||||
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # pin@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Install Base Python Dependencies
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
)
|
||||
steps:
|
||||
- name: Backport Action
|
||||
uses: sorenlouv/backport-github-action@8a6c0381851f43f9f1fddc7303f0e9015eb57b62 # v12.0.4
|
||||
uses: sqren/backport-github-action@ad888e978060bc1b2798690dd9d03c4036560947 # pin@v9.2.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ on:
|
|||
- l10
|
||||
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -46,6 +46,4 @@ jobs:
|
|||
run: |
|
||||
python ./.github/scripts/check_source_strings.py --frontend --backend
|
||||
- name: Check Migration Files
|
||||
run: |
|
||||
invoke migrate --detect
|
||||
python3 .github/scripts/check_migration_files.py
|
||||
run: python3 .github/scripts/check_migration_files.py
|
||||
|
|
|
|||
|
|
@ -39,10 +39,10 @@ jobs:
|
|||
docker: ${{ steps.filter.outputs.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -62,12 +62,12 @@ jobs:
|
|||
contents: read
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: 3.12
|
||||
python_version: "3.11"
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Test Docker Image
|
||||
|
|
@ -134,12 +134,12 @@ jobs:
|
|||
contents: read
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: 3.12
|
||||
python_version: "3.11"
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run Migration Tests
|
||||
|
|
@ -158,16 +158,16 @@ jobs:
|
|||
id-token: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
python_version: 3.12
|
||||
python_version: "3.11"
|
||||
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set Up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Version Check
|
||||
|
|
@ -178,13 +178,13 @@ jobs:
|
|||
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
|
||||
- name: Set up QEMU
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # pin@v4.1.0
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # pin@v4.1.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # pin@v4.1.2
|
||||
- name: Check if Dockerhub login is required
|
||||
id: docker_login
|
||||
run: |
|
||||
|
|
@ -195,14 +195,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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # pin@v4.2.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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # pin@v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
@ -211,16 +211,16 @@ jobs:
|
|||
- name: Extract Docker metadata
|
||||
if: github.event_name != 'pull_request'
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # pin@v6.1.0
|
||||
with:
|
||||
images: |
|
||||
inventree/inventree
|
||||
ghcr.io/${{ github.repository }}
|
||||
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1
|
||||
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1
|
||||
- name: Push Docker Images
|
||||
id: push-docker
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1
|
||||
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # pin@v1
|
||||
with:
|
||||
project: jczzbjkk68
|
||||
context: .
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@ name: Frontend
|
|||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: ["l10*", "dependabot/**", "backport/**"]
|
||||
branches-ignore: ["l10*", "dependabot/*", "backport/*"]
|
||||
pull_request:
|
||||
branches-ignore: ["l10*"]
|
||||
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
node_version: 24
|
||||
plugin_creator_version: 1.20.0
|
||||
# The OS version must be set per job
|
||||
server_start_sleep: 60
|
||||
|
||||
|
|
@ -46,10 +45,10 @@ jobs:
|
|||
force: ${{ steps.force.outputs.force }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -69,7 +68,7 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -86,7 +85,7 @@ jobs:
|
|||
run: |
|
||||
cd src/backend/InvenTree/web/static
|
||||
zip -r frontend-build.zip web/ web/.vite
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: src/backend/InvenTree/web/static/web
|
||||
|
|
@ -123,7 +122,7 @@ jobs:
|
|||
VITE_COVERAGE: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -141,7 +140,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: invoke int.frontend-compile --extract
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
|
|
@ -154,7 +153,7 @@ jobs:
|
|||
run: cd src/frontend && npx playwright install-deps
|
||||
- name: Install Sample Plugin
|
||||
run: |
|
||||
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
|
||||
pip install -U inventree-plugin-creator
|
||||
create-inventree-plugin --default
|
||||
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
|
||||
- name: Run Playwright tests
|
||||
|
|
@ -165,7 +164,7 @@ jobs:
|
|||
cp ./tests/fixtures/playwright_custom_splash.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_splash.png
|
||||
invoke static
|
||||
env INVENTREE_CUSTOM_SPLASH="img/playwright_custom_splash.png" INVENTREE_CUSTOM_LOGO="img/playwright_custom_logo.png" PLAYWRIGHT_BASE_URL=http://localhost:8000 npx playwright test --project=firefox --shard=${{ matrix.shard }}/2
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
|
||||
with:
|
||||
name: playwright-report-firefox-${{ matrix.shard }}
|
||||
|
|
@ -205,7 +204,7 @@ jobs:
|
|||
VITE_COVERAGE: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -223,7 +222,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: invoke int.frontend-compile --extract
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
|
|
@ -236,7 +235,7 @@ jobs:
|
|||
run: cd src/frontend && npx playwright install-deps
|
||||
- name: Install Sample Plugin
|
||||
run: |
|
||||
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
|
||||
pip install -U inventree-plugin-creator
|
||||
create-inventree-plugin --default
|
||||
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
|
||||
- name: Playwright [${{ matrix.shard }} / 4]
|
||||
|
|
@ -245,7 +244,7 @@ jobs:
|
|||
cd src/frontend
|
||||
npx nyc playwright test --project=chromium --shard=${{ matrix.shard }}/4
|
||||
- name: Playwright Report [${{ matrix.shard }} / 4]
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
|
||||
with:
|
||||
name: playwright-report-chromium-${{ matrix.shard }}
|
||||
|
|
@ -253,7 +252,7 @@ jobs:
|
|||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: Upload Coverage Artifact [${{ matrix.shard }} / 4]
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
id: coverage-upload
|
||||
if: ${{ !cancelled() && steps.tests.outcome != 'failure' }}
|
||||
with:
|
||||
|
|
@ -273,7 +272,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -285,7 +284,7 @@ jobs:
|
|||
update: false
|
||||
|
||||
- name: Download Coverage Artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
||||
with:
|
||||
pattern: coverage-*
|
||||
path: all-coverage/
|
||||
|
|
@ -304,7 +303,7 @@ jobs:
|
|||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: ${{ !cancelled() && github.ref == 'refs/heads/master' }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: inventree/InvenTree
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ name: Import / Export
|
|||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: ["l10*", "dependabot/**", "backport/**"]
|
||||
branches-ignore: ["l10*", "dependabot/*", "backport/*"]
|
||||
pull_request:
|
||||
branches-ignore: ["l10*"]
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
@ -48,10 +48,10 @@ jobs:
|
|||
outputs:
|
||||
server: ${{ steps.filter.outputs.server }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -76,7 +76,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ name: QC
|
|||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: ["l10*", "dependabot/**", "backport/**"]
|
||||
branches-ignore: ["l10*", "dependabot/*", "backport/*"]
|
||||
pull_request:
|
||||
branches-ignore: ["l10*"]
|
||||
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
node_version: 24
|
||||
# The OS version must be set per job
|
||||
server_start_sleep: 60
|
||||
|
|
@ -44,10 +44,10 @@ jobs:
|
|||
submit-performance: ${{ steps.runner-perf.outputs.submit-performance }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -108,16 +108,16 @@ jobs:
|
|||
if: needs.paths-filter.outputs.cicd == 'true' || needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: "pip"
|
||||
- name: Run pre commit hook Checks
|
||||
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # pin@v2
|
||||
- name: Check Version
|
||||
run: |
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
|
|
@ -130,7 +130,7 @@ jobs:
|
|||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -152,11 +152,11 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Check Config
|
||||
|
|
@ -165,7 +165,7 @@ jobs:
|
|||
pip install --require-hashes -r docs/requirements.txt
|
||||
python docs/ci/check_mkdocs_config.py
|
||||
- name: Check Links
|
||||
uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1
|
||||
uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # pin@v1
|
||||
with:
|
||||
folder-path: docs
|
||||
config-file: docs/mlc_config.json
|
||||
|
|
@ -190,7 +190,7 @@ jobs:
|
|||
version: ${{ steps.version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -202,7 +202,7 @@ jobs:
|
|||
- name: Export API Documentation
|
||||
run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
|
||||
- name: Upload schema
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: schema.yml
|
||||
path: src/backend/InvenTree/schema.yml
|
||||
|
|
@ -222,7 +222,7 @@ jobs:
|
|||
echo "Downloaded api.yaml"
|
||||
- name: Running OpenAPI Spec diff action
|
||||
id: breaking_changes
|
||||
uses: oasdiff/oasdiff-action/diff@3e9d440d37f468355457604348009f50e0cddbf3 # v0.1.4
|
||||
uses: oasdiff/oasdiff-action/diff@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
|
||||
with:
|
||||
base: "api.yaml"
|
||||
revision: "src/backend/InvenTree/schema.yml"
|
||||
|
|
@ -251,17 +251,17 @@ jobs:
|
|||
- name: Extract settings / tags
|
||||
run: invoke int.export-definitions --basedir docs
|
||||
- name: Upload settings
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: inventree_settings.json
|
||||
path: docs/generated/inventree_settings.json
|
||||
- name: Upload tags
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: inventree_tags.yml
|
||||
path: docs/generated/inventree_tags.yml
|
||||
- name: Upload filters
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: inventree_filters.yml
|
||||
path: docs/generated/inventree_filters.yml
|
||||
|
|
@ -275,7 +275,7 @@ jobs:
|
|||
version: ${{ needs.schema.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
name: Checkout Code
|
||||
with:
|
||||
repository: inventree/schema
|
||||
|
|
@ -284,7 +284,7 @@ jobs:
|
|||
- name: Create artifact directory
|
||||
run: mkdir -p artifact
|
||||
- name: Download schema artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
||||
with:
|
||||
path: artifact
|
||||
merge-multiple: true
|
||||
|
|
@ -301,7 +301,7 @@ jobs:
|
|||
echo "after move"
|
||||
ls -la artifact
|
||||
rm -rf artifact
|
||||
- uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # pin@v7.1.0
|
||||
name: Commit schema changes
|
||||
with:
|
||||
commit_message: "Update API schema for ${{ env.version }} / ${{ github.sha }}"
|
||||
|
|
@ -332,7 +332,7 @@ jobs:
|
|||
node_version: '>=24'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -363,7 +363,7 @@ jobs:
|
|||
pip install .
|
||||
if: needs.paths-filter.outputs.submit-performance == 'true'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1
|
||||
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
||||
# check if we are in inventree/inventree - reporting only works in that OIDC context
|
||||
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
|
||||
with:
|
||||
|
|
@ -379,7 +379,7 @@ jobs:
|
|||
continue-on-error: true # continue if a step fails so that coverage gets pushed
|
||||
strategy:
|
||||
matrix:
|
||||
python_version: [3.12, 3.14]
|
||||
python_version: [3.11, 3.14]
|
||||
|
||||
env:
|
||||
INVENTREE_DB_NAME: ./inventree.sqlite
|
||||
|
|
@ -390,7 +390,7 @@ jobs:
|
|||
python_version: ${{ matrix.python_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -405,19 +405,17 @@ jobs:
|
|||
- name: Test Translations
|
||||
run: invoke dev.translate
|
||||
- name: Check Migration Files
|
||||
run: |
|
||||
invoke migrate --detect
|
||||
python3 .github/scripts/check_migration_files.py
|
||||
run: python3 .github/scripts/check_migration_files.py
|
||||
- name: Coverage Tests
|
||||
run: invoke dev.test --check --coverage --translations
|
||||
- name: Upload raw coverage to artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: coverage
|
||||
path: .coverage
|
||||
retention-days: 14
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -443,7 +441,7 @@ jobs:
|
|||
INVENTREE_AUTO_UPDATE: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -456,7 +454,7 @@ jobs:
|
|||
env:
|
||||
node_version: '>=24'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1
|
||||
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
||||
with:
|
||||
mode: walltime
|
||||
run: inv dev.test --pytest
|
||||
|
|
@ -494,7 +492,7 @@ jobs:
|
|||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -543,7 +541,7 @@ jobs:
|
|||
- 3306:3306
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -586,7 +584,7 @@ jobs:
|
|||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -599,7 +597,7 @@ jobs:
|
|||
- name: Run Tests
|
||||
run: invoke dev.test --check --migrations --report --coverage --translations
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # pin@v7.0.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -620,7 +618,7 @@ jobs:
|
|||
INVENTREE_PLUGINS_ENABLED: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
name: Checkout Code
|
||||
|
|
@ -676,8 +674,8 @@ jobs:
|
|||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
|
||||
jobs:
|
||||
stable:
|
||||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Version Check
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
python3 .github/scripts/version_check.py
|
||||
- name: Push to Stable Branch
|
||||
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # v1.3.0
|
||||
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # pin@v1.3.0
|
||||
if: env.stable_release == 'true'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
artifact-metadata: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Create SBOM for frontend
|
||||
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
|
||||
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # pin@v0
|
||||
with:
|
||||
artifact-name: frontend-build.spdx
|
||||
path: src/frontend
|
||||
|
|
@ -75,7 +75,7 @@ jobs:
|
|||
zip -r ../frontend-build.zip * .vite
|
||||
- name: Attest Build Provenance
|
||||
id: attest
|
||||
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # pin@v4
|
||||
with:
|
||||
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ jobs:
|
|||
REF: ${{ github.ref_name }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload frontend to artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: src/backend/InvenTree/web/static/frontend-build.zip
|
||||
|
|
@ -115,7 +115,7 @@ jobs:
|
|||
INVENTREE_DEBUG: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -151,17 +151,17 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- ubuntu:22.04
|
||||
- ubuntu:24.04
|
||||
- ubuntu:26.04
|
||||
- debian:13
|
||||
- debian:12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Get frontend artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
- name: Setup
|
||||
|
|
@ -216,7 +216,7 @@ jobs:
|
|||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
python3 .github/scripts/version_check.py
|
||||
- name: Package - current release channel
|
||||
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main
|
||||
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
|
||||
id: package
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
|
@ -234,7 +234,7 @@ jobs:
|
|||
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
|
||||
APP_REPO=inventree/InvenTree
|
||||
- name: Publish to go.packager.io - current release channel
|
||||
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
|
||||
uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
|
||||
|
|
@ -249,7 +249,7 @@ jobs:
|
|||
PACKAGE_NAME: ${{ matrix.target }}-${{ steps.setup.outputs.version }}.tar.gz
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Package - stable release channel
|
||||
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main
|
||||
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
|
||||
id: package-stable
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
|
@ -267,7 +267,7 @@ jobs:
|
|||
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
|
||||
APP_REPO=inventree/InvenTree
|
||||
- name: Publish to go.packager.io - stable release channel
|
||||
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
|
||||
uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # pin@v10.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: "This issue seems stale. Please react to show this is still important."
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ on:
|
|||
- master
|
||||
|
||||
env:
|
||||
python_version: 3.12
|
||||
python_version: 3.11
|
||||
node_version: 24
|
||||
|
||||
permissions:
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
echo "Resetting to HEAD~"
|
||||
git reset HEAD~ || true
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2
|
||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # pin@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup
|
||||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
run: pip-compile --output-file=requirements.txt requirements.in -U
|
||||
- name: Update requirements-dev.txt
|
||||
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
|
||||
- uses: stefanzweifel/git-auto-commit-action@fd157da78fa13d9383e5580d1fd1184d89554b51 # v4.15.1
|
||||
- uses: stefanzweifel/git-auto-commit-action@fd157da78fa13d9383e5580d1fd1184d89554b51 # pin@v4.15.1
|
||||
with:
|
||||
commit_message: "[Bot] Updated dependency"
|
||||
branch: dep-update
|
||||
|
|
|
|||
10
.pkgr.yml
10
.pkgr.yml
|
|
@ -21,9 +21,9 @@ before:
|
|||
dependencies:
|
||||
- curl
|
||||
- poppler-utils
|
||||
- "python3.12 | python3.13 | python3.14"
|
||||
- "python3.12-venv | python3.13-venv | python3.14-venv"
|
||||
- "python3.12-dev | python3.13-dev | python3.14-dev"
|
||||
- "python3.11 | python3.12 | python3.13 | python3.14"
|
||||
- "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
|
||||
- "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
|
||||
- python3-pip
|
||||
- python3-cffi
|
||||
- python3-brotli
|
||||
|
|
@ -36,6 +36,6 @@ dependencies:
|
|||
- jq
|
||||
- "libffi7 | libffi8"
|
||||
targets:
|
||||
ubuntu-22.04: true
|
||||
ubuntu-24.04: true
|
||||
ubuntu-26.04: true
|
||||
debian-13: true
|
||||
debian-12: true
|
||||
|
|
|
|||
|
|
@ -97,4 +97,4 @@ repos:
|
|||
rev: 0.4.3
|
||||
hooks:
|
||||
- id: teyit
|
||||
language_version: python3.12
|
||||
language_version: python3.11
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
"name": "InvenTree invoke schema",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/.venv/lib/python3.12/site-packages/invoke/__main__.py",
|
||||
"program": "${workspaceFolder}/.venv/lib/python3.11/site-packages/invoke/__main__.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"dev.schema","--ignore-warnings"
|
||||
|
|
|
|||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -5,30 +5,6 @@ All major notable changes to this project will be documented in this file (start
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased - xxxx.xx.xx
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [#12360](https://github.com/inventree/InvenTree/pull/12360) removes the MPTT mixin from the StockItem model, and removes the self-referential tree structure from the database. This change was made to simplify the StockItem model and improve performance, as the MPTT tree structure was causing significant overhead in certain operations. Any external client applications which made use of the MPTT functionality will need to be updated to account for this change.
|
||||
- [#12320](https://github.com/inventree/InvenTree/pull/12320) changes the default behavior of the `invoke migrate` command. Now, it no longer generates new migrations by default. Instead, it will only apply existing migrations to the database. If you want to detect and generate new migrations, you must now explicitly use the `--detect` flag. This change was made to prevent accidental generation of migrations when running the command, which could lead to unexpected changes in the database schema. Additionally, `invoke update` will no longer result in new migrations being generated, and will only apply existing migrations to the database. This change was made to ensure that the update process is predictable and does not introduce unexpected changes to the database schema.
|
||||
- [#12223](https://github.com/inventree/InvenTree/pull/12223) removes support for python 3.11 and stops providing packages for Debian 11 and Ubuntu 20.04.
|
||||
|
||||
### Added
|
||||
|
||||
- [#12391](https://github.com/inventree/InvenTree/pull/12391) adds facility for bulk deleting line items against orders
|
||||
- [#12388](https://github.com/inventree/InvenTree/pull/12388) adds uniqueness requirements options for the Parameter and ParameterTemplate models. This allows users to specify whether a parameter value should be unique for a given model type, or globally unique across all models.
|
||||
- [#12310](https://github.com/inventree/InvenTree/pull/12310) adds the ability to disassemble (or break apart) assembled stock items into their component parts, based on the Bill of Materials (BOM) associated with the stock item. This allows users to easily break down assembled items into their constituent parts, which can be useful for inventory management and tracking purposes.
|
||||
- [#12117](https://github.com/inventree/InvenTree/pull/12117) adds a "preview" drawer to the InvenTree table component, allowing users to preview the details of a selected row without navigating away from the table view. This feature is optional and can be enabled or disabled via the `PREVIEW_DRAWER_ENABLED` system setting.
|
||||
- [#12341](https://github.com/inventree/InvenTree/pull/12341) adds support for importing internal part prices.
|
||||
- [#12295](https://github.com/inventree/InvenTree/pull/12295) adds "consumable" field to the Part model and API endpoints
|
||||
- [#12250](https://github.com/inventree/InvenTree/pull/12250) adds "active" field to the ProjectCode model and API endpoints
|
||||
|
||||
### Changed
|
||||
|
||||
- [#12274](https://github.com/inventree/InvenTree/pull/12274) fixes a bug in rendering the "table field" component in frontend forms. Any plugins which make use of the "table field" component in their forms may be affected by this change, and will need to update their form definitions accordingly.
|
||||
|
||||
### Removed
|
||||
|
||||
## 1.4.0 - 2026-06-24
|
||||
|
||||
### Breaking Changes
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ The InvenTree project is split into two main components: frontend and backend. T
|
|||
|
||||
```
|
||||
InvenTree/
|
||||
├─ .devops/ # Files for Azure DevOps
|
||||
├─ .github/ # Files for GitHub
|
||||
│ ├─ actions/ # Reused actions
|
||||
│ ├─ ISSUE_TEMPLATE/ # Templates for issues and pull requests
|
||||
|
|
@ -104,8 +105,8 @@ The project uses [Invoke](https://www.pyinvoketasks.com/) (`tasks.py`) as the ta
|
|||
# One-time setup: creates venv at dev/venv/, installs deps, sets up pre-commit hooks
|
||||
invoke dev.setup-dev
|
||||
|
||||
# Apply database migrations (and detect/create new migration files if required)
|
||||
invoke migrate --detect
|
||||
# Apply database migrations
|
||||
invoke migrate
|
||||
|
||||
# Create an admin account (required to log in)
|
||||
invoke superuser
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
[](https://inventree.readthedocs.io/en/latest/?badge=latest)
|
||||

|
||||
[](https://app.netlify.com/sites/inventree/deploys)
|
||||
[](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_build/latest?definitionId=3&branchName=testing)
|
||||
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7179)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
|
||||
|
|
@ -201,7 +202,6 @@ Find a full list of used third-party libraries in the license information dialog
|
|||
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-bg.svg" alt="Deploys by Netlify" /> </a>
|
||||
<a href="https://crowdin.com"> <img src="https://crowdin.com/images/crowdin-logo.svg" alt="Translation by Crowdin" /> </a> <br>
|
||||
<a href="https://codspeed.io/inventree/InvenTree?utm_source=badge"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"/></a>
|
||||
<a href="https://flakiness.io/InvenTree/InvenTree"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fflakiness.io%2Fapi%2Fbadge%3Finput%3D%257B%2522badgeToken%2522%253A%2522badge-35mqq5Ht4uL3vGF8lR9P2D%2522%257D" alt="Flakiness Badge"/></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ flag_management:
|
|||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 38%
|
||||
target: 40%
|
||||
- name: web
|
||||
carryforward: true
|
||||
statuses:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
# - Monitors source files for any changes, and live-reloads server
|
||||
|
||||
# Base image last bumped 2026-06-16
|
||||
FROM python:3.14.6-slim-trixie@sha256:b877e50bd90de10af8d82c57a022fc2e0dc731c5320d762a27986facfc3355c1 AS inventree_base
|
||||
FROM python:3.14.6-slim-trixie@sha256:44dd04494ee8f3b538294360e7c4b3acb87c8268e4d0a4828a6500b1eff50061 AS inventree_base
|
||||
|
||||
# Build arguments for this image
|
||||
ARG commit_tag=""
|
||||
|
|
@ -119,8 +119,7 @@ RUN pip install --user --require-hashes -r base_requirements.txt --no-cache-dir
|
|||
rm -rf /root/.cache/pip
|
||||
|
||||
# Install frontend build dependencies
|
||||
# pinned to v0.40.5
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/1889911f0841e669de0be5bd02c737a3f1fd20fa/install.sh | bash && \
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash && \
|
||||
bash -c "export NVM_DIR="$HOME/.nvm" && source $HOME/.nvm/nvm.sh && \
|
||||
nvm install $NODE_VERSION && corepack enable yarn && yarn config set network-timeout 600000 -g"
|
||||
RUN bash -c "source $HOME/.nvm/nvm.sh && cd '${INVENTREE_HOME}' && invoke int.frontend-compile --extract"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# Base python requirements for docker containers
|
||||
|
||||
# Basic package requirements
|
||||
invoke # Invoke build tool
|
||||
pyyaml
|
||||
setuptools
|
||||
wheel
|
||||
invoke>=2.2.0 # Invoke build tool
|
||||
pyyaml>=6.0.1
|
||||
setuptools>=69.0.0
|
||||
wheel>=0.41.0
|
||||
|
||||
# Database links
|
||||
psycopg[binary, pool]
|
||||
mysqlclient
|
||||
mariadb
|
||||
mysqlclient>=2.2.0
|
||||
mariadb>=1.1.8
|
||||
|
||||
# gunicorn web server
|
||||
gunicorn
|
||||
gunicorn>=22.0.0
|
||||
|
||||
# LDAP required packages
|
||||
django-auth-ldap # Django integration for ldap auth
|
||||
|
|
|
|||
|
|
@ -236,26 +236,26 @@ typing-extensions==4.15.0 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# psycopg-pool
|
||||
uv==0.11.24 \
|
||||
--hash=sha256:047d763d20d71968c00f4afec40b0e75d9da7e3693f725b9f502d84a25256893 \
|
||||
--hash=sha256:0e100b9cbc59beff2730840ac989b1f100cc03c90514e7cab6992a1f3f13784e \
|
||||
--hash=sha256:3176668ea8a2318d775c0d9188661c61ccc36790220724b1d360fcc8945d520e \
|
||||
--hash=sha256:356435577fae11fafe7a067ee3269cccefd35b031b83a3a36c87fe9d192bffba \
|
||||
--hash=sha256:38f38c9449aafd71dc0fa35e66a8a547ee48947b2f2f94084893c2afe6058cfa \
|
||||
--hash=sha256:438f8291fb9daea30a4bd333299b82e6ef755578302745a021162f328cfcfc68 \
|
||||
--hash=sha256:48a6123f71b801e0e0b8a38520b011632ad81e0a043445044ce5b1a7b1cec7b6 \
|
||||
--hash=sha256:6ecdad43e870f88d3772d9d37e877259ae35ec374d51589805cdcf6196205829 \
|
||||
--hash=sha256:79757f90b7765996366b80e77cad13555c7f7e1282ca8d8b686ee21773ab0b77 \
|
||||
--hash=sha256:8346b0086d213b80430f3bb4477379a8a4fa550b03447d23c84eee85c2e52bc4 \
|
||||
--hash=sha256:8602a1b6300a3a948afacc62e1cb933c8394c27966db85ed7e29483300b69dc4 \
|
||||
--hash=sha256:9312b6fd44361674e9c847a828ded792493766697816261e05848a024fe34129 \
|
||||
--hash=sha256:bf568c62dddd55ad9c85a16ffdfbcc7746be9c3159ed644e4f9e6f5e44905cbb \
|
||||
--hash=sha256:c4ab221c0a949f8006a7582786dae384706b2f82016a0db60baa7cc696042705 \
|
||||
--hash=sha256:c8ec3caf656645f58b53cb9aee9aa95cfc65c82ba2d7f1362bfd2660d1484307 \
|
||||
--hash=sha256:d6a49543c659c0cf1ff4c944955d97e69dbce4b76e3d284f5a92cea19133ebb6 \
|
||||
--hash=sha256:e499579f557abf17b8ffd1490d3e827748aea096f6eaa66737e729e046449f08 \
|
||||
--hash=sha256:e7e78c18686202c8b8715bebb83bfaf58f82d7fb848b6a5ae4e925a9fac3de4c \
|
||||
--hash=sha256:ed0c9a9d7909f0e48a9dafe666ca9ebefe2a1534e51ed05c0a7de7406465f868
|
||||
uv==0.11.23 \
|
||||
--hash=sha256:03fbb0a1c7b6d15e96778bdd79e8d1826c6259fea17fc13337fb0744136953f2 \
|
||||
--hash=sha256:3a900c8fd757f8c3da9dc5532d9a22d30540e91fdefd63c93909fedbfd756655 \
|
||||
--hash=sha256:576d776a1ca62e3d8aba99f0d8ec607db91a5ebaf52feaff820f28ed820e1665 \
|
||||
--hash=sha256:61e6bd7e7f0fe24f103540ba19516443bea6e689022c787217310a1e64558e3f \
|
||||
--hash=sha256:6d7cea7d9ade3c1c3e3db1dfcc23d335bceaabf38f51e442b6f57f8f7885a9a6 \
|
||||
--hash=sha256:7a85330de0a7eb0d5c6cf03c80edfb86facad19df367a0b52fc906db1ab15ce9 \
|
||||
--hash=sha256:91d19c4249d7437b69b91c385134360d7ed9d0ee2e2e83e81d369867151e78c2 \
|
||||
--hash=sha256:985aa93c9d6223e32fc747e09662537c4073c9ebef59c0a4fd7c6949d1d24fb3 \
|
||||
--hash=sha256:9dd412127cbe0e115bd3fc5c6cbe9cf59f593273fafab9f7dc6b2ac95efcc7c1 \
|
||||
--hash=sha256:ac337dacd640aab1ef97cf00f8c01e2889f0fd0ef8460a0f4e816bf12bb5988b \
|
||||
--hash=sha256:b3f515fd6b43068f241467496bced62cb2ed36d52d4c0877cfe61a1240713d32 \
|
||||
--hash=sha256:b8abe7d6f5e0d92bd41a9c000bbd9c8387af7886df4790c0451a34e781b8a075 \
|
||||
--hash=sha256:bbc41182d655f92cd380ecdf378da7fc1598c6b19057208f450f0ee9c259f46a \
|
||||
--hash=sha256:c2089b992919858dabae89d410cbb5cecf9034d26bbb04f14e6da52dffced290 \
|
||||
--hash=sha256:ca1d37e851fb9323250385403d8512a71c0d1b6162c729ff4909f37cfd067920 \
|
||||
--hash=sha256:d256f90513d01ff6cbc2f17d88c0ccde65d138500df547ece214e6a50731c4b7 \
|
||||
--hash=sha256:d62410e5f60a961cfda00ead8a1cc5fd37d052afda021099e488e90c15419beb \
|
||||
--hash=sha256:e7e215d69ea21fd5824a63edf8fef933bee2c028a0c2930651cfa6b88ca4ff8e \
|
||||
--hash=sha256:f2476dda35866ea3ded3a5905759da2d32dfac36dfd5b3428191a99a8ce15b02
|
||||
# via -r contrib/container/requirements.in
|
||||
wheel==0.47.0 \
|
||||
--hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Packages needed for CI/packages
|
||||
requests==2.34.2
|
||||
pyyaml==6.0.3
|
||||
jc
|
||||
jc==1.25.6
|
||||
|
|
|
|||
|
|
@ -145,9 +145,9 @@ idna==3.18 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
jc==1.25.7 \
|
||||
--hash=sha256:5ed6a0da915c931c04693cab806d5c31d9ef14ca998c6c32308460d37cb30baf \
|
||||
--hash=sha256:63481e34d92715781e1b094eca7d76d122a14489463d45b7f3e8180f44ea3a10
|
||||
jc==1.25.6 \
|
||||
--hash=sha256:27f58befc7ae0a4c63322926c5f1ec892e3eac4a065eff3b07cfe420a6924a07 \
|
||||
--hash=sha256:7367b59e6e0da8babeede1e5b0da083f3c5aa6b6e585b4aed28dd7c4b2d76162
|
||||
# via -r contrib/dev_reqs/requirements.in
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function detect_python() {
|
|||
echo "${On_Red}"
|
||||
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
|
||||
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
|
||||
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.12'."
|
||||
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.11'."
|
||||
echo "${Color_Off}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export DATA_DIR=${APP_HOME}/data
|
|||
export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
|
||||
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
|
||||
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false}
|
||||
export SETUP_PYTHON=${SETUP_PYTHON:-python3.12}
|
||||
export SETUP_PYTHON=${SETUP_PYTHON:-python3.11}
|
||||
export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
|
||||
# SETUP_DEBUG can be set to get debug info
|
||||
# SETUP_EXTRA_PIP can be set to install extra pip packages
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ Users can authenticate against the API using basic authentication - specifically
|
|||
Each user is assigned an authentication token which can be used to access the API. This token is persistent for that user (unless invalidated by an administrator) and can be used across multiple sessions.
|
||||
|
||||
!!! info "Token Administration"
|
||||
User tokens can be created and/or invalidated via the user settings, [Admin Center](../settings/admin.md#admin-center), or the [Database Admin interface](../settings/db_admin.md).
|
||||
User tokens can be created and/or invalidated via the user settings, [Admin Center](../settings/admin.md#admin-center) or admin interface.
|
||||
|
||||
#### Requesting a Token
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB |
|
|
@ -101,7 +101,7 @@ If enabled, InvenTree can retain logs of the most recent barcode scans. This can
|
|||
|
||||
Refer to the [barcode settings](../settings/global.md#barcodes) to enable barcode history logging.
|
||||
|
||||
The barcode history can be viewed in the [Admin Center](../settings/admin.md#admin-center) in the web interface.
|
||||
The barcode history can be viewed via the admin panel in the web interface.
|
||||
|
||||
## Barcode Settings
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Importing Data
|
|||
|
||||
## Importing Data
|
||||
|
||||
External data can be imported via the [Admin Center](../settings/admin.md#admin-center), allowing for rapid integration of existing datasets, or bulk editing of table data.
|
||||
External data can be imported via the admin interface, allowing for rapid integration of existing datasets, or bulk editing of table data.
|
||||
|
||||
!!! danger "Danger"
|
||||
Uploading bulk data directly is a non-reversible action.
|
||||
|
|
@ -57,7 +57,7 @@ An import session can be initiated from a number of different contexts within th
|
|||
|
||||
Staff users can create an import session from within the [Admin Center](../settings/admin.md#admin-center). This is a general-purpose import session, and the user will be required to select the type of data to import.
|
||||
|
||||
Users can quickly navigate to the data import management page from the [spotlight search](./ui/index.md#spotlight), by searching for "import" and selecting the "Import data" option.
|
||||
Users can quickly navigate to the data import management page from the [spotlight search](../concepts/user_interface.md#spotlight), by searching for "import" and selecting the "Import data" option.
|
||||
|
||||
### Data Tables
|
||||
|
||||
|
|
|
|||
|
|
@ -30,13 +30,12 @@ Parameter templates are used to define the different types of parameters which a
|
|||
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
|
||||
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
|
||||
| Selection List | If set, parameters linked to this template can only be assigned values from the linked [selection list](#selection-lists) |
|
||||
| Unique | Enforce a [uniqueness requirement](#parameter-uniqueness) on parameter values linked to this template |
|
||||
|
||||
{{ image("concepts/parameter-template.png", "Parameters Template") }}
|
||||
|
||||
### Create Template
|
||||
|
||||
Parameter templates are created and edited via the [Admin Center](../settings/admin.md#admin-center).
|
||||
Parameter templates are created and edited via the [admin interface](../settings/admin.md).
|
||||
|
||||
To create a template:
|
||||
|
||||
|
|
@ -60,45 +59,6 @@ To add a parameter, navigate to a specific part detail page, click on the "Param
|
|||
|
||||
Select the parameter `Template` you would like to use for this parameter, fill-out the `Data` field (value of this specific parameter) and click the "Submit" button.
|
||||
|
||||
### Parameter Uniqueness
|
||||
|
||||
A parameter template can be configured to enforce a uniqueness requirement on the values of any parameters linked to it. This is useful for parameters which are expected to act as a unique identifier and prevents duplicate values from being entered by mistake.
|
||||
|
||||
The `Unique` attribute on a parameter template supports the following options:
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| No uniqueness required | The default option - no restriction is placed on parameter values |
|
||||
| Unique for model type | A parameter value must be unique amongst all other parameters (linked to this template) which are assigned to the *same* model type. For example, a template with this option enabled could be used to enforce unique serial numbers across all `Part` instances, without preventing the same value from also being used against a `Company` instance |
|
||||
| Globally unique | A parameter value must be unique amongst *all* other parameters linked to this template, regardless of the model type to which they are assigned |
|
||||
|
||||
!!! info "Case Insensitive"
|
||||
Uniqueness checks are case-insensitive - for example, the values `ABC123` and `abc123` are considered to be duplicates of each other.
|
||||
|
||||
If a parameter value is entered which does not satisfy the uniqueness requirement of its template, it will be rejected and an error message displayed.
|
||||
|
||||
#### Validation Against Numeric Value
|
||||
|
||||
If a parameter template defines a set of [units](#parameter-units), uniqueness is checked against the *normalized numeric value* of the parameter, rather than the raw text entered. This ensures that equivalent values expressed in different (but compatible) units are correctly detected as duplicates.
|
||||
|
||||
For example, if a *Resistance* template (with base units of `ohm`) enforces uniqueness, then a value of `1k` would be rejected as a duplicate of an existing value of `1000`, as they both represent the same physical quantity.
|
||||
|
||||
Templates which do not define a set of units are compared using a direct (case-insensitive) text comparison of the raw parameter value.
|
||||
|
||||
#### Copying Caveats
|
||||
|
||||
Some workflows in InvenTree allow parameters to be copied from one object to another - for example, when duplicating a part, build order, or other object which supports parameters.
|
||||
|
||||
Any parameter which is linked to a template with a uniqueness requirement is *skipped* when copying parameters in this way. This prevents the copy operation from creating a duplicate (and therefore invalid) value on the new object. If this occurs, the new object simply will not have a parameter created against that particular template - it can be added manually (with a distinct value) afterwards.
|
||||
|
||||
#### Category Parameter Caveats
|
||||
|
||||
A parameter template can be linked to a part category, along with a default value which is automatically applied to any new parts created within that category (or a sub-category).
|
||||
|
||||
This default value system is not compatible with a template which enforces a uniqueness requirement - applying the *same* default value to every part in a category would immediately conflict with the "unique" requirement, as soon as more than one part exists in that category.
|
||||
|
||||
For this reason, a category-based default value is *not* applied for any parameter template which has a uniqueness requirement configured. If you need to assign values for such parameters, this must be done manually (or via the API) on a per-part basis.
|
||||
|
||||
## Parametric Tables
|
||||
|
||||
Parametric tables gather all parameters from all objects of a particular type, to be sorted and filtered.
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
title: Forms
|
||||
---
|
||||
|
||||
## Forms
|
||||
|
||||
Data entry and editing within InvenTree is typically performed through the use of forms, which provide a structured interface for inputting and modifying data. Forms are designed to be user-friendly and efficient, allowing users to quickly enter and update information within the system.
|
||||
|
||||
Forms are typically displayed as a modal dialog, separated into multiple sections and fields.
|
||||
|
||||
### Data Creation
|
||||
|
||||
Example: Creating a new part via the "Add Part" form:
|
||||
|
||||
{{ image("concepts/ui_form_add_part.png", "Add Part Button") }}
|
||||
|
||||
On several forms is displayed option "Keep form open" in bottom part of the form on left side of Submit button (option is visible on the screenshot above). When this switch is turned on, form window is not closed after submit and filled form data is not reset. This is useful for creating more entries at one time with similar properties (e.g. only different number in name).
|
||||
|
||||
### Data Editing
|
||||
|
||||
Example: Editing an existing purchase order via the "Edit Purchase Order" form:
|
||||
|
||||
{{ image("concepts/ui_form_edit_po.png", "Edit Purchase Order") }}
|
||||
|
||||
### Form Submission
|
||||
|
||||
A form can be submitted by clicking the "Submit" button located at the bottom-right corner of the form.
|
||||
|
||||
Alternatively, an open form can be submitted using the keyboard shortcut `Ctrl + Enter` (or `Cmd + Enter` on macOS). This shortcut works even while typing in a form field, providing a quick way to submit the form without needing to reach for the mouse. It is particularly useful in multi-line text fields, where pressing `Enter` inserts a new line rather than submitting the form.
|
||||
|
||||
The keyboard shortcut is only active while a form is open, and follows the same rules as the "Submit" button - for example, it has no effect while the form is loading, or when editing an existing item without any changes.
|
||||
|
||||
### Confirm Actions
|
||||
|
||||
Many actions within InvenTree require user confirmation before they can be executed. This is typically implemented through the use of confirmation dialogs, which prompt the user to confirm their intention before proceeding with the action.
|
||||
|
||||
{{ image("concepts/ui_form_hold_po.png", "Confirmation Dialog") }}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
title: Global Search
|
||||
---
|
||||
|
||||
## Global Search
|
||||
|
||||
Accessible from the [main menu](./index.md#main-menu), the global search functionality allows users to quickly find specific items or information within the InvenTree system. The search icon is located at the top of the interface and provides a convenient way to search across all sections of the system.
|
||||
|
||||
Clicking on the "search" icon (in the menu bar) opens the search menu, which allows users to enter search queries and view results from across the system.
|
||||
|
||||
{{ image("concepts/ui_global_search.png", "Global Search") }}
|
||||
|
||||
Search results are organized by category (e.g. Parts, Stock, Manufacturing, etc.) and provide quick access to the relevant pages for each search result.
|
||||
|
||||
### Detail View
|
||||
|
||||
To navigate to the detail page for a particular search result, simply click on the desired result from the search results list. This will take you directly to the relevant page within the InvenTree system, allowing you to view and interact with the specific item or information you were searching for.
|
||||
|
||||
### Full Results
|
||||
|
||||
The "global search" menu provides a limited set of search results for each category, typically showing the most relevant or recent results. To view the full set of search results for a particular category, click on the "View all results" button located at the top-left of the search results list for that category:
|
||||
|
||||
{{ image("concepts/ui_global_search_view_all.png", "View Full Search Results") }}
|
||||
|
||||
### Collapse Result Groups
|
||||
|
||||
To collapse a particular category of search results in the global search menu, click on the "collapse" icon located at the top-right corner of the search results list for that category. This will hide the search results for that category, allowing you to focus on other categories or search results.
|
||||
|
||||
### Remove Result Groups
|
||||
|
||||
To remove a particular category of search results from the global search menu, click on the "remove" icon located at the top-right corner of the search results list for that category.
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
---
|
||||
title: User Interface
|
||||
---
|
||||
|
||||
## User Interface
|
||||
|
||||
The InvenTree user interface is designed to be intuitive and user-friendly, providing easy access to the various features and functions of the system. The interface is organized into several key components, including navigation menus, settings, forms, tables, search functionality, and more.
|
||||
|
||||
The interface is designed for large-format displays, and as such is explicitly *not* optimized for mobile devices. However, the interface is responsive and should work on a wide range of desktop screen sizes.
|
||||
|
||||
## Navigation
|
||||
|
||||
Navigation throughout the InvenTree interface is designed to be straightforward and efficient, allowing users to quickly access the various sections and features of the system. The navigation is organized into several key areas, including the main menu, navigation menu, and page panels.
|
||||
|
||||
### Main Menu
|
||||
|
||||
The main menu is located at the top of the interface and provides access to the primary sections of the system:
|
||||
|
||||
{{ image("concepts/ui_main_menu.png", "Main Menu") }}
|
||||
|
||||
From the main menu, users can access the following items:
|
||||
|
||||
- [Navigation Menu](#navigation-menu)
|
||||
- [Dashboard](#dashboard)
|
||||
- [Global Search](./global_search.md)
|
||||
- [Spotlight](#spotlight)
|
||||
- [Barcode Scanning](#barcode-scanning)
|
||||
- [Notifications](#notifications)
|
||||
- [User Menu](#user-menu)
|
||||
|
||||
As well as allowing navigation to the following main sections:
|
||||
|
||||
- [Parts](../../part/index.md)
|
||||
- [Stock](../../stock/index.md)
|
||||
- [Manufacturing](../../manufacturing/index.md)
|
||||
- [Purchasing](../../purchasing/index.md)
|
||||
- [Sales](../../sales/index.md)
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
The global navigation menu is located on the left-hand side of the interface and provides access to the various sections of the system.
|
||||
|
||||
{{ image("concepts/ui_navigation_menu.png", "Navigation Menu") }}
|
||||
|
||||
The navigation menu is organized into several key areas, including:
|
||||
|
||||
- **Navigation:** Provides access to the main sections of the system, including Parts, Stock, Manufacturing, Purchasing, and Sales.
|
||||
- **Settings:** Quick access to user settings, system settings, and the admin center.
|
||||
- **Actions:** Provides quick access to commonly used actions
|
||||
- **Documentation:** Links to the online documentation.
|
||||
- **About:** InvenTree version and license information.
|
||||
|
||||
### User Menu
|
||||
|
||||
The user menu is located in the top-right corner of the interface and provides access to user-specific settings and actions.
|
||||
|
||||
{{ image("concepts/ui_user_menu.png", "User Menu") }}
|
||||
|
||||
The user menu provides access to the following items:
|
||||
|
||||
- **User Settings:** Access to [user settings](../../settings/user.md).
|
||||
- **System Settings:** Access to [global settings](../../settings/global.md) settings. *Note: Access to system settings may be restricted based on user permissions.*
|
||||
- **Admin Center:** Access to the [admin center](../../settings/admin.md#admin-center) for operational administration
|
||||
- **Database Admin Interface:** Low level administration via the [Database Admin Interface](../../settings/db_admin.md). *Note: Access may be restricted based on user permissions.*
|
||||
- **Change Color Mode:** Toggle between light and dark color modes.
|
||||
- **About InvenTree:** View version and license information about InvenTree.
|
||||
- **Logout:** Log out of the InvenTree system.
|
||||
|
||||
### Page Panels
|
||||
|
||||
Most detail pages views within InvenTree are organized into panels, which provide a structured layout for displaying information and actions related to the current page.
|
||||
|
||||
Panels are arranged in a vertical stack on the left side of the page, with the main content area on the right. Each panel contains related information and actions, allowing users to easily navigate and interact with the content.
|
||||
|
||||
{{ image("concepts/ui_panels.png", "Page Panels") }}
|
||||
|
||||
#### Collapse Panels
|
||||
|
||||
The panel sidebar can be collapsed to provide more space for the main content area. To collapse or expand the panel sidebar, click the collapse icon located at the bottom of the sidebar. To expand the sidebar again, click the expand icon that appears when the sidebar is collapsed.
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
On some pages, a breadcrumb navigation trail is provided at the top of the page, just below the main menu. Breadcrumbs provide a visual representation of the user's current location within the system and allow for easy navigation back to previous pages.
|
||||
|
||||
{{ image("concepts/ui_breadcrumbs.png", "Breadcrumb Navigation") }}
|
||||
|
||||
### Navigation Tree
|
||||
|
||||
On some pages, a navigation tree is provided on the left-hand side of the page, next to the breadcrumbs. The navigation tree provides a hierarchical view of the current section of the system, allowing users to quickly navigate to related pages and sections.
|
||||
|
||||
Click on the navigation tree icon to expand the tree and view the available navigation options:
|
||||
|
||||
{{ image("concepts/ui_navigation_tree.png", "Navigation Tree") }}
|
||||
|
||||
#### Searching
|
||||
|
||||
The navigation tree includes a search bar at the top of the panel. Typing into the search bar filters the tree to show only entries that match the search query. When a search is active, all matching results are expanded and displayed in a flat list. Clearing the search field returns the tree to its normal browsing mode.
|
||||
|
||||
#### Highlight Selected Entry
|
||||
|
||||
The currently selected entry in the navigation tree is highlighted with a distinct background color, making it easy to identify the active page or section within the hierarchy.
|
||||
|
||||
#### Auto-Expand to Selected Entry
|
||||
|
||||
When the navigation tree is opened, it automatically expands to reveal the currently selected entry. All ancestor nodes in the hierarchy are expanded so the active entry is immediately visible, without requiring manual navigation through the tree.
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard provides a customizable landing page for users when they log in to the system. The dashboard can be configured to display a variety of widgets and information panels, providing users with quick access to important data and actions.
|
||||
|
||||
{{ image("concepts/ui_dashboard.png", "Dashboard") }}
|
||||
|
||||
### Editing Layout
|
||||
|
||||
To edit the layout (add, remove, or rearrange widgets) of the dashboard, open the dashboard context menu (located at the top-right corner of the dashboard) and view the available options:
|
||||
|
||||
{{ image("concepts/ui_dashboard_edit.png", "Dashboard Context Menu") }}
|
||||
|
||||
### Custom Widgets
|
||||
|
||||
In addition to the set of built-in widgets provided by InvenTree, custom dashboard widgets can be implemented using [plugins](../../plugins/mixins/ui.md#dashboard-items). This allows users to create personalized dashboard experiences tailored to their specific needs and workflows.
|
||||
|
||||
## Tables
|
||||
|
||||
Information throughout the InvenTree interface is often presented in tabular format. Table views support a wide range of features, including pagination, sorting, filtering, and data export.
|
||||
|
||||
Read more about working with table views on the dedicated [Tables](./tables.md) page.
|
||||
|
||||
## Preview Panels
|
||||
|
||||
Rather than navigating directly to an item's detail page, clicking on a table row can instead open a "preview" drawer, providing a quick summary of that item without leaving the current view.
|
||||
|
||||
Read more about this optional feature on the dedicated [Preview Panels](./preview_panels.md) page.
|
||||
|
||||
## Forms
|
||||
|
||||
Data entry and editing within InvenTree is typically performed through the use of forms, which provide a structured interface for inputting and modifying data.
|
||||
|
||||
Read more about working with forms on the dedicated [Forms](./forms.md) page.
|
||||
|
||||
## Global Search
|
||||
|
||||
Accessible from the [main menu](#main-menu), the global search functionality allows users to quickly find specific items or information within the InvenTree system.
|
||||
|
||||
Read more about the global search feature on the dedicated [Global Search](./global_search.md) page.
|
||||
|
||||
## Spotlight
|
||||
|
||||
The user interface features a "spotlight" search functionality, which provides a quick and efficient way to access common actions or navigate to specific pages within the InvenTree system. The spotlight search is designed to enhance user productivity by allowing users to quickly find and execute actions without needing to navigate through menus or remember specific page locations.
|
||||
|
||||
{{ image("concepts/ui_spotlight.png", "Spotlight Search") }}
|
||||
|
||||
### Open Spotlight
|
||||
|
||||
To open the "spotlight" search, click on the "spotlight" icon located in the main menu at the top of the interface. This will open the spotlight search menu, allowing you to enter search queries and view available actions.
|
||||
|
||||
Alternatively, the spotlight search can be opened using the keyboard shortcut `Ctrl + K` (or `Cmd + K` on macOS), providing a quick and convenient way to access the spotlight functionality without needing to click on the menu icon.
|
||||
|
||||
### Disable Spotlight
|
||||
|
||||
Users may opt to disable the spotlight search functionality if they do not find it useful or prefer not to use it. To disable the spotlight search, navigate to your [user settings](../../settings/user.md) and locate the option to disable the spotlight feature. Once disabled, the spotlight search will no longer be accessible from the main menu or via keyboard shortcuts.
|
||||
|
||||
## Copy Button
|
||||
|
||||
Many fields within the InvenTree user interface include a "copy" button, which allows users to quickly copy the value of that field to their clipboard. This is particularly useful for fields that contain important identifiers, such as part numbers, stock item codes, or other relevant data that may need to be easily copied and pasted elsewhere.
|
||||
|
||||
!!! important "Secure Context"
|
||||
The "copy" button functionality relies on the browser's clipboard API, which may not be available in all contexts (e.g. if the user is accessing the InvenTree interface via a non-https connection, or through an embedded iframe or a non-standard browser). In such cases, the "copy" button may not function as intended.
|
||||
|
||||
## User Permissions
|
||||
|
||||
Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization.
|
||||
|
||||
If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability.
|
||||
|
||||
## Language Support
|
||||
|
||||
The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language.
|
||||
|
||||
The default system language can be configured by the system administrator in the [server configuration options](../../start/config.md#basic-options).
|
||||
|
||||
Additionally, users can select their preferred language in their [user settings](../../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
---
|
||||
title: Preview Panels
|
||||
---
|
||||
|
||||
## Preview Panels
|
||||
|
||||
Many [table views](./tables.md) in InvenTree link each row to the detail page for the underlying item. By default, clicking on a row navigates directly to that detail page.
|
||||
|
||||
If the preview panel feature is enabled, clicking on a row instead opens a "preview" drawer on the right-hand side of the screen. The preview drawer displays a summary of the selected item, without navigating away from the current table view.
|
||||
|
||||
{{ image("concepts/ui_preview_drawer.png", "Preview Drawer") }}
|
||||
|
||||
## Enabling the Preview Panel
|
||||
|
||||
The preview panel is disabled by default. It can be enabled on a per-user basis via the **Table Preview Panel** option, found in the *Display Options* tab of the [user settings](../../settings/user.md) page.
|
||||
|
||||
## Using the Preview Panel
|
||||
|
||||
Once enabled, clicking on a row in a supported table opens the preview drawer for that item, rather than navigating to its detail page.
|
||||
|
||||
### Viewing Full Details
|
||||
|
||||
The preview drawer title includes an arrow icon, linking to the full detail page for the previewed item. Click on the title (or the arrow icon) to navigate to the detail page and close the drawer.
|
||||
|
||||
{{ image("concepts/ui_preview_details_link.png", "View Details Link") }}
|
||||
|
||||
### Following Links
|
||||
|
||||
Any link within the preview drawer (for example, a link to a related part or category) can be clicked to navigate directly to that page. The preview drawer closes automatically when a link is followed.
|
||||
|
||||
### Bypassing the Preview
|
||||
|
||||
To navigate directly to an item's detail page without opening the preview drawer, hold `Ctrl` (or `Cmd` on macOS) while clicking the row, or middle-click the row to open the detail page in a new tab.
|
||||
|
||||
### Closing the Preview
|
||||
|
||||
The preview drawer can be closed by clicking the close button, clicking outside the drawer, or pressing `Escape`.
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
---
|
||||
title: Tables
|
||||
---
|
||||
|
||||
## Table Views
|
||||
|
||||
Information throughout the InvenTree interface is often presented in tabular format, allowing users to easily view and interact with large datasets. Tables are designed to be flexible and customizable, providing a range of features to enhance the user experience.
|
||||
|
||||
{{ image("concepts/ui_table.png", "Table View") }}
|
||||
|
||||
### Pagination
|
||||
|
||||
The pagination controls are located at the bottom of the table, allowing users to navigate through large datasets by moving between pages. Users can also adjust the number of rows displayed per page using the pagination settings.
|
||||
|
||||
### Row Selection
|
||||
|
||||
For tables where data selection is supported, a checkbox is provided at the left-hand side of each row, allowing users to select one or more rows for further actions. A master checkbox is also provided in the table header, allowing users to quickly select or deselect all rows in the table.
|
||||
|
||||
!!! info "Pagination and Row Selection"
|
||||
When using the "master select" checkbox to select all rows, only the rows on the current page will be selected.
|
||||
|
||||
{{ image("concepts/ui_table_row_selection.png", "Row Selection") }}
|
||||
|
||||
### Table Actions
|
||||
|
||||
A particular table view may have a set of actions associated with it, which are typically located at the top-left corner of the table. These actions may include options for adding new entries, or performing bulk actions on [selected rows](#row-selection).
|
||||
|
||||
{{ image("concepts/ui_table_actions.png", "Table Actions") }}
|
||||
|
||||
### Searching
|
||||
|
||||
Some tables support searching, allowing users to quickly find specific entries within the dataset. The search bar is located at the top-right corner of the table view:
|
||||
|
||||
{{ image("concepts/ui_table_search.png", "Table Search") }}
|
||||
|
||||
### Column Selection
|
||||
|
||||
Some tables allow the user to toggle the visibility of certain columns to, enabling a more customized view of the data.
|
||||
|
||||
Column selection is accessed via the "Select Columns" menu, located to the top-right of the table view:
|
||||
|
||||
{{ image("concepts/ui_table_column_selection.png", "Column Selection") }}
|
||||
|
||||
### Filtering
|
||||
|
||||
The dataset (which is fetched dynamically from the server via an API request) can be filtered by providing query parameters to the API endpoint.
|
||||
|
||||
Select the "table filters" button to open the filter selection menu
|
||||
|
||||
{{ image("concepts/ui_table_filter_button.png", "Table Filter Button") }}
|
||||
|
||||
{{ image("concepts/ui_table_filter_menu.png", "Table Filter Menu") }}
|
||||
|
||||
Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view.
|
||||
|
||||
#### Column Filters
|
||||
|
||||
Many table columns expose an inline filter icon directly in the column header, providing a quick way to filter by that column without opening the full filter drawer. Columns that support filtering display a small filter icon alongside the column title. The icon is highlighted when a filter for that column is currently active, giving an at-a-glance indication of which columns have active filters.
|
||||
|
||||
Clicking the icon opens a compact popover anchored to the column header:
|
||||
|
||||
{{ image("concepts/ui_table_column_filter_popover.png", "Column Filter Popover") }}
|
||||
|
||||
**Single-filter columns** — for columns linked to one filter (e.g. *Active*, *Has IPN*, *Status*), selecting a value immediately applies the filter and the popover closes automatically.
|
||||
|
||||
**Range columns** — for columns that represent a range concept (e.g. *Start Date*, *Target Date*, *Creation Date*), the popover stays open and presents multiple controls — for example *before* and *after* date pickers — so both bounds can be set in a single interaction.
|
||||
|
||||
Once a filter is active, the popover shows a badge with the current value and a remove button (red ×) instead of the value picker. Clicking the × clears only that column's filter.
|
||||
|
||||
!!! info "Column filters and the filter drawer share the same state"
|
||||
Filters applied via a column popover appear immediately in the filter drawer's active-filter list, and filters added through the drawer are reflected in the column icons. Clearing all filters from the drawer also removes any filters set via column popovers.
|
||||
|
||||
#### Saved Filter Groups
|
||||
|
||||
Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually.
|
||||
|
||||
The **Saved Filter Groups** panel is displayed at the bottom of the filter drawer. When one or more filters are active, a **Save current filters** button is available. Clicking it opens an inline name input — enter a name and press Enter (or click the confirm icon) to save the group. Press Escape or click the cancel icon to discard.
|
||||
|
||||
{{ image("concepts/ui_table_filter_group.png", "Filter Groups") }}
|
||||
|
||||
Previously saved filter groups are listed in the panel. Each entry shows the group name alongside two actions:
|
||||
|
||||
- **Load** (green reload icon): Replaces the current active filters with the filters stored in that group. The table immediately re-fetches data using the restored filters.
|
||||
- **Delete** (red × icon): Permanently removes the saved filter group.
|
||||
|
||||
Saved filter groups are stored in the browser's local storage and are specific to each table or calendar view, so groups saved for one view are not available in another. They persist across local browser sessions until explicitly deleted. Filter groups are not shared to other devices.
|
||||
|
||||
!!! info "Loading a filter group replaces active filters"
|
||||
Loading a saved filter group replaces all currently active filters with those stored in the group. Any unsaved active filters will be overwritten.
|
||||
|
||||
### Data Sorting
|
||||
|
||||
Some table columns support data sorting, allowing the dataset to be sorted in ascending or descending order based on the values in that column. To sort a column, click on the column header. Clicking the column header again will toggle the sort order between ascending and descending. The current sort order is indicated by an arrow icon in the column header.
|
||||
|
||||
{{ image("concepts/ui_table_sorting.png", "Data Sorting") }}
|
||||
|
||||
### Data Export
|
||||
|
||||
Some tables support downloading of the dataset in various formats (e.g. CSV, Excel, PDF). If data download is available for a given table, the "export data" button will be located at the top-right corner of the table view.
|
||||
|
||||
This opens the "Export Data" form, which allows the user to select the desired file format for download, as well as any additional options related to the data export.
|
||||
|
||||
{{ image("concepts/ui_table_download.png", "Data Download") }}
|
||||
|
||||
### Row Actions
|
||||
|
||||
In some tables, there may be specific actions associated with individual rows, allowing users to perform actions directly on a particular entry in the dataset. Row actions are typically accessed via an "actions" menu located at the right-hand side of each row.
|
||||
|
||||
{{ image("concepts/ui_table_row_actions.png", "Row Actions") }}
|
||||
|
||||
### Right-Click Context Menu
|
||||
|
||||
For rows that support row actions, a right-click context menu is also available, providing quick access to the same set of actions without needing to click on the "actions" menu.
|
||||
|
||||
{{ image("concepts/ui_table_context_menu.png", "Right-Click Context Menu") }}
|
||||
|
||||
### Row Navigation
|
||||
|
||||
For tables which reference other objects within the system, clicking on a row will navigate to the detail page for that particular entry. For example, clicking on a row in the "Part" table will navigate to the detail page for that specific part.
|
||||
|
||||
If the [preview panel](./preview_panels.md) feature is enabled, clicking on a row instead opens a preview drawer for that entry, rather than navigating directly to its detail page.
|
||||
|
||||
## Calendar Views
|
||||
|
||||
Some [table views](#table-views) associated with various order types can be switched to a calendar view, which provides a visual representation of data based on date fields. The calendar view allows users to easily see and interact with data that is organized by date, such as scheduled tasks, events, or deadlines.
|
||||
|
||||
To switch to the "calendar view" (for a table which supports it), click on the "calendar view" button located above and to the right of the table view:
|
||||
|
||||
{{ image("concepts/ui_calendar_select.png", "Calendar View Button") }}
|
||||
|
||||
This will display the data in a calendar format:
|
||||
|
||||
{{ image("concepts/ui_calendar_view.png", "Calendar View") }}
|
||||
|
||||
### Calendar Horizon
|
||||
|
||||
The calendar view provides a configurable "horizon" setting, which allows users to adjust the number of months displayed in the calendar view.
|
||||
|
||||
## Parametric Views
|
||||
|
||||
Some [table views](#table-views) can be switched to a parametric view, which provides a visual representation of data based on specific parameters or attributes. The parametric view allows users to easily see and interact with data that is organized by certain characteristics, such as categories, types, or other relevant attributes.
|
||||
|
||||
To switch to the "parametric view" (for a table which supports it), click on the "parametric view" button located above and to the right of the table view:
|
||||
|
||||
{{ image("concepts/ui_parametric_select.png", "Parametric View Button") }}
|
||||
|
||||
This will display the data in a parametric format:
|
||||
|
||||
{{ image("concepts/ui_parametric_view.png", "Parametric View") }}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
---
|
||||
title: User Interface
|
||||
---
|
||||
|
||||
## User Interface
|
||||
|
||||
The InvenTree user interface is designed to be intuitive and user-friendly, providing easy access to the various features and functions of the system. The interface is organized into several key components, including navigation menus, settings, forms, tables, search functionality, and more.
|
||||
|
||||
The interface is designed for large-format displays, and as such is explicitly *not* optimized for mobile devices. However, the interface is responsive and should work on a wide range of desktop screen sizes.
|
||||
|
||||
## Navigation
|
||||
|
||||
Navigation throughout the InvenTree interface is designed to be straightforward and efficient, allowing users to quickly access the various sections and features of the system. The navigation is organized into several key areas, including the main menu, navigation menu, and page panels.
|
||||
|
||||
### Main Menu
|
||||
|
||||
The main menu is located at the top of the interface and provides access to the primary sections of the system:
|
||||
|
||||
{{ image("concepts/ui_main_menu.png", "Main Menu") }}
|
||||
|
||||
From the main menu, users can access the following items:
|
||||
|
||||
- [Navigation Menu](#navigation-menu)
|
||||
- [Dashboard](#dashboard)
|
||||
- [Global Search](#search)
|
||||
- [Spotlight](#spotlight)
|
||||
- [Barcode Scanning](#barcode-scanning)
|
||||
- [Notifications](#notifications)
|
||||
- [User Menu](#user-menu)
|
||||
|
||||
As well as allowing navigation to the following main sections:
|
||||
|
||||
- [Parts](../part/index.md)
|
||||
- [Stock](../stock/index.md)
|
||||
- [Manufacturing](../manufacturing/index.md)
|
||||
- [Purchasing](../purchasing/index.md)
|
||||
- [Sales](../sales/index.md)
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
The global navigation menu is located on the left-hand side of the interface and provides access to the various sections of the system.
|
||||
|
||||
{{ image("concepts/ui_navigation_menu.png", "Navigation Menu") }}
|
||||
|
||||
The navigation menu is organized into several key areas, including:
|
||||
|
||||
- **Navigation:** Provides access to the main sections of the system, including Parts, Stock, Manufacturing, Purchasing, and Sales.
|
||||
- **Settings:** Quick access to user settings, system settings, and the admin interface.
|
||||
- **Actions:** Provides quick access to commonly used actions
|
||||
- **Documentation:** Links to the online documentation.
|
||||
- **About:** InvenTree version and license information.
|
||||
|
||||
### User Menu
|
||||
|
||||
The user menu is located in the top-right corner of the interface and provides access to user-specific settings and actions.
|
||||
|
||||
{{ image("concepts/ui_user_menu.png", "User Menu") }}
|
||||
|
||||
The user menu provides access to the following items:
|
||||
|
||||
- **User Settings:** Access to [user settings](../settings/user.md).
|
||||
- **System Settings:** Access to [global settings](../settings/global.md) settings. *Note: Access to system settings may be restricted based on user permissions.*
|
||||
- **Admin Interface:** Access to the [admin interface](../settings/admin.md) for data management. *Note: Access to the admin interface may be restricted based on user permissions.*
|
||||
- **Change Color Mode:** Toggle between light and dark color modes.
|
||||
- **About InvenTree:** View version and license information about InvenTree.
|
||||
- **Logout:** Log out of the InvenTree system.
|
||||
|
||||
### Page Panels
|
||||
|
||||
Most detail pages views within InvenTree are organized into panels, which provide a structured layout for displaying information and actions related to the current page.
|
||||
|
||||
Panels are arranged in a vertical stack on the left side of the page, with the main content area on the right. Each panel contains related information and actions, allowing users to easily navigate and interact with the content.
|
||||
|
||||
{{ image("concepts/ui_panels.png", "Page Panels") }}
|
||||
|
||||
#### Collapse Panels
|
||||
|
||||
The panel sidebar can be collapsed to provide more space for the main content area. To collapse or expand the panel sidebar, click the collapse icon located at the bottom of the sidebar. To expand the sidebar again, click the expand icon that appears when the sidebar is collapsed.
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
On some pages, a breadcrumb navigation trail is provided at the top of the page, just below the main menu. Breadcrumbs provide a visual representation of the user's current location within the system and allow for easy navigation back to previous pages.
|
||||
|
||||
{{ image("concepts/ui_breadcrumbs.png", "Breadcrumb Navigation") }}
|
||||
|
||||
### Navigation Tree
|
||||
|
||||
On some pages, a navigation tree is provided on the left-hand side of the page, next to the breadcrumbs. The navigation tree provides a hierarchical view of the current section of the system, allowing users to quickly navigate to related pages and sections.
|
||||
|
||||
Click on the navigation tree icon to expand the tree and view the available navigation options:
|
||||
|
||||
{{ image("concepts/ui_navigation_tree.png", "Navigation Tree") }}
|
||||
|
||||
#### Searching
|
||||
|
||||
The navigation tree includes a search bar at the top of the panel. Typing into the search bar filters the tree to show only entries that match the search query. When a search is active, all matching results are expanded and displayed in a flat list. Clearing the search field returns the tree to its normal browsing mode.
|
||||
|
||||
#### Highlight Selected Entry
|
||||
|
||||
The currently selected entry in the navigation tree is highlighted with a distinct background color, making it easy to identify the active page or section within the hierarchy.
|
||||
|
||||
#### Auto-Expand to Selected Entry
|
||||
|
||||
When the navigation tree is opened, it automatically expands to reveal the currently selected entry. All ancestor nodes in the hierarchy are expanded so the active entry is immediately visible, without requiring manual navigation through the tree.
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard provides a customizable landing page for users when they log in to the system. The dashboard can be configured to display a variety of widgets and information panels, providing users with quick access to important data and actions.
|
||||
|
||||
{{ image("concepts/ui_dashboard.png", "Dashboard") }}
|
||||
|
||||
### Editing Layout
|
||||
|
||||
To edit the layout (add, remove, or rearrange widgets) of the dashboard, open the dashboard context menu (located at the top-right corner of the dashboard) and view the available options:
|
||||
|
||||
{{ image("concepts/ui_dashboard_edit.png", "Dashboard Context Menu") }}
|
||||
|
||||
### Custom Widgets
|
||||
|
||||
In addition to the set of built-in widgets provided by InvenTree, custom dashboard widgets can be implemented using [plugins](../plugins/mixins/ui.md#dashboard-items). This allows users to create personalized dashboard experiences tailored to their specific needs and workflows.
|
||||
|
||||
## Table Views
|
||||
|
||||
Information throughout the InvenTree interface is often presented in tabular format, allowing users to easily view and interact with large datasets. Tables are designed to be flexible and customizable, providing a range of features to enhance the user experience.
|
||||
|
||||
{{ image("concepts/ui_table.png", "Table View") }}
|
||||
|
||||
### Pagination
|
||||
|
||||
The pagination controls are located at the bottom of the table, allowing users to navigate through large datasets by moving between pages. Users can also adjust the number of rows displayed per page using the pagination settings.
|
||||
|
||||
### Row Selection
|
||||
|
||||
For tables where data selection is supported, a checkbox is provided at the left-hand side of each row, allowing users to select one or more rows for further actions. A master checkbox is also provided in the table header, allowing users to quickly select or deselect all rows in the table.
|
||||
|
||||
!!! info "Pagination and Row Selection"
|
||||
When using the "master select" checkbox to select all rows, only the rows on the current page will be selected.
|
||||
|
||||
{{ image("concepts/ui_table_row_selection.png", "Row Selection") }}
|
||||
|
||||
### Table Actions
|
||||
|
||||
A particular table view may have a set of actions associated with it, which are typically located at the top-left corner of the table. These actions may include options for adding new entries, or performing bulk actions on [selected rows](#row-selection).
|
||||
|
||||
{{ image("concepts/ui_table_actions.png", "Table Actions") }}
|
||||
|
||||
### Searching
|
||||
|
||||
Some tables support searching, allowing users to quickly find specific entries within the dataset. The search bar is located at the top-right corner of the table view:
|
||||
|
||||
{{ image("concepts/ui_table_search.png", "Table Search") }}
|
||||
|
||||
### Column Selection
|
||||
|
||||
Some tables allow the user to toggle the visibility of certain columns to, enabling a more customized view of the data.
|
||||
|
||||
Column selection is accessed via the "Select Columns" menu, located to the top-right of the table view:
|
||||
|
||||
{{ image("concepts/ui_table_column_selection.png", "Column Selection") }}
|
||||
|
||||
### Filtering
|
||||
|
||||
The dataset (which is fetched dynamically from the server via an API request) can be filtered by providing query parameters to the API endpoint.
|
||||
|
||||
Select the "table filters" button to open the filter selection menu
|
||||
|
||||
{{ image("concepts/ui_table_filter_button.png", "Table Filter Button") }}
|
||||
|
||||
{{ image("concepts/ui_table_filter_menu.png", "Table Filter Menu") }}
|
||||
|
||||
Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view.
|
||||
|
||||
#### Column Filters
|
||||
|
||||
Many table columns expose an inline filter icon directly in the column header, providing a quick way to filter by that column without opening the full filter drawer. Columns that support filtering display a small filter icon alongside the column title. The icon is highlighted when a filter for that column is currently active, giving an at-a-glance indication of which columns have active filters.
|
||||
|
||||
Clicking the icon opens a compact popover anchored to the column header:
|
||||
|
||||
{{ image("concepts/ui_table_column_filter_popover.png", "Column Filter Popover") }}
|
||||
|
||||
**Single-filter columns** — for columns linked to one filter (e.g. *Active*, *Has IPN*, *Status*), selecting a value immediately applies the filter and the popover closes automatically.
|
||||
|
||||
**Range columns** — for columns that represent a range concept (e.g. *Start Date*, *Target Date*, *Creation Date*), the popover stays open and presents multiple controls — for example *before* and *after* date pickers — so both bounds can be set in a single interaction.
|
||||
|
||||
Once a filter is active, the popover shows a badge with the current value and a remove button (red ×) instead of the value picker. Clicking the × clears only that column's filter.
|
||||
|
||||
!!! info "Column filters and the filter drawer share the same state"
|
||||
Filters applied via a column popover appear immediately in the filter drawer's active-filter list, and filters added through the drawer are reflected in the column icons. Clearing all filters from the drawer also removes any filters set via column popovers.
|
||||
|
||||
#### Saved Filter Groups
|
||||
|
||||
Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually.
|
||||
|
||||
The **Saved Filter Groups** panel is displayed at the bottom of the filter drawer. When one or more filters are active, a **Save current filters** button is available. Clicking it opens an inline name input — enter a name and press Enter (or click the confirm icon) to save the group. Press Escape or click the cancel icon to discard.
|
||||
|
||||
{{ image("concepts/ui_table_filter_group.png", "Filter Groups") }}
|
||||
|
||||
Previously saved filter groups are listed in the panel. Each entry shows the group name alongside two actions:
|
||||
|
||||
- **Load** (green reload icon): Replaces the current active filters with the filters stored in that group. The table immediately re-fetches data using the restored filters.
|
||||
- **Delete** (red × icon): Permanently removes the saved filter group.
|
||||
|
||||
Saved filter groups are stored in the browser's local storage and are specific to each table or calendar view, so groups saved for one view are not available in another. They persist across local browser sessions until explicitly deleted. Filter groups are not shared to other devices.
|
||||
|
||||
!!! info "Loading a filter group replaces active filters"
|
||||
Loading a saved filter group replaces all currently active filters with those stored in the group. Any unsaved active filters will be overwritten.
|
||||
|
||||
### Data Sorting
|
||||
|
||||
Some table columns support data sorting, allowing the dataset to be sorted in ascending or descending order based on the values in that column. To sort a column, click on the column header. Clicking the column header again will toggle the sort order between ascending and descending. The current sort order is indicated by an arrow icon in the column header.
|
||||
|
||||
{{ image("concepts/ui_table_sorting.png", "Data Sorting") }}
|
||||
|
||||
### Data Export
|
||||
|
||||
Some tables support downloading of the dataset in various formats (e.g. CSV, Excel, PDF). If data download is available for a given table, the "export data" button will be located at the top-right corner of the table view.
|
||||
|
||||
This opens the "Export Data" form, which allows the user to select the desired file format for download, as well as any additional options related to the data export.
|
||||
|
||||
{{ image("concepts/ui_table_download.png", "Data Download") }}
|
||||
|
||||
### Row Actions
|
||||
|
||||
In some tables, there may be specific actions associated with individual rows, allowing users to perform actions directly on a particular entry in the dataset. Row actions are typically accessed via an "actions" menu located at the right-hand side of each row.
|
||||
|
||||
{{ image("concepts/ui_table_row_actions.png", "Row Actions") }}
|
||||
|
||||
### Right-Click Context Menu
|
||||
|
||||
For rows that support row actions, a right-click context menu is also available, providing quick access to the same set of actions without needing to click on the "actions" menu.
|
||||
|
||||
{{ image("concepts/ui_table_context_menu.png", "Right-Click Context Menu") }}
|
||||
|
||||
### Row Navigation
|
||||
|
||||
For tables which reference other objects within the system, clicking on a row will navigate to the detail page for that particular entry. For example, clicking on a row in the "Part" table will navigate to the detail page for that specific part.
|
||||
|
||||
## Calendar Views
|
||||
|
||||
Some [table views](#table-views) associated with various order types can be switched to a calendar view, which provides a visual representation of data based on date fields. The calendar view allows users to easily see and interact with data that is organized by date, such as scheduled tasks, events, or deadlines.
|
||||
|
||||
To switch to the "calendar view" (for a table which supports it), click on the "calendar view" button located above and to the right of the table view:
|
||||
|
||||
{{ image("concepts/ui_calendar_select.png", "Calendar View Button") }}
|
||||
|
||||
This will display the data in a calendar format:
|
||||
|
||||
{{ image("concepts/ui_calendar_view.png", "Calendar View") }}
|
||||
|
||||
### Calendar Horizon
|
||||
|
||||
The calendar view provides a configurable "horizon" setting, which allows users to adjust the number of months displayed in the calendar view.
|
||||
|
||||
## Parametric Views
|
||||
|
||||
Some [table views](#table-views) can be switched to a parametric view, which provides a visual representation of data based on specific parameters or attributes. The parametric view allows users to easily see and interact with data that is organized by certain characteristics, such as categories, types, or other relevant attributes.
|
||||
|
||||
To switch to the "parametric view" (for a table which supports it), click on the "parametric view" button located above and to the right of the table view:
|
||||
|
||||
{{ image("concepts/ui_parametric_select.png", "Parametric View Button") }}
|
||||
|
||||
This will display the data in a parametric format:
|
||||
|
||||
{{ image("concepts/ui_parametric_view.png", "Parametric View") }}
|
||||
|
||||
## Forms
|
||||
|
||||
Data entry and editing within InvenTree is typically performed through the use of forms, which provide a structured interface for inputting and modifying data. Forms are designed to be user-friendly and efficient, allowing users to quickly enter and update information within the system.
|
||||
|
||||
Forms are typically displayed as a modal dialog, separated into multiple sections and fields.
|
||||
|
||||
### Data Creation
|
||||
|
||||
Example: Creating a new part via the "Add Part" form:
|
||||
|
||||
{{ image("concepts/ui_form_add_part.png", "Add Part Button") }}
|
||||
|
||||
On several forms is displayed option "Keep form open" in bottom part of the form on left side of Submit button (option is visible on the screenshot above). When this switch is turned on, form window is not closed after submit and filled form data is not reset. This is useful for creating more entries at one time with similar properties (e.g. only different number in name).
|
||||
|
||||
### Data Editing
|
||||
|
||||
Example: Editing an existing purchase order via the "Edit Purchase Order" form:
|
||||
|
||||
{{ image("concepts/ui_form_edit_po.png", "Edit Purchase Order") }}
|
||||
|
||||
### Confirm Actions
|
||||
|
||||
Many actions within InvenTree require user confirmation before they can be executed. This is typically implemented through the use of confirmation dialogs, which prompt the user to confirm their intention before proceeding with the action.
|
||||
|
||||
{{ image("concepts/ui_form_hold_po.png", "Confirmation Dialog") }}
|
||||
|
||||
## Global Search
|
||||
|
||||
Accessible from the [main menu](#main-menu), the global search functionality allows users to quickly find specific items or information within the InvenTree system. The search icon is located at the top of the interface and provides a convenient way to search across all sections of the system.
|
||||
|
||||
Clicking on the "search" icon (in the menu bar) opens the search menu, which allows users to enter search queries and view results from across the system.
|
||||
|
||||
{{ image("concepts/ui_global_search.png", "Global Search") }}
|
||||
|
||||
Search results are organized by category (e.g. Parts, Stock, Manufacturing, etc.) and provide quick access to the relevant pages for each search result.
|
||||
|
||||
### Detail View
|
||||
|
||||
To navigate to the detail page for a particular search result, simply click on the desired result from the search results list. This will take you directly to the relevant page within the InvenTree system, allowing you to view and interact with the specific item or information you were searching for.
|
||||
|
||||
### Full Results
|
||||
|
||||
The "global search" menu provides a limited set of search results for each category, typically showing the most relevant or recent results. To view the full set of search results for a particular category, click on the "View all results" button located at the top-left of the search results list for that category:
|
||||
|
||||
{{ image("concepts/ui_global_search_view_all.png", "View Full Search Results") }}
|
||||
|
||||
### Collapse Result Groups
|
||||
|
||||
To collapse a particular category of search results in the global search menu, click on the "collapse" icon located at the top-right corner of the search results list for that category. This will hide the search results for that category, allowing you to focus on other categories or search results.
|
||||
|
||||
### Remove Result Groups
|
||||
|
||||
To remove a particular category of search results from the global search menu, click on the "remove" icon located at the top-right corner of the search results list for that category.
|
||||
|
||||
## Spotlight
|
||||
|
||||
The user interface features a "spotlight" search functionality, which provides a quick and efficient way to access common actions or navigate to specific pages within the InvenTree system. The spotlight search is designed to enhance user productivity by allowing users to quickly find and execute actions without needing to navigate through menus or remember specific page locations.
|
||||
|
||||
{{ image("concepts/ui_spotlight.png", "Spotlight Search") }}
|
||||
|
||||
### Open Spotlight
|
||||
|
||||
To open the "spotlight" search, click on the "spotlight" icon located in the main menu at the top of the interface. This will open the spotlight search menu, allowing you to enter search queries and view available actions.
|
||||
|
||||
Alternatively, the spotlight search can be opened using the keyboard shortcut `Ctrl + K` (or `Cmd + K` on macOS), providing a quick and convenient way to access the spotlight functionality without needing to click on the menu icon.
|
||||
|
||||
### Disable Spotlight
|
||||
|
||||
Users may opt to disable the spotlight search functionality if they do not find it useful or prefer not to use it. To disable the spotlight search, navigate to your [user settings](../settings/user.md) and locate the option to disable the spotlight feature. Once disabled, the spotlight search will no longer be accessible from the main menu or via keyboard shortcuts.
|
||||
|
||||
## Copy Button
|
||||
|
||||
Many fields within the InvenTree user interface include a "copy" button, which allows users to quickly copy the value of that field to their clipboard. This is particularly useful for fields that contain important identifiers, such as part numbers, stock item codes, or other relevant data that may need to be easily copied and pasted elsewhere.
|
||||
|
||||
!!! important "Secure Context"
|
||||
The "copy" button functionality relies on the browser's clipboard API, which may not be available in all contexts (e.g. if the user is accessing the InvenTree interface via a non-https connection, or through an embedded iframe or a non-standard browser). In such cases, the "copy" button may not function as intended.
|
||||
|
||||
## User Permissions
|
||||
|
||||
Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization.
|
||||
|
||||
If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability.
|
||||
|
||||
## Language Support
|
||||
|
||||
The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language.
|
||||
|
||||
The default system language can be configured by the system administrator in the [server configuration options](../start/config.md#basic-options).
|
||||
|
||||
Additionally, users can select their preferred language in their [user settings](../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with.
|
||||
|
|
@ -184,7 +184,7 @@ django-upgrade --target-version {{ config.extra.django_version }} `find . -name
|
|||
|
||||
## Migration Files
|
||||
|
||||
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate --detect` and commit the migration files before submitting the PR.
|
||||
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.
|
||||
|
||||
*Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import json
|
|||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from distutils.version import StrictVersion # type: ignore[import]
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
here = Path(__file__).parent
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ def fetch_rtd_versions():
|
|||
print('No RTD token found - skipping RTD version fetch')
|
||||
|
||||
# Sort versions by version number
|
||||
versions = sorted(versions, key=lambda x: Version(x['version']), reverse=True)
|
||||
versions = sorted(versions, key=lambda x: StrictVersion(x['version']), reverse=True)
|
||||
|
||||
# Add "latest" version first
|
||||
if not any(x['title'] == 'latest' for x in versions):
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ In the example below, see that the *Wood Screw* line item is marked as consumabl
|
|||
|
||||
Further, in the [Build Order](./build.md) stock allocation table, we see that this line item cannot be allocated, as it is *consumable*.
|
||||
|
||||
Marking a BOM line item as consumable is a per-assembly override - it only affects how the part is treated within *this* particular BOM, and does not change the underlying part definition. A part can also be marked as consumable directly - refer to the [consumable parts documentation](../part/consumable.md) for more information on the distinction between the two, and reasons why a part may be marked as consumable.
|
||||
|
||||
### Optional BOM Line Items
|
||||
|
||||
If a BOM line item is marked as *optional*, this means that the part and quantity information is tracked in the BOM, but this line item is not required to be allocated to a [Build Order](./build.md). This may be useful for certain items which are not strictly required for the build process to be completed.
|
||||
|
|
|
|||
|
|
@ -23,9 +23,3 @@ Read more about build orders in the [Build Order documentation](./build.md).
|
|||
InvenTree allows users to allocate stock items to specific build orders, ensuring that the required components are reserved for production. This helps to prevent stock shortages and ensures that the right parts are available when needed.
|
||||
|
||||
Read more about stock allocation in the [Stock Allocation documentation](./allocate.md).
|
||||
|
||||
### Disassembly
|
||||
|
||||
The reverse process is also supported - an assembled stock item can be broken back down into its component parts, based on its BOM. This is useful for reworking or scrapping an assembly, or for splitting a bundled "kit" product purchased from a supplier into its individual components.
|
||||
|
||||
Read more about this process in the [Stock Disassembly documentation](../stock/disassemble.md).
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
title: Consumable Parts
|
||||
---
|
||||
|
||||
## Consumable Parts
|
||||
|
||||
A *Consumable* part is one which is generally used up, or expended, during a build or other process, rather than being tracked and allocated as a discrete item.
|
||||
|
||||
Consumable parts can be used to represent things such as:
|
||||
|
||||
- Glue or adhesive
|
||||
- Solder
|
||||
- Cleaning fluid
|
||||
- Fasteners or other low-value hardware
|
||||
- Other general workshop supplies
|
||||
|
||||
Marking a part as consumable is intended to help distinguish these types of supplies from other parts in the inventory, making it easier to filter and report on "real" stock items versus general consumables.
|
||||
|
||||
### Why Mark a Part as Consumable?
|
||||
|
||||
Not every item required to complete a build needs to be tracked and allocated with the same rigor as a high-value component. A part might be marked as consumable because it is:
|
||||
|
||||
- Low value, making the overhead of precise allocation not worth the effort
|
||||
- Kept in abundant stock, such that running out is unlikely to be a concern
|
||||
- Difficult to track in discrete units, such as a fluid or adhesive
|
||||
- Not something the business needs (or wants) visibility into at the build order level
|
||||
|
||||
Marking a part as consumable communicates this intent clearly wherever the part is used, without needing to remember to configure each individual BOM line item.
|
||||
|
||||
### Stock Items
|
||||
|
||||
Consumable parts can have stock items associated with them, the same as any other part. Stock levels for consumable parts can be tracked and managed as normal.
|
||||
|
||||
### Bills of Material
|
||||
|
||||
Consumable parts can be added as a subcomponent to the [Bills of Material](../manufacturing/bom.md) for an assembled part, in the same way as any other component.
|
||||
|
||||
### Build Orders
|
||||
|
||||
Unlike a regular component, a consumable part is not allocated to (or consumed by) a [Build Order](../manufacturing/build.md). When a build order is completed, stock quantities for consumable parts are not adjusted.
|
||||
|
||||
If stock levels for a consumable part need to be updated to reflect usage during a build, this must be done manually.
|
||||
|
||||
### Consumable BOM Line Items
|
||||
|
||||
Separately from the *Consumable* flag on the part itself, an individual [BOM line item](../manufacturing/bom.md#consumable-bom-line-items) can also be marked as *consumable*.
|
||||
|
||||
This allows a part which is **not** normally marked as consumable to be treated as consumable *for the purposes of a specific BOM*. For example, a fastener which is usually tracked precisely in one assembly might be treated as consumable in another assembly, without needing to change the underlying part definition.
|
||||
|
||||
In other words:
|
||||
|
||||
- The part-level *Consumable* flag is a permanent property of the part, and applies everywhere that part is used.
|
||||
- The BOM line item *Consumable* flag is a per-assembly override, and only affects how that part is treated within that particular BOM.
|
||||
|
||||
If a part is already marked as consumable, marking the corresponding BOM line item as consumable as well has no additional effect.
|
||||
|
|
@ -85,4 +85,4 @@ In addition to the primary methods for creating or importing part data, the foll
|
|||
|
||||
- [Via the REST API](../api/index.md)
|
||||
- [Using the Python library](../api/python/index.md)
|
||||
- [Within the Database Admin interface](../settings/db_admin.md)
|
||||
- [Within the Admin interface](../settings/admin.md)
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ A *Template* part is one which can have *variants* which exist underneath it. [R
|
|||
|
||||
If a part is designated as an *Assembly* it can be created (or built) from other component parts. As an example, a circuit board assembly is made using multiple electronic components, which are tracked in the system. An *Assembly* Part has a Bill of Materials (BOM) which lists all the required sub-components. [Read further information about BOM management here](../manufacturing/bom.md).
|
||||
|
||||
An assembled stock item can also be broken back down into its component parts, using the [disassembly](../stock/disassemble.md) process.
|
||||
|
||||
### Component
|
||||
|
||||
If a part is designated as a *Component* it can be used as a sub-component of an *Assembly*. [Read further information about BOM management here](../manufacturing/bom.md)
|
||||
|
|
@ -89,9 +87,6 @@ A [Purchase Order](../purchasing/purchase_order.md) allows parts to be ordered f
|
|||
|
||||
If a part is designated as *Salable* it can be sold to external customers. Setting this flag allows parts to be added to sales orders.
|
||||
|
||||
### Consumable
|
||||
|
||||
A *Consumable* part is one which is generally used up during a build or other process, rather than being tracked and allocated as a discrete item. Refer to the [consumable parts documentation](./consumable.md) for more information.
|
||||
|
||||
## Locked Parts
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ title: Part Notifications
|
|||
Users can select to receive notifications when certain events occur.
|
||||
|
||||
!!! warning "Email Configuration Required"
|
||||
External notifications require correct [email configuration](../start/config.md#email-settings). They also need to be enabled in the settings under *Notifications*.
|
||||
External notifications require correct [email configuration](../start/config.md#email-settings). They also need to be enabled in the settings under notifications`.
|
||||
|
||||
!!! warning "Valid Email Address"
|
||||
Each user must have a valid email address associated with their account to receive email notifications
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ Each price range is calculated in the [Default Currency](../concepts/pricing.md#
|
|||
Price range data is [cached in the database](#price-data-caching) when underlying pricing information changes.
|
||||
|
||||
!!! tip "Refresh Pricing"
|
||||
While pricing data is [automatically updated](#pricing-updates), the user can also manually refresh the pricing calculations manually, by pressing the "Refresh" button in the overview section.
|
||||
While pricing data is [automatically updated](#data-updates), the user can also manually refresh the pricing calculations manually, by pressing the "Refresh" button in the overview section.
|
||||
|
||||
#### Overall Pricing
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ If this tab is not visible, ensure that the *Enable Stock History* [user setting
|
|||
|
||||
### Stocktake Entry Generation
|
||||
|
||||
By default, stocktake entries are generated automatically at regular intervals (see [settings](#stocktake-settings) below). However, users can generate a stocktake entry on demand, using the *Generate Stocktake Entry* button in the *Stock History* tab:
|
||||
By default, stocktake entries are generated automatically at regular intervals (see [settings](#stock-history-settings) below). However, users can generate a stocktake entry on demand, using the *Generate Stocktake Entry* button in the *Stock History* tab:
|
||||
|
||||
{{ image("part/part_stocktake_manual.png", "Generate stocktake entry") }}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ Enable or disable stocktake functionality. Note that by default, stocktake funct
|
|||
|
||||
### Automatic Stocktake Period
|
||||
|
||||
Configure the number of days between generation of [automatic stocktake entries](#stocktake-entry-generation). If this value is set to zero, automatic stocktake entries will not be generated.
|
||||
Configure the number of days between generation of [automatic stocktake reports](#automatic-stocktake). If this value is set to zero, automatic stocktake reports will not be generated.
|
||||
|
||||
### Delete Old Stocktake Entries
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ The Part detail view page provides a detailed view of a single part in the syste
|
|||
|
||||
### Category Breadcrumb List
|
||||
|
||||
The categories of each part is displayed on the top navigation bar.
|
||||
The categories of each part is displayed on the top navigation bar as show in the above screenshot.
|
||||
[Click here](./index.md#part-category) for more information about categories.
|
||||
|
||||
## Part Details
|
||||
|
|
@ -83,10 +83,6 @@ The *Build Orders* tab shows a list of the builds for this part. It provides a v
|
|||
|
||||
The *Used In* tab displays a list of other parts that this part is used to make. This tab is only visible if the Part is a *component*.
|
||||
|
||||
### Part Pricing
|
||||
|
||||
The *Part Pricing* tab displays all available pricing information for the part, aggregated from multiple sources (internal pricing, supplier pricing, purchase history, BOM pricing, sale pricing, etc). Refer to the [part pricing documentation](./pricing.md) for further information.
|
||||
|
||||
### Suppliers
|
||||
|
||||
The *Suppliers* tab displays all the *Part Suppliers* and *Part Manufacturers* for the selected *Part*.
|
||||
|
|
@ -105,14 +101,6 @@ This tab is only displayed if the part is marked as *Purchaseable*.
|
|||
|
||||
The *Sales Orders* tab shows a list of the sales orders for this part. It provides a view for important sales order information like customer, status, creation and shipment dates.
|
||||
|
||||
### Return Orders
|
||||
|
||||
The *Return Orders* tab shows a list of the [return orders](../sales/return_order.md) which reference this part. This tab is only visible if the Part is marked as *Salable*, and the return order feature is enabled.
|
||||
|
||||
### Transfer Orders
|
||||
|
||||
The *Transfer Orders* tab shows a list of the [transfer orders](../stock/transfer_order.md) which reference this part. This tab is hidden if the Part is marked as *Virtual*, or the transfer order feature is not enabled.
|
||||
|
||||
### Stock History
|
||||
|
||||
The *Stock History* tab provide historical stock level information. Refer to the [stock history documentation](./stocktake.md) for further information.
|
||||
|
|
@ -121,10 +109,6 @@ The *Stock History* tab provide historical stock level information. Refer to the
|
|||
|
||||
If a part is marked as *testable*, the user can define tests which must be performed on any stock items which are instances of this part. [Read more about testing](./test.md).
|
||||
|
||||
### Test Results
|
||||
|
||||
The *Test Results* tab displays [test result](../stock/test.md) data uploaded against *any* stock item of this part, aggregated into a single table. This differs from the *Test Templates* tab, which configures the tests themselves rather than displaying recorded results. This tab is only visible if the part is marked as *testable*.
|
||||
|
||||
### Related Parts
|
||||
|
||||
Related Part denotes a relationship between two parts, when users want to show their usage is "related" to another part or simply emphasize a link between two parts.
|
||||
|
|
|
|||
|
|
@ -113,10 +113,10 @@ Refer to the [sample plugins]({{ sourcedir("src/backend/InvenTree/plugin/samples
|
|||
|
||||
A *PluginConfig* database entry will be created for each plugin "discovered" when the server launches. This configuration entry is used to determine if a particular plugin is enabled.
|
||||
|
||||
The configuration entries must be enabled via the [Admin Center](../settings/admin.md#admin-center).
|
||||
The configuration entries must be enabled via the [InvenTree admin interface](../settings/admin.md).
|
||||
|
||||
!!! warning "Disabled by Default"
|
||||
Newly discovered plugins are disabled by default, and must be manually enabled (in the Admin Center) by a user with staff privileges.
|
||||
Newly discovered plugins are disabled by default, and must be manually enabled (in the admin interface) by a user with staff privileges.
|
||||
|
||||
## Plugin Mixins
|
||||
|
||||
|
|
|
|||
|
|
@ -4,57 +4,15 @@ title: Report Mixin
|
|||
|
||||
## ReportMixin
|
||||
|
||||
The `ReportMixin` class provides a plugin with the ability to extend the functionality of custom [report templates](../../report/report.md). A plugin which implements the ReportMixin mixin class can add custom context data to a report template for rendering, and can also receive a callback when a report is generated.
|
||||
The `ReportMixin` class provides a plugin with the ability to extend the functionality of custom [report templates](../../report/report.md). A plugin which implements the ReportMixin mixin class can add custom context data to a report template for rendering.
|
||||
|
||||
### Add Report Context
|
||||
|
||||
A plugin which implements the ReportMixin mixin can define the `add_report_context` method, allowing custom context data to be added to a report template at time of printing.
|
||||
|
||||
This method is called each time a report is generated, and is passed the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `report_instance` | The report template instance which is being rendered |
|
||||
| `model_instance` | The model instance against which the report is being generated |
|
||||
| `user` | The user who initiated the report generation |
|
||||
| `context` | The context dictionary, which can be modified in-place |
|
||||
|
||||
Any data added to the provided `context` dictionary is made available to the report template, and can be rendered using standard django template syntax:
|
||||
|
||||
```python
|
||||
def add_report_context(self, report_instance, model_instance, user, context):
|
||||
"""Add extra context data to the report template."""
|
||||
context['my_custom_data'] = self.calculate_custom_data(model_instance)
|
||||
```
|
||||
|
||||
### Add Label Context
|
||||
|
||||
Similarly, the `add_label_context` method allows custom context data to be added to a label template at time of printing:
|
||||
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `label_instance` | The label template instance which is being rendered |
|
||||
| `model_instance` | The model instance against which the label is being generated |
|
||||
| `user` | The user who initiated the label generation |
|
||||
| `context` | The context dictionary, which can be modified in-place |
|
||||
|
||||
### Report Callback
|
||||
|
||||
The `report_callback` method is called after a report has been generated, and allows the plugin to perform custom actions with the generated report - for example, forwarding the report to an external system, or performing custom post-processing.
|
||||
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| `template` | The report template instance which was used to generate the report |
|
||||
| `instance` | The model instance against which the report was generated |
|
||||
| `report` | The generated report (PDF file data) |
|
||||
| `user` | The user who initiated the report generation |
|
||||
|
||||
```python
|
||||
def report_callback(self, template, instance, report, user, **kwargs):
|
||||
"""Custom callback function - called after a report is generated."""
|
||||
# For example, forward the generated report to an external service
|
||||
self.upload_to_external_service(report)
|
||||
```
|
||||
Additionally the `add_label_context` method, allowing custom context data to be added to a label template at time of printing.
|
||||
|
||||
### Sample Plugin
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ npm install
|
|||
npm run build
|
||||
```
|
||||
|
||||
Copy the built `attachment_carousel` directory to the `inventree-data/plugins` directory and enable it via the [Admin Center](../settings/admin.md#admin-center).
|
||||
Copy the built `attachment_carousel` directory to the `inventree-data/plugins` directory and enable it via the admin interface.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ Individuals and companies can also support via [GitHub sponsors](https://github.
|
|||
- [Codecov](https://codecov.io) - Code coverage as a service, used to track the code coverage of the various components
|
||||
- [Netlify](https://www.netlify.com/) - Static site hosting provider, used to test deploy the frontend and website
|
||||
- [Depot](https://depot.dev/?utm_source=inventree) - Docker build accelerator, used to build the multi-arch images for the InvenTree docker image
|
||||
- [CodSpeed](https://codspeed.io/) - Performance testing platform, used to track the performance of a few performance benchmarks
|
||||
- [Flakiness](https://flakiness.io/) - Frontend test flakiness detection, used to track the flakiness of the various test harnesses
|
||||
|
||||
## Past Supporters
|
||||
|
||||
|
|
|
|||
|
|
@ -135,22 +135,6 @@ The unit cost of the purchase order line item is transferred across to the creat
|
|||
|
||||
However, if the [Convert Currency](#purchase-order-settings) setting is enabled, the currency of the stock item will be converted to the [default currency](../concepts/pricing.md#default-currency) of the system. This may be useful when ordering stock in a different currency, to ensure that the unit cost of the stock item is converted to the base currency at the time of receipt.
|
||||
|
||||
## Bundled Items
|
||||
|
||||
Some suppliers only sell a group of components as a single bundled or "kit" product, rather than as individual purchasable line items - for example, a fastener kit containing an assortment of different screws, or a "starter kit" containing several components required for a particular use case.
|
||||
|
||||
Rather than receiving the bundle as a single opaque stock quantity, InvenTree allows the bundle to be modelled as an assembly, so that it can be broken apart into its individual components once required:
|
||||
|
||||
1. Create a part to represent the bundle itself, and mark it as an [assembly](../part/index.md#assembly)
|
||||
2. Link a [supplier part](./supplier.md#supplier-parts) to the bundle part, representing how it is purchased from the supplier
|
||||
3. Define a [Bill of Materials](../manufacturing/bom.md) for the bundle part, listing each of the individual components and the quantity contained within a single bundle
|
||||
4. Create and receive a purchase order against the bundle's supplier part, as normal - a single stock item is created for the bundle, retaining the purchase price and source purchase order of the order as a whole
|
||||
|
||||
Once the individual components are actually required, the received bundle stock item can be [disassembled](../stock/disassemble.md) into its component parts. The purchase price and traceability data (batch code, source purchase order) of the original bundle are automatically apportioned across the newly generated component stock items.
|
||||
|
||||
!!! tip "Pack Size vs Bundled Items"
|
||||
A supplier part with a [pack size](./supplier.md#supplier-part-pack-size) greater than one still represents multiple units of the *same* part - the pack size simply determines how many physical units are added to stock per unit ordered. A *bundled* item is different: a single supplier part represents an assortment of *different* components, which must be disassembled before the individual components can be used or sold separately.
|
||||
|
||||
## Complete Order
|
||||
|
||||
Once the quantity of all __received__ items is equal or above the quantity of all line items, the order will be automatically marked as __complete__.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,3 @@ Supplier parts can have a pack size defined. This value is defined when creating
|
|||
When buying parts, they are bought in packs. This is taken into account in Purchase Orders: if a supplier part with a pack size of 5 is bought in a quantity of 4, 20 parts will be added to stock when the parts are received.
|
||||
|
||||
When adding stock manually, the supplier part can be added in packs or in individual parts. This is to allow the addition of items in opened packages. Set the flag "Use pack size" (`use_pack_size` in the API) to True in order to add parts in packs.
|
||||
|
||||
!!! tip "Bundled Items"
|
||||
A pack size only ever represents multiple units of the *same* part. If a supplier instead sells a kit or assortment of *different* components as a single purchasable item, refer to the [bundled items](./purchase_order.md#bundled-items) documentation instead.
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
title: Report Assets
|
||||
---
|
||||
|
||||
## Report Assets
|
||||
|
||||
Users can upload asset files (e.g. images) which can be used when generating reports. For example, you may wish to generate a report with your company logo in the header.
|
||||
|
||||
Asset files are managed from the [Admin Center](../settings/admin.md#admin-center), via the *Report Assets* panel. Staff users can upload new asset files, and remove assets which are no longer required.
|
||||
|
||||
Asset files can be rendered directly into the template as follows
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
<!-- Need to include the report template tags at the start of the template file -->
|
||||
{% load report %}
|
||||
|
||||
<!-- Simple stylesheet -->
|
||||
<head>
|
||||
<style>
|
||||
.company-logo {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Report template code here -->
|
||||
|
||||
<!-- Render an uploaded asset image -->
|
||||
<img src="{% asset 'company_image.png' %}" class="company-logo">
|
||||
|
||||
<!-- ... -->
|
||||
</body>
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
!!! warning "Asset Naming"
|
||||
If the requested asset name does not match the name of an uploaded asset, the template will continue without loading the image.
|
||||
|
||||
!!! info "Assets location"
|
||||
Upload new assets via the *Report Assets* panel in the [Admin Center](../settings/admin.md#admin-center) to ensure they are uploaded to the correct location on the server.
|
||||
|
||||
There are various [helper functions](./helpers.md#report-assets) available to assist with embedding assets into templates.
|
||||
|
|
@ -69,14 +69,12 @@ Templates (whether for generating [reports](./report.md) or [labels](./labels.md
|
|||
|
||||
| Model Type | Description |
|
||||
| --- | --- |
|
||||
| [company](#company) | A Company instance |
|
||||
| company | A Company instance |
|
||||
| [build](#build-order) | A [Build Order](../manufacturing/build.md) instance |
|
||||
| [buildline](#build-line) | A [Build Order Line Item](../manufacturing/build.md) instance |
|
||||
| [salesorder](#sales-order) | A [Sales Order](../sales/sales_order.md) instance |
|
||||
| [salesordershipment](#sales-order-shipment) | A [Sales Order Shipment](../sales/sales_order.md#sales-order-shipments) instance |
|
||||
| [returnorder](#return-order) | A [Return Order](../sales/return_order.md) instance |
|
||||
| [purchaseorder](#purchase-order) | A [Purchase Order](../purchasing/purchase_order.md) instance |
|
||||
| [transferorder](#transfer-order) | A [Transfer Order](../stock/transfer_order.md) instance |
|
||||
| [stockitem](#stock-item) | A [StockItem](../stock/index.md#stock-item) instance |
|
||||
| [stocklocation](#stock-location) | A [StockLocation](../stock/index.md#stock-location) instance |
|
||||
| [part](#part) | A [Part](../part/index.md) instance |
|
||||
|
|
@ -143,16 +141,6 @@ When printing a report or label against a [PurchaseOrder](../purchasing/purchase
|
|||
|
||||
{{ report_context("models", "purchaseorder") }}
|
||||
|
||||
### Transfer Order
|
||||
|
||||
When printing a report or label against a [TransferOrder](../stock/transfer_order.md) object, the following context variables are available:
|
||||
|
||||
{{ report_context("models", "transferorder") }}
|
||||
|
||||
::: order.models.TransferOrder.report_context
|
||||
options:
|
||||
show_source: True
|
||||
|
||||
### Stock Item
|
||||
|
||||
When printing a report or label against a [StockItem](../stock/index.md#stock-item) object, the following context variables are available:
|
||||
|
|
@ -185,7 +173,7 @@ When printing a report or label against a [Part](../part/index.md) object, the f
|
|||
|
||||
## Model Variables
|
||||
|
||||
Additional to the context variables provided directly to each template, each model type has a number of attributes and methods which can be accessed via the template.
|
||||
Additional to the context variables provided directly to each template, each model type has a number of attributes and methods which can be accessedd via the template.
|
||||
|
||||
For each model type, a subset of the most commonly used attributes are listed below. For a full list of attributes and methods, refer to the source code for the particular model type.
|
||||
|
||||
|
|
@ -199,6 +187,7 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
|----------|-------------|
|
||||
| name | Brief name for this part |
|
||||
| full_name | Full name for this part (including IPN, if not null and including variant, if not null) |
|
||||
| variant | Optional variant number for this part - Must be unique for the part name
|
||||
| category | The [PartCategory](#part-category) object to which this part belongs
|
||||
| description | Longer form description of the part
|
||||
| keywords | Optional keywords for improving part search results
|
||||
|
|
@ -255,6 +244,7 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| parent | Link to another [StockItem](#stock-item) from which this StockItem was created |
|
||||
| uid | Field containing a unique-id which is mapped to a third-party identifier (e.g. a barcode) |
|
||||
| part | Link to the master abstract [Part](#part) that this [StockItem](#stock-item) is an instance of |
|
||||
| supplier_part | Link to a specific [SupplierPart](#supplierpart) (optional) |
|
||||
| location | The [StockLocation](#stock-location) Where this [StockItem](#stock-item) is located |
|
||||
|
|
@ -273,6 +263,7 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
| build | Link to a Build (if this stock item was created from a build) |
|
||||
| is_building | Boolean field indicating if this stock item is currently being built (or is "in production") |
|
||||
| purchase_order | Link to a [PurchaseOrder](#purchase-order) (if this stock item was created from a PurchaseOrder) |
|
||||
| infinite | If True this [StockItem](#stock-item) can never be exhausted |
|
||||
| sales_order | Link to a [SalesOrder](#sales-order) object (if the StockItem has been assigned to a SalesOrder) |
|
||||
| purchase_price | The unit purchase price for this [StockItem](#stock-item) - this is the unit price at time of purchase (if this item was purchased from an external supplier) |
|
||||
| packaging | Description of how the StockItem is packaged (e.g. "reel", "loose", "tape" etc) |
|
||||
|
|
@ -286,7 +277,7 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
| icon | The name of the icon if set, e.g. fas fa-warehouse |
|
||||
| item_count | Simply returns the number of stock items in this location |
|
||||
| name | The name of the location. This is only the name of this location, not the path |
|
||||
| owner | The owner of the location if it has one |
|
||||
| owner | The owner of the location if it has one. The owner can only be assigned in the admin interface |
|
||||
| parent | The parent location. Returns None if it is already the top most one |
|
||||
| path | A queryset of locations that contains the hierarchy starting from the top most parent |
|
||||
| pathstring | A string that contains all names of the path separated by slashes e.g. A/B/C |
|
||||
|
|
@ -307,8 +298,8 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
| contact | Contact Name |
|
||||
| phone | Contact phone number |
|
||||
| email | Contact email address |
|
||||
| link | URL associated with the company |
|
||||
| notes | Extra notes about the company |
|
||||
| link | A second URL to the company (Actually only accessible in the admin interface) |
|
||||
| notes | Extra notes about the company (Actually only accessible in the admin interface) |
|
||||
| is_customer | Boolean value, is this company a customer |
|
||||
| is_supplier | Boolean value, is this company a supplier |
|
||||
| is_manufacturer | Boolean value, is this company a manufacturer |
|
||||
|
|
@ -362,7 +353,7 @@ Each part object has access to a lot of context variables about the part. The fo
|
|||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| username | the username of the user |
|
||||
| first_name | The first name of the user |
|
||||
| fist_name | The first name of the user |
|
||||
| last_name | The last name of the user |
|
||||
| email | The email address of the user |
|
||||
| pk | The primary key of the user |
|
||||
|
|
|
|||
|
|
@ -907,7 +907,7 @@ If you have a custom logo, but explicitly wish to load the InvenTree logo itself
|
|||
|
||||
## Report Assets
|
||||
|
||||
[Report Assets](./assets.md) are files specifically uploaded by the user for inclusion in generated reports and labels.
|
||||
[Report Assets](./index.md#report-assets) are files specifically uploaded by the user for inclusion in generated reports and labels.
|
||||
|
||||
You can add asset images to the reports and labels by using the `{% raw %}{% asset ... %}{% endraw %}` template tag:
|
||||
|
||||
|
|
|
|||
|
|
@ -53,43 +53,29 @@ To read more about the capabilities of the report templating engine, and how to
|
|||
|
||||
## Creating Templates
|
||||
|
||||
Report and label templates are managed from the [Admin Center](../settings/admin.md#admin-center), which provides dedicated panels (under the *Reporting* group) for each template type:
|
||||
Report and label templates can be created (and edited) via the [admin interface](../settings/admin.md), under the *Report* section.
|
||||
|
||||
- **Label Templates** - Create and edit [label templates](./labels.md)
|
||||
- **Report Templates** - Create and edit [report templates](./report.md)
|
||||
- **Report Snippets** - Manage reusable [snippet](#report-snippets) files
|
||||
- **Report Assets** - Manage uploaded [asset](#report-assets) files
|
||||
Select the type of template you are wanting to create (a *Report Template* or *Label Template*) and press the *Add* button in the top right corner:
|
||||
|
||||
Label and report templates are created and edited using the built-in [template editor](./template_editor.md), which allows templates to be written directly within the browser, with a live preview of the rendered output.
|
||||
{{ image("report/report_template_admin.png", "Report template admin") }}
|
||||
|
||||
!!! tip "Staff Access Only"
|
||||
Only users with staff access can create, upload or edit templates, snippets and assets.
|
||||
Only users with staff access can upload or edit report template files.
|
||||
|
||||
!!! info "Database Admin Interface"
|
||||
Templates can also be managed at a lower level via the [Database Admin interface](../settings/db_admin.md), under the *Report* section. This is recommended for advanced users only.
|
||||
!!! info "Editing Reports"
|
||||
Existing reports can be edited from the admin interface, in the same location as described above. To change the contents of the template, re-upload a template file, to override the existing template data.
|
||||
|
||||
!!! tip "Template Editor"
|
||||
InvenTree also provides a powerful [template editor](./template_editor.md) which allows for the creation and editing of report templates directly within the browser.
|
||||
|
||||
### Name and Description
|
||||
|
||||
Each report template requires a name and description, which identify and describe the report template.
|
||||
|
||||
### Revision
|
||||
|
||||
Each template has a revision number, which is automatically incremented each time the template is updated. This provides a simple mechanism for tracking changes to a template over time. The revision number is read-only, and cannot be edited directly.
|
||||
|
||||
!!! info "Template Revision Context"
|
||||
The revision number of the template is made available when rendering, via the `template_revision` [context variable](./context_variables.md#global-context).
|
||||
|
||||
### Enabled Status
|
||||
|
||||
Boolean field which determines if the specific report template is enabled, and available for use. Reports can be disabled to remove them from the list of available templates, but without deleting them from the database.
|
||||
|
||||
### Attach to Model
|
||||
|
||||
If the *Attach to Model on Print* option is enabled, a copy of the generated report is automatically saved as a file attachment against the item (model instance) for which it was generated, each time the template is printed.
|
||||
|
||||
!!! warning "Attachment Support"
|
||||
The report output is only attached if the target model type supports file attachments.
|
||||
|
||||
### Filename Pattern
|
||||
|
||||
The filename pattern used to generate the output `.pdf` file. Defaults to "report.pdf".
|
||||
|
|
@ -161,15 +147,89 @@ Setting the *Debug Mode* option renders the template as raw HTML instead of PDF,
|
|||
|
||||
## Report Assets
|
||||
|
||||
User can upload asset files (e.g. images) which can be used when generating reports. For example, you may wish to generate a report with your company logo in the header.
|
||||
User can upload asset files (e.g. images) which can be used when generating reports. For example, you may wish to generate a report with your company logo in the header. Asset files are uploaded via the admin interface.
|
||||
|
||||
Asset files can be rendered directly into the template as follows
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
<!-- Need to include the report template tags at the start of the template file -->
|
||||
{% load report %}
|
||||
|
||||
<!-- Simple stylesheet -->
|
||||
<head>
|
||||
<style>
|
||||
.company-logo {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Report template code here -->
|
||||
|
||||
<!-- Render an uploaded asset image -->
|
||||
<img src="{% asset 'company_image.png' %}" class="company-logo">
|
||||
|
||||
<!-- ... -->
|
||||
</body>
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
!!! warning "Asset Naming"
|
||||
If the requested asset name does not match the name of an uploaded asset, the template will continue without loading the image.
|
||||
|
||||
!!! info "Assets location"
|
||||
Upload new assets via the [admin interface](../settings/admin.md) to ensure they are uploaded to the correct location on the server.
|
||||
|
||||
Refer to the [report assets](./assets.md) documentation for further information.
|
||||
|
||||
## Report Snippets
|
||||
|
||||
InvenTree provides report "snippets" - reusable template files which cannot be rendered by themselves, but can be included in other templates.
|
||||
A powerful feature provided by the django / WeasyPrint templating framework is the ability to include external template files. This allows commonly used template features to be broken out into separate files and reused across multiple templates.
|
||||
|
||||
Refer to the [report snippets](./snippets.md) documentation for further information.
|
||||
To support this, InvenTree provides report "snippets" - short (or not so short) template files which cannot be rendered by themselves, but can be called from other templates.
|
||||
|
||||
Similar to assets files, snippet template files are uploaded via the admin interface.
|
||||
|
||||
Snippets are included in a template as follows:
|
||||
|
||||
```
|
||||
{% raw %}{% include 'snippets/<snippet_name.html>' %}{% endraw %}
|
||||
```
|
||||
|
||||
For example, consider a custom stocktake report for a particular stock location, where we wish to render a table with a row for each item in that location.
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
|
||||
<table class='stock-table'>
|
||||
<thead>
|
||||
<!-- table header data -->
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in location.stock_items %}
|
||||
{% include 'snippets/stock_row.html' with item=item %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
!!! info "Snippet Arguments"
|
||||
Note above that named argument variables can be passed through to the snippet!
|
||||
|
||||
And the snippet file `stock_row.html` may be written as follows:
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
<!-- stock_row snippet -->
|
||||
<tr>
|
||||
<td>{{ item.part.full_name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
</tr>
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
|
|
@ -183,7 +243,7 @@ When WeasyPrint renders a template to PDF it can make outbound requests to load
|
|||
|---|---|
|
||||
| `data:` URIs | Always permitted — self-contained, no network access |
|
||||
| `file://` | Always blocked — assets and images must be inlined as `data:` URIs before reaching WeasyPrint |
|
||||
| `http` / `https` | Disabled by default, but can be enabled - see *Remote URL Fetching* below |
|
||||
| `http` / `https` | Disabled by default, but can be blocked - see *Remote URL Fetching* below |
|
||||
| Any other scheme | Always blocked |
|
||||
|
||||
HTTP redirects are also disabled: a URL that passes validation cannot redirect to an internal address.
|
||||
|
|
@ -199,6 +259,6 @@ When enabled, URLs are still validated against private, loopback, link-local, an
|
|||
|
||||
### Asset Files
|
||||
|
||||
[Asset files](./assets.md) uploaded through the admin interface are embedded directly into the rendered PDF as base64 `data:` URIs — they are read via the Django storage API and never loaded through WeasyPrint's URL fetcher. This means assets work correctly regardless of whether remote URL fetching is enabled, and also work with remote storage backends such as S3.
|
||||
Asset files uploaded through the admin interface are embedded directly into the rendered PDF as base64 `data:` URIs — they are read via the Django storage API and never loaded through WeasyPrint's URL fetcher. This means assets work correctly regardless of whether remote URL fetching is enabled, and also work with remote storage backends such as S3.
|
||||
|
||||
There are various [helper functions](./helpers.md#report-assets) available to assist with embedding assets into templates.
|
||||
|
|
|
|||
|
|
@ -128,25 +128,6 @@ As an example, consider a label template for a StockItem. A user may wish to def
|
|||
|
||||
To restrict the label accordingly, we could set the *filters* value to `part__IPN=IPN123`.
|
||||
|
||||
## Printing Labels
|
||||
|
||||
Labels are printed directly from the web interface, from the pages where the target items are displayed. To print labels against one or more items:
|
||||
|
||||
1. Select the items to print - either from a table (using the row checkboxes), or by viewing the detail page of a single item
|
||||
2. Select the *Print* action, and choose the *Print Label* option
|
||||
3. Select the desired label template - only *enabled* templates which match the selected model type (and pass any template filters) are available for selection
|
||||
4. Select the *printer* (plugin) to use for printing the labels
|
||||
|
||||
### Label Printing Plugins
|
||||
|
||||
The actual printing of labels is handled by a [label printing plugin](../plugins/mixins/label.md). InvenTree provides a number of built-in printing plugins:
|
||||
|
||||
- The default [InvenTree Label Printer](../plugins/builtin/inventree_label.md) plugin generates a PDF file, which is then made available for download.
|
||||
- The [Label Sheet](../plugins/builtin/inventree_label_sheet.md) plugin arranges multiple labels onto a single sheet for printing.
|
||||
- The [Label Machine](../plugins/builtin/inventree_label_machine.md) plugin sends the label to an external [label printer machine](../plugins/machines/label_printer.md).
|
||||
|
||||
Custom label printing plugins (e.g. for driving a specific hardware printer) can be installed to extend this list - refer to the [label mixin documentation](../plugins/mixins/label.md) for further information.
|
||||
|
||||
## Built-In Templates
|
||||
|
||||
The InvenTree installation provides a number of simple *default* templates which can be used as a starting point for creating custom labels. These built-in templates can be disabled if they are not required.
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
---
|
||||
title: Report Templates
|
||||
title: Report and Label Generation
|
||||
---
|
||||
|
||||
## Report Templates
|
||||
## Custom Reports
|
||||
|
||||
Report templates are used to generate formal PDF documents - such as order reports, packing lists, or test reports - rendered against a particular database item (or list of items).
|
||||
InvenTree supports a customizable reporting ecosystem, allowing the user to develop document templates that meet their particular needs.
|
||||
|
||||
Like all InvenTree templates, report templates are written using a mixture of HTML / CSS and the django template language, and are rendered to a PDF file using the WeasyPrint engine:
|
||||
PDF files are generated from custom HTML template files which are written by the user.
|
||||
|
||||
- Refer to the [template overview](./index.md) for information on creating and managing templates.
|
||||
- Refer to the [template rendering documentation](./weasyprint.md) for information on the WeasyPrint engine and the django template language.
|
||||
- Refer to the [context variables documentation](./context_variables.md) for the variables available when rendering a template.
|
||||
Templates can be used to generate *reports* or *labels* which can be used in a variety of situations to format data in a friendly format for printing, distribution, conformance and testing.
|
||||
|
||||
## Report Options
|
||||
In addition to providing the ability for end-users to provide their own reporting templates, some report types offer "built-in" report templates ready for use.
|
||||
|
||||
In addition to the [options common to all templates](./index.md#creating-templates), each report template provides the following options:
|
||||
### WeasyPrint Templates
|
||||
|
||||
### Page Size
|
||||
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
|
||||
|
||||
The page size (e.g. `A4` or `Letter`) used when rendering the report to a PDF file. When a new report template is created, this value defaults to the [default page size](./index.md#default-page-size) specified in the global settings.
|
||||
!!! info "WeasyPrint"
|
||||
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/) for further information.
|
||||
|
||||
### Landscape
|
||||
### Stylesheets
|
||||
|
||||
If enabled, the report is rendered in landscape orientation, rather than the default portrait orientation.
|
||||
Templates are rendered using standard HTML / CSS - if you are familiar with web page layout, you're ready to go!
|
||||
|
||||
### Merge
|
||||
### Template Language
|
||||
|
||||
If enabled, a single report document is generated for all selected items, rather than a separate document for each item. Refer to the [merging reports](#merging-reports) section below for further information.
|
||||
Uploaded report template files are passed through the [django template rendering framework]({% include "django.html" %}/topics/templates/), and as such accept the same variable template strings as any other django template file. Different variables are passed to the report template (based on the context of the report) and can be used to customize the contents of the generated PDF.
|
||||
|
||||
!!! info "Context Variables"
|
||||
The `page_size`, `landscape` and `merge` values are also made available to the template as [context variables](./context_variables.md#report-context).
|
||||
### Variables
|
||||
|
||||
## Generating Reports
|
||||
Each report template is provided a set of *context variables* which can be used when rendering the template.
|
||||
|
||||
Reports are generated directly from the web interface, from the pages where the target items are displayed. To generate a report against one or more items:
|
||||
For example, rendering the name of a part (which is available in the particular template context as `part`) is as follows:
|
||||
|
||||
1. Select the items to print - either from a table (using the row checkboxes), or by viewing the detail page of a single item
|
||||
2. Select the *Print* action, and choose the *Print Report* option
|
||||
3. Select the desired report template - only *enabled* templates which match the selected model type (and pass any [template filters](./index.md#template-filters)) are available for selection
|
||||
4. The report is generated by the server, and the resulting PDF file is made available for download
|
||||
```html
|
||||
{% raw %}
|
||||
|
||||
!!! info "Enable Reports"
|
||||
Report generation must be [enabled in the global settings](./index.md#enable-reports) before reports can be generated.
|
||||
<!-- Template variables use {{ double_curly_braces }} -->
|
||||
<h2>Part: {{ part.name }}</h3>
|
||||
<p><i>
|
||||
Description:<br>
|
||||
{{ part.description }}
|
||||
</p></i>
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
## Merging Reports
|
||||
|
||||
When rendering reports for multiple items, the default behaviour is that each item is rendered as a separate report. The chosen template is rendered multiple times, once for each item selected, and expects a single item in the context variable.
|
||||
When rendering reports for multiple items, the default behaviour is that each item is rendered as a separate report. The chosen templeate is rendered multiple times, once for each item selected, and expects a single item in the context variable.
|
||||
|
||||
However, it is possible to merge multiple items into a single report document. This is achieved by enabling the `merge` attribute of the report template:
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ The following report templates are provided "out of the box" and can be used as
|
|||
| [Sales Order Shipment](#sales-order-shipment) | [SalesOrderShipment](../sales/sales_order.md) | Sales Order Shipment report |
|
||||
| [Stock Location](#stock-location) | [StockLocation](../stock/index.md#stock-location) | Stock Location report |
|
||||
| [Test Report](#test-report) | [StockItem](../stock/index.md#stock-item) | Test Report |
|
||||
| [Transfer Order](#transfer-order) | [TransferOrder](../stock/transfer_order.md) | Transfer Order report |
|
||||
| [Selected Stock Items Report](#selected-stock-items-report) | [StockItem](../stock/index.md#stock-item) | Selected Stock Items report |
|
||||
|
||||
|
||||
|
|
@ -58,10 +57,6 @@ The following report templates are provided "out of the box" and can be used as
|
|||
|
||||
{{ templatefile("report/inventree_test_report.html") }}
|
||||
|
||||
### Transfer Order
|
||||
|
||||
{{ templatefile("report/inventree_transfer_order_report.html") }}
|
||||
|
||||
### Selected Stock Items Report
|
||||
|
||||
{{ templatefile("report/inventree_stock_report_merge.html") }}
|
||||
|
|
@ -73,11 +68,9 @@ The following label templates are provided "out of the box" and can be used as a
|
|||
| Template | Model Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [Build Line](#build-line-label) | [Build line item](../manufacturing/build.md) | Build Line label |
|
||||
| [Part](#part-label) | [Part](../part/index.md) | Part label (QR code and part name) |
|
||||
| [Part (Code128)](#part-label-code128) | [Part](../part/index.md) | Part label (Code128 barcode) |
|
||||
| [Part](#part-label) | [Part](../part/index.md) | Part label |
|
||||
| [Stock Item](#stock-item-label) | [StockItem](../stock/index.md#stock-item) | Stock Item label |
|
||||
| [Stock Location](#stock-location-label) | [StockLocation](../stock/index.md#stock-location) | Stock Location label (QR code) |
|
||||
| [Stock Location (with text)](#stock-location-label-with-text) | [StockLocation](../stock/index.md#stock-location) | Stock Location label (QR code and location details) |
|
||||
| [Stock Location](#stock-location-label) | [StockLocation](../stock/index.md#stock-location) | Stock Location label |
|
||||
|
||||
### Build Line Label
|
||||
|
||||
|
|
@ -85,10 +78,6 @@ The following label templates are provided "out of the box" and can be used as a
|
|||
|
||||
### Part Label
|
||||
|
||||
{{ templatefile("label/part_label.html") }}
|
||||
|
||||
### Part Label (Code128)
|
||||
|
||||
{{ templatefile("label/part_label_code128.html") }}
|
||||
|
||||
### Stock Item Label
|
||||
|
|
@ -97,8 +86,4 @@ The following label templates are provided "out of the box" and can be used as a
|
|||
|
||||
### Stock Location Label
|
||||
|
||||
{{ templatefile("label/stocklocation_qr.html") }}
|
||||
|
||||
### Stock Location Label (with text)
|
||||
|
||||
{{ templatefile("label/stocklocation_qr_and_text.html") }}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
---
|
||||
title: Report Snippets
|
||||
---
|
||||
|
||||
## Report Snippets
|
||||
|
||||
A powerful feature provided by the django / WeasyPrint templating framework is the ability to include external template files. This allows commonly used template features to be broken out into separate files and reused across multiple templates.
|
||||
|
||||
To support this, InvenTree provides report "snippets" - short (or not so short) template files which cannot be rendered by themselves, but can be called from other templates.
|
||||
|
||||
Snippet files are managed from the [Admin Center](../settings/admin.md#admin-center), via the *Report Snippets* panel. Staff users can upload new snippet files, and edit or remove existing snippets.
|
||||
|
||||
Additionally, the content of an existing snippet can be modified directly within the browser - simply select a snippet from the table to open it in the built-in [template editor](./template_editor.md#editing-snippets).
|
||||
|
||||
Snippets are included in a template as follows:
|
||||
|
||||
```
|
||||
{% raw %}{% include 'snippets/<snippet_name.html>' %}{% endraw %}
|
||||
```
|
||||
|
||||
For example, consider a custom stocktake report for a particular stock location, where we wish to render a table with a row for each item in that location.
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
|
||||
<table class='stock-table'>
|
||||
<thead>
|
||||
<!-- table header data -->
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in location.stock_items %}
|
||||
{% include 'snippets/stock_row.html' with item=item %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
!!! info "Snippet Arguments"
|
||||
Note above that named argument variables can be passed through to the snippet!
|
||||
|
||||
And the snippet file `stock_row.html` may be written as follows:
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
<!-- stock_row snippet -->
|
||||
<tr>
|
||||
<td>{{ item.part.full_name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
</tr>
|
||||
{% endraw %}
|
||||
```
|
||||
|
|
@ -4,11 +4,11 @@ title: Template editor
|
|||
|
||||
## Template editor
|
||||
|
||||
The Template Editor is integrated into the [Admin Center](../settings/admin.md#admin-center) of the Web UI. It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
|
||||
The Template Editor is integrated into the [Admin Center](../settings//admin.md#admin-center) of the Web UI. It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
|
||||
|
||||

|
||||
|
||||
On the left side (1) are all possible template types for labels and reports listed. With the "+" button (2), above the template table (3), new templates for the selected type can be created. To switch to the template editor click on a template.
|
||||
On the left side (1) are all possible possible template types for labels and reports listed. With the "+" button (2), above the template table (3), new templates for the selected type can be created. To switch to the template editor click on a template.
|
||||
|
||||
### Editing Templates
|
||||
|
||||
|
|
@ -31,10 +31,3 @@ If you don't want to override the template, but just render a preview for a temp
|
|||
#### Edit template metadata
|
||||
|
||||
Editing metadata such as name, description, filters and even width/height for labels and orientation/page size for reports can be done from the edit modal accessible when clicking on the three dots (4) and select "Edit" in the dropdown menu.
|
||||
|
||||
### Editing Snippets
|
||||
|
||||
[Report snippets](./index.md#report-snippets) can also be edited directly within the browser, from the *Report Snippets* panel in the Admin Center. Selecting a snippet from the table opens it in the same code editor as used for report and label templates.
|
||||
|
||||
!!! info "No Preview"
|
||||
As snippets cannot be rendered by themselves (they must be included in a report or label template), no preview area is available when editing a snippet.
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ To make MFA mandatory for all users:
|
|||
|
||||
### Security Consideration
|
||||
|
||||
A user can lock themselves out if they lose access to both the device with their TOTP app and their backup tokens. An admin can delete their tokens from the Database Admin interface (they exist under the 'TOTP devices' / 'static devices' models). This should be a last resort and only done by people knowledgeable about the [Database Admin interface](../settings/db_admin.md), as changes there might circumvent InvenTree's business and security logic.
|
||||
A user can lock themselves out if they lose access to both the device with their TOTP app and their backup tokens. An admin can delete their tokens from the admin pages (they exist under the 'TOTP devices' / 'static devices' models) . This should be a last resort and only done by people knowledgeable about the [admin pages](../settings/admin.md) as changes there might circumvent InvenTree's business and security logic.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ The basic requirements for configuring SSO are outlined below:
|
|||
|
||||
1. Enable backend for each required SSO provider(s) in the [config file or environment variables](../start/config.md#single-sign-on).
|
||||
1. Create an external *app* with your provider of choice
|
||||
1. Add the required client configurations in the `SocialApp` app in the [Database Admin interface](../settings/db_admin.md).
|
||||
1. Add the required client configurations in the `SocialApp` app in the [admin interface](../settings/admin.md).
|
||||
1. Configure the *callback* URL for the external app.
|
||||
1. Enable SSO for the users in the [global settings](../settings/global.md).
|
||||
1. Configure [e-mail](../settings/email.md).
|
||||
|
|
@ -161,4 +161,4 @@ Make sure all users with admin privileges have sufficient passwords - they can r
|
|||
|
||||
## Error Handling
|
||||
|
||||
If you encounter an error during the SSO process, the error should be logged in the InvenTree database. You can view the [error log](./logs.md) in the [Admin Center](./admin.md#admin-center) to see the details of the error.
|
||||
If you encounter an error during the SSO process, the error should be logged in the InvenTree database. You can view the [error log](./logs.md) in the [admin interface](./admin.md) to see the details of the error.
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@ title: InvenTree Admin Interfaces
|
|||
|
||||
## InvenTree Admin Interfaces
|
||||
|
||||
InvenTree provides multiple administration interfaces with different safety levels and intended use cases.
|
||||
There are multiple administration interfaces available in InvenTree, which provide different levels of access to the underlying resources and different operational safety.
|
||||
|
||||
[**Admin Center**](#admin-center):
|
||||
|
||||
- Main administration interface for day-to-day operations
|
||||
- Uses API-backed flows with validation and safety checks
|
||||
- Main interface for managing InvenTree
|
||||
- Robust verification and safety checks
|
||||
|
||||
[**System Settings**](#system-settings):
|
||||
|
||||
- Access to global runtime settings
|
||||
- Available to staff users (or users with equivalent API scope)
|
||||
- Access to all settings
|
||||
- Robust verification, requires reading the documentation
|
||||
|
||||
[**Database Admin Interface**](./db_admin.md):
|
||||
[**Backend Admin Interface**](#backend-admin-interface):
|
||||
|
||||
- Low-level database administration
|
||||
- Fewer safeguards than the Admin Center
|
||||
- Intended for advanced users and troubleshooting scenarios
|
||||
- Low level access to the database
|
||||
- Few verification or safety checks
|
||||
- Requires knowledge of InvenTree internals
|
||||
- Recommended for advanced users only
|
||||
|
||||
### Admin Center
|
||||
|
||||
|
|
@ -33,13 +34,7 @@ The Admin Center is the main interface for managing InvenTree. It provides a use
|
|||
- Integration with external services (via machines and plugins)
|
||||
- Reporting and statistics
|
||||
|
||||
#### Access Admin Center
|
||||
|
||||
The Admin Center can be accessed in any of the following ways:
|
||||
|
||||
- User menu in the top-right corner: *Admin Center*
|
||||
- Command palette quick action: *Admin Center*
|
||||
- Direct URL: `/web/settings/admin`
|
||||
It can be access via the *Admin Center* link in the top right user menu, the *Admin Center* quick-link in the command palette, or via the navigation menu.
|
||||
|
||||
#### Permissions
|
||||
|
||||
|
|
@ -49,6 +44,40 @@ Some panes can only be accessed by users with specific permissions. For example,
|
|||
|
||||
The System Settings interface provides ordered access to all global settings in InvenTree. Users need to have _staff_ privileges enabled or the _a:staff_ scope.
|
||||
|
||||
### Database Admin Interface
|
||||
### Backend Admin Interface
|
||||
|
||||
For low-level administration tasks, use the [Database Admin Interface](./db_admin.md).
|
||||
Users which have *staff* privileges have access to an Admin interface which provides extremely low level control of the database. Every item in the database is available and this interface provides a unrestricted option for directly viewing and modifying database objects.
|
||||
|
||||
!!! warning "Caution"
|
||||
Admin users should exercise extreme care when modifying data via the admin interface, as performing the wrong action may have unintended consequences!
|
||||
|
||||
The admin interface allows *staff* users the ability to directly view / add / edit / delete database entries according to their [user permissions](./permissions.md).
|
||||
|
||||
#### Access Backend Admin Interface
|
||||
|
||||
To directly access the admin interface, append /admin/ to the InvenTree site URL - e.g. http://localhost:8000/admin/.
|
||||
|
||||
An administration panel will be presented as shown below:
|
||||
|
||||
{{ image("admin/admin.png", "Admin panel") }}
|
||||
|
||||
#### View Database Objects
|
||||
|
||||
Database objects can be listed and filtered directly. The image below shows an example of displaying existing part categories.
|
||||
|
||||
{{ image("admin/part_cats.png", "Part categories") }}
|
||||
|
||||
!!! info "Permissions"
|
||||
A "staff" account does not necessarily provide access to all administration options, depending on the roles assigned to the user.
|
||||
|
||||
##### Filtering
|
||||
|
||||
Some admin views support filtering of results against specified criteria. For example, the list of Part objects can be filtered as follows:
|
||||
|
||||
{{ image("admin/filter.png", "Filter part list") }}
|
||||
|
||||
#### Edit Database Objects
|
||||
|
||||
Individual database objects can be edited directly in the admin interface. The image below shows an example of editing a Part object:
|
||||
|
||||
{{ image("admin/edit_part.png", "Edit part") }}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
---
|
||||
title: InvenTree Database Admin Interface
|
||||
---
|
||||
|
||||
## Database Admin Interface
|
||||
|
||||
The Database Admin interface provides low-level access to InvenTree database objects.
|
||||
|
||||
!!! danger "Low-Level Interface"
|
||||
The Database Admin bypasses many of the application-level safety checks used in the Admin Center.
|
||||
Incorrect edits can create inconsistent data, break workflows, or expose security issues.
|
||||
Use this interface only if you understand the data model and operational impact.
|
||||
|
||||
!!! warning "Recommended Usage"
|
||||
Prefer the [Admin Center](./admin.md#admin-center) for routine administration.
|
||||
Use the Database Admin only for advanced administration and troubleshooting.
|
||||
|
||||
### Access Database Admin Interface
|
||||
|
||||
Access to the Database Admin requires a user account with *staff* privileges.
|
||||
|
||||
Use one of the following methods:
|
||||
|
||||
- Append `/admin/` to the base InvenTree URL (for example: `http://localhost:8000/admin/`)
|
||||
- Use the configured administrator URL from `INVENTREE_ADMIN_URL`
|
||||
|
||||
{{ image("admin/admin.png", "Database Admin panel") }}
|
||||
|
||||
### Permissions
|
||||
|
||||
A "staff" account does not necessarily provide access to all administration options, depending on the roles assigned to the user.
|
||||
|
||||
### View Database Objects
|
||||
|
||||
Database objects can be listed and filtered directly. The image below shows an example of displaying existing part categories.
|
||||
|
||||
{{ image("admin/part_cats.png", "Part categories") }}
|
||||
|
||||
#### Filtering
|
||||
|
||||
Some admin views support filtering of results against specified criteria. For example, the list of Part objects can be filtered as follows:
|
||||
|
||||
{{ image("admin/filter.png", "Filter part list") }}
|
||||
|
||||
### Edit Database Objects
|
||||
|
||||
Individual database objects can be edited directly in the Database Admin interface. The image below shows an example of editing a Part object:
|
||||
|
||||
{{ image("admin/edit_part.png", "Edit part") }}
|
||||
|
||||
!!! danger "Before You Save Changes"
|
||||
Verify your changes carefully before saving.
|
||||
If possible, test changes in a non-production environment first.
|
||||
Record what you changed so it can be reviewed and reverted if needed.
|
||||
|
|
@ -226,7 +226,6 @@ Configuration of stock item options
|
|||
{{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS") }}
|
||||
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
|
||||
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
|
||||
{{ globalsetting("STOCK_MERGE_ON_TRANSFER") }}
|
||||
{{ globalsetting("TEST_STATION_DATA") }}
|
||||
|
||||
### Build Orders
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
---
|
||||
title: Error Logs
|
||||
title: Admin Shell
|
||||
---
|
||||
|
||||
## Error Logs
|
||||
|
||||
Any critical server error logs are recorded to the database, and can be viewed by staff users in the [Admin Center](./admin.md#admin-center), under the *Error Reports* section:
|
||||
Any critical server error logs are recorded to the database, and can be viewed by staff users using the admin interface.
|
||||
|
||||
{{ image("admin/admin_errors_link.png", "Error Reports in the Admin Center") }}
|
||||
In the admin interface, select the "Errors" view:
|
||||
|
||||
A list of error logs is presented. Select an entry to view the full error details, including the traceback.
|
||||
{{ image("admin/admin_errors_link.png", "Admin errors") }}
|
||||
|
||||
!!! info "URL"
|
||||
Alternatively, navigate to the error list view at /admin/error_report/error/
|
||||
|
||||
A list of error logs is presented.
|
||||
|
||||
{{ image("admin/admin_errors.png", "Error logs") }}
|
||||
|
||||
!!! info "Database Admin Interface"
|
||||
Error logs can also be viewed via the [Database Admin interface](./db_admin.md), at the URL `/admin/error_report/error/`
|
||||
|
||||
!!! info "Deleting Logs"
|
||||
Error logs should be deleted periodically
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ title: User Permissions
|
|||
InvenTree provides access control to various features and data, by assigning each *user* to one (or more) *groups* which have multiple *roles* assigned.
|
||||
|
||||
!!! info "Superuser"
|
||||
The superuser account is afforded *all* permissions across an InvenTree installation. This includes the [Database Admin interface](./db_admin.md), web interface, and API.
|
||||
The superuser account is afforded *all* permissions across an InvenTree installation. This includes the admin interface, web interface, and API.
|
||||
|
||||
### User
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ Within each role, there are four levels of available permissions:
|
|||
## Dangerous User Flags
|
||||
|
||||
In addition to the above permissions, there are two special flags that can be assigned to a user:
|
||||
- **Staff** - A user with the *staff* flag is able to access the [Database Admin interface](./db_admin.md), and can trigger dangerous actions that might have a security impact such as changing parsable files on the server (templates / reports / plugins). Some of these actions require the *admin* role to be assigned as well.
|
||||
- **Staff** - A user with the *staff* flag is able to access the admin interface, and can trigger dangerous actions that might have a security impact such as changing parsable files on the server (templates / reports / plugins). Some of these actions require the *admin* role to be assigned as well.
|
||||
- **Superuser** - A user with the *superuser* flag is able to access and change all data and functions of InvenTree. A superuser can modify and access all data that the InvenTree installation / server has access to - including shell access on the server OS itself. This is a very powerful flag, and should be used with caution.
|
||||
|
||||
It is strongly recommended to register any users with staff / superuser flags with strong MFA methods to reduce the risk of unauthorized access. These accounts should be used with caution, and should not be used for day-to-day operations.
|
||||
|
|
@ -62,11 +62,11 @@ It is strongly recommended to register any users with staff / superuser flags wi
|
|||
Practicing account tiering is strongly recommended.
|
||||
|
||||
|
||||
## Database Admin Permissions
|
||||
## Admin Interface Permissions
|
||||
|
||||
If a user does not have the required permissions to perform a certain action in the [Database Admin interface](./db_admin.md), those options will not be displayed.
|
||||
If a user does not have the required permissions to perform a certain action in the admin interface, those options not be displayed.
|
||||
|
||||
If a user is expecting a certain option to be available in the Database Admin interface, but it is not present, it is most likely the case that the user does not have those permissions assigned.
|
||||
If a user is expecting a certain option to be available in the admin interface, but it is not present, it is most likely the case that the user does not have those permissions assigned.
|
||||
|
||||
## Web Interface Permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ The Django Q work must run separately to the web server. This is started as a se
|
|||
|
||||
If the worker is not running, a warning indicator is displayed in the InvenTree menu bar.
|
||||
|
||||
## Admin Center
|
||||
## Admin Interface
|
||||
|
||||
Scheduled, pending and failed tasks can be viewed in the [Admin Center](./admin.md#admin-center), under the *Background Tasks* section.
|
||||
Scheduled tasks can be viewed in the InvenTree admin interface.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ The *Display Settings* screen shows general display configuration options:
|
|||
{{ usersetting("BARCODE_IN_FORM_FIELDS") }}
|
||||
{{ usersetting("DATE_DISPLAY_FORMAT") }}
|
||||
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
|
||||
{{ usersetting("ENABLE_PREVIEW_PANEL") }}
|
||||
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}
|
||||
{{ usersetting("SHOW_FULL_CATEGORY_IN_TABLES")}}
|
||||
{{ usersetting("SHOW_BOM_SUBASSEMBLY_LEVELS")}}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ The following basic options are available:
|
|||
{{ configsetting("INVENTREE_SITE_URL") }} Specify a fixed site URL |
|
||||
{{ configsetting("INVENTREE_TIMEZONE") }} Server timezone |
|
||||
{{ configsetting("INVENTREE_ADMIN_ENABLED") }} Enable the [django administrator interface]({% include "django.html" %}/ref/contrib/admin/) |
|
||||
{{ configsetting("INVENTREE_ADMIN_URL") }} URL for accessing the [Database Admin interface](../settings/db_admin.md) |
|
||||
{{ configsetting("INVENTREE_ADMIN_URL") }} URL for accessing [admin interface](../settings/admin.md) |
|
||||
{{ configsetting("INVENTREE_LANGUAGE") }} Default language |
|
||||
{{ configsetting("INVENTREE_AUTO_UPDATE") }} Database migrations will be run automatically |
|
||||
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ 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.12-alpine3.18 as prebuild
|
||||
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 && \
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
---
|
||||
title: Stock Disassembly
|
||||
---
|
||||
|
||||
## Stock Disassembly
|
||||
|
||||
A stock item of an [assembly](../part/index.md#assembly) part can be *disassembled* back into its component parts, based on the [Bill of Materials](../manufacturing/bom.md) (BOM) for that part. This is the reverse of building an assembly - instead of consuming components to create an assembled item, the assembled item is broken back down into its constituent components.
|
||||
|
||||
This is useful in a number of scenarios, for example:
|
||||
|
||||
- An assembly is being reworked or scrapped, and the still-usable components need to be returned to stock
|
||||
- A supplier ships a "bundled" or "kit" product as a single line item, which needs to be split apart into its individual components before the parts can be used or sold separately (see below)
|
||||
- Correcting an assembly which was built or received in error
|
||||
|
||||
Disassembly is only available for a stock item whose part is marked as an *assembly*, and requires that the part has at least one [BOM line item](../manufacturing/bom.md#bom-line-items) defined.
|
||||
|
||||
To disassemble a stock item, navigate to the stock item detail page and select the *Disassemble* option from the actions menu. This requires that the user has the *Stock: Add* permission, and that the stock item is currently [in stock](./status.md).
|
||||
|
||||
{{ image("stock/stock_options.png", "Stock Options") }}
|
||||
|
||||
### Disassembly Form
|
||||
|
||||
The disassembly form is pre-populated with one line per BOM line item defined for the part, based on the quantity of assemblies being disassembled. Any line item marked as [consumable](../manufacturing/bom.md#consumable-bom-line-items) (whether the BOM line itself or its underlying part is marked consumable), or which points to a [virtual](../part/index.md) part, is excluded, as these components are not expected to be tracked as physical stock. This exclusion is enforced by the API - such a BOM line cannot be submitted for disassembly, even if referenced directly.
|
||||
|
||||
For each line, the following values may be adjusted:
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| Quantity | The total quantity of the component part to generate. This is automatically scaled as the top-level *Quantity* field is changed, unless the user has manually edited it |
|
||||
| Location | An optional destination location for the generated stock item. If not specified, the component is placed in the same location as the disassembled item (or the default location specified at the top of the form) |
|
||||
| Status | An optional [stock status](./status.md) to apply to the generated stock item. If not specified, the component is created with the default *OK* status |
|
||||
| Unit Price | An optional purchase price to record against the generated stock item. If left blank, a price is calculated automatically (see below) |
|
||||
|
||||
A line item can be removed from the form entirely if that particular component is not required to be split out - for example, if it is being scrapped rather than returned to stock. A line cannot be removed if it has installed items associated with it (see below).
|
||||
|
||||
### Quantity
|
||||
|
||||
Only the *available* quantity of a stock item can be disassembled. A [serialized](./traceability.md#serial-numbers) stock item does not have an adjustable quantity - since it represents a single physical unit, it must always be disassembled in its entirety.
|
||||
|
||||
The original stock item is never deleted as a result of disassembly - its quantity is reduced by the disassembled amount, in order to preserve traceability. If a stock item is disassembled down to a zero quantity, it is retained in the database (in an *unavailable* state) rather than removed, even if the item has the [Delete on Deplete](./availability.md#delete-on-deplete) flag set. A serialized item cannot be reduced to a zero quantity, so in this case the original item is instead marked with a *Destroyed* status.
|
||||
|
||||
### Accounting for Installed Items
|
||||
|
||||
If the stock item being disassembled has other stock items [installed](../manufacturing/allocate.md#allocating-tracked-stock) within it (for example, tracked components that were installed during a build order), these installed items **must** be accounted for during disassembly:
|
||||
|
||||
- Each installed item is matched against a BOM line item, based on the component part (including any [substitute](../manufacturing/bom.md#substitute-bom-line-items) or [variant](../part/index.md#assembly) parts allowed for that line)
|
||||
- Matched installed items are *uninstalled* directly, rather than being discarded and re-created - this preserves the original stock item, including its own tracking history, batch code, and purchase price
|
||||
- The quantity requested for the matching BOM line is reduced by the quantity already covered by the installed item(s). A new stock item is only created for any remaining quantity - if the installed items fully cover the required quantity, no new stock item is created for that line
|
||||
- Any installed item which does *not* match one of the selected BOM lines is still uninstalled (it cannot be left "installed" inside a smaller or non-existent parent), but its quantity is **not** subtracted from any line
|
||||
|
||||
Because installed items cannot be partially accounted for, **a stock item with any installed items must be disassembled in its entirety** - a partial disassembly (disassembling less than the full available quantity) is rejected if any items are currently installed.
|
||||
|
||||
The disassembly form displays a count of installed items against each matching BOM line, and lists any "leftover" installed items (which do not match a BOM line) in a separate warning panel.
|
||||
|
||||
### Automatic Cost Allocation
|
||||
|
||||
If the original stock item has a recorded purchase price, and no explicit *Unit Price* has been entered for the generated lines, InvenTree attempts to automatically apportion that cost across the newly generated components:
|
||||
|
||||
- The total cost (unit purchase price × disassembled quantity) is split across the lines, weighted by the existing [pricing](../part/pricing.md) data (average of minimum and maximum overall price) for each component part
|
||||
- If pricing data is not available for *every* line, the cost is instead split evenly on a per-unit basis across all generated units
|
||||
- Cost is only allocated across newly *created* stock items - any matched installed items retain their own existing purchase price, and are excluded from the cost split entirely
|
||||
- If any line has an explicit *Unit Price* provided by the user, automatic cost allocation is skipped entirely, and prices are only applied where explicitly set
|
||||
|
||||
### Traceability
|
||||
|
||||
Disassembling a stock item generates a full audit trail:
|
||||
|
||||
- A `Disassembled into components` entry is added to the tracking history of the original stock item
|
||||
- Each newly created component stock item receives a `Created from disassembly` tracking entry, referencing the original stock item
|
||||
- The *batch code* and source *purchase order* of the original stock item are copied directly to each generated component
|
||||
- If the original stock item was generated by a build order, that build order cannot be directly copied to the new component (since the component was not actually built by that order) - instead, it is recorded as a reference within the `Created from disassembly` tracking entry
|
||||
|
||||
### Purchasing Bundled Items
|
||||
|
||||
Some suppliers only sell a group of components as a single bundled or "kit" product, rather than as individual purchasable line items. Such a bundle can be modelled as an assembly part, purchased and received as a single stock item, and later disassembled into its individual components - with purchase price and traceability data automatically apportioned across the generated components, as described above.
|
||||
|
||||
Refer to the [Bundled Items](../purchasing/purchase_order.md#bundled-items) documentation for a full description of how to set this up.
|
||||
|
||||
### Enforced Limitations
|
||||
|
||||
The following limitations are enforced when disassembling a stock item:
|
||||
|
||||
- The part associated with the stock item must be marked as an *assembly*
|
||||
- The stock item must currently be [in stock](./status.md) - for example, it cannot be allocated to a sales order, installed in another assembly, or already fully consumed
|
||||
- At least one BOM line item must be selected for disassembly
|
||||
- The disassembly quantity cannot exceed the available quantity of the stock item
|
||||
- A serialized stock item must be disassembled in its entirety (its quantity cannot be partially reduced)
|
||||
- Each BOM line may only be referenced once per disassembly operation
|
||||
- A selected BOM line must be a valid line item for the part associated with the stock item
|
||||
- If the stock item has any installed items, it must be disassembled in its entirety
|
||||
|
|
@ -40,31 +40,6 @@ This view displays all tracking entries associated with any stock item linked to
|
|||
!!! info "Deleted Stock Items"
|
||||
Even if a stock item is deleted from the system, the associated stock tracking entries are retained for historical reference. They will be visible in the part tracking history, but not in the stock item tracking history (as the stock item itself has been deleted).
|
||||
|
||||
## Installed Stock Items
|
||||
|
||||
A stock item can be *installed* inside another stock item, forming a parent/child relationship between the two. This is used to represent physical assembly - for example, a serialized PCB assembly which has been fitted with a tracked sub-component, such as a wireless module or a pre-programmed IC.
|
||||
|
||||
An installed stock item is no longer available for regular stock actions (it cannot be moved, allocated to an order, or built into another assembly) while it remains installed - it is only accessible "through" its parent item.
|
||||
|
||||
### How Items Become Installed
|
||||
|
||||
There are two ways that a stock item can become installed inside another:
|
||||
|
||||
- **Tracked build allocation** - when a [tracked BOM item](../manufacturing/allocate.md#allocating-tracked-stock) is allocated to a build order and the build output is completed, the allocated stock item is automatically installed into the completed build output
|
||||
- **Manual installation** - from the *Stock Item Detail* page, a user can manually install one stock item into another, using the *Install Item* action. By default, the selected item's part must appear in the [Bill of Materials](../manufacturing/bom.md) of the parent item's part - this check can be disabled using the {{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION", short=True) }} setting
|
||||
|
||||
### Viewing Installed Items
|
||||
|
||||
Any stock items installed within a particular stock item are displayed on the *Installed Items* tab of the *Stock Item Detail* page.
|
||||
|
||||
By default, installed stock items are hidden from general stock item tables (as they are not directly available for use) - this can be changed using the {{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS", short=True) }} setting.
|
||||
|
||||
### Removing Installed Items
|
||||
|
||||
An installed stock item can be removed (uninstalled) from its parent using the *Uninstall Item* action, which returns the item to a selected stock location and makes it available for regular stock actions once more.
|
||||
|
||||
Additionally, installed items are automatically uninstalled when the parent item is [disassembled](./disassemble.md#accounting-for-installed-items) into its component parts - refer to that page for a detailed description of how installed items are matched against Bill of Materials line items during disassembly.
|
||||
|
||||
## Stock Tracking Settings
|
||||
|
||||
There are a number of configuration options available for controlling the behavior of stock tracking functionality in the [system settings view](../settings/global.md):
|
||||
|
|
|
|||
|
|
@ -148,4 +148,3 @@ The following [global settings](../settings/global.md) are available for transfe
|
|||
{{ globalsetting("TRANSFERORDER_ENABLED") }}
|
||||
{{ globalsetting("TRANSFERORDER_REFERENCE_PATTERN") }}
|
||||
{{ globalsetting("TRANSFERORDER_REQUIRE_RESPONSIBLE") }}
|
||||
{{ globalsetting("TRANSFERORDER_EDIT_COMPLETED_ORDERS") }}
|
||||
|
|
|
|||
|
|
@ -91,12 +91,7 @@ nav:
|
|||
- Privacy: privacy.md
|
||||
- Concepts:
|
||||
- Terminology: concepts/terminology.md
|
||||
- User Interface:
|
||||
- Overview: concepts/ui/index.md
|
||||
- Tables: concepts/ui/tables.md
|
||||
- Preview Panels: concepts/ui/preview_panels.md
|
||||
- Forms: concepts/ui/forms.md
|
||||
- Global Search: concepts/ui/global_search.md
|
||||
- User Interface: concepts/user_interface.md
|
||||
- Threat Model: concepts/threat_model.md
|
||||
- Physical Units: concepts/units.md
|
||||
- Companies: concepts/company.md
|
||||
|
|
@ -135,7 +130,6 @@ nav:
|
|||
- Parts: part/index.md
|
||||
- Creating Parts: part/create.md
|
||||
- Virtual Parts: part/virtual.md
|
||||
- Consumable Parts: part/consumable.md
|
||||
- Part Views: part/views.md
|
||||
- Tracking: part/trackable.md
|
||||
- Revisions: part/revision.md
|
||||
|
|
@ -151,7 +145,6 @@ nav:
|
|||
- Stock Tracking: stock/tracking.md
|
||||
- Stock Status: stock/status.md
|
||||
- Adjusting Stock: stock/adjust.md
|
||||
- Stock Disassembly: stock/disassemble.md
|
||||
- Stock Expiry: stock/expiry.md
|
||||
- Stock Ownership: stock/owner.md
|
||||
- Test Results: stock/test.md
|
||||
|
|
@ -181,8 +174,6 @@ nav:
|
|||
- Template Editor: report/template_editor.md
|
||||
- Reports: report/report.md
|
||||
- Labels: report/labels.md
|
||||
- Report Assets: report/assets.md
|
||||
- Report Snippets: report/snippets.md
|
||||
- Context Variables: report/context_variables.md
|
||||
- Helper Functions: report/helpers.md
|
||||
- Barcodes: report/barcodes.md
|
||||
|
|
@ -191,8 +182,7 @@ nav:
|
|||
- Global Settings: settings/global.md
|
||||
- User Settings: settings/user.md
|
||||
- Reference Patterns: settings/reference.md
|
||||
- Admin Center: settings/admin.md
|
||||
- Database Admin Interface: settings/db_admin.md
|
||||
- Admin Interface: settings/admin.md
|
||||
- Setup:
|
||||
- User Permissions: settings/permissions.md
|
||||
- Single Sign on: settings/SSO.md
|
||||
|
|
@ -376,7 +366,7 @@ extra:
|
|||
# provider: google
|
||||
# property: UA-143467500-1
|
||||
|
||||
min_python_version: 3.12
|
||||
min_python_version: 3.11
|
||||
min_invoke_version: 2.0.0
|
||||
django_version: 5.2
|
||||
docker_postgres_version: 17
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@
|
|||
},
|
||||
{
|
||||
"pattern": "https://opensource.org/license/MIT"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://dev.azure.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile docs/requirements.in -o docs/requirements.txt -c src/backend/requirements.txt
|
||||
anyio==4.14.2 \
|
||||
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
|
||||
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
|
||||
anyio==4.12.1 \
|
||||
--hash=sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703 \
|
||||
--hash=sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c
|
||||
# via httpx
|
||||
babel==2.18.0 \
|
||||
--hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
|
||||
|
|
@ -11,21 +11,22 @@ babel==2.18.0 \
|
|||
# -c src/backend/requirements.txt
|
||||
# mkdocs-git-revision-date-localized-plugin
|
||||
# mkdocs-material
|
||||
backrefs==7.0 \
|
||||
--hash=sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82 \
|
||||
--hash=sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475 \
|
||||
--hash=sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9 \
|
||||
--hash=sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9 \
|
||||
--hash=sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12 \
|
||||
--hash=sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a
|
||||
backrefs==6.2 \
|
||||
--hash=sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be \
|
||||
--hash=sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8 \
|
||||
--hash=sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b \
|
||||
--hash=sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7 \
|
||||
--hash=sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90 \
|
||||
--hash=sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7 \
|
||||
--hash=sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49
|
||||
# via mkdocs-material
|
||||
beautifulsoup4==4.15.0 \
|
||||
--hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \
|
||||
--hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9
|
||||
beautifulsoup4==4.14.3 \
|
||||
--hash=sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb \
|
||||
--hash=sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86
|
||||
# via mkdocs-mermaid2-plugin
|
||||
bracex==3.0 \
|
||||
--hash=sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5 \
|
||||
--hash=sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111
|
||||
bracex==2.6 \
|
||||
--hash=sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952 \
|
||||
--hash=sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7
|
||||
# via wcmatch
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
|
|
@ -168,9 +169,9 @@ charset-normalizer==3.4.7 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
click==8.4.2 \
|
||||
--hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \
|
||||
--hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
|
||||
click==8.4.1 \
|
||||
--hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \
|
||||
--hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96
|
||||
# via
|
||||
# mkdocs
|
||||
# neoteroi-mkdocs
|
||||
|
|
@ -201,13 +202,12 @@ gitdb==4.0.12 \
|
|||
--hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \
|
||||
--hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf
|
||||
# via gitpython
|
||||
gitpython==3.1.52 \
|
||||
--hash=sha256:79a36ee1f83523214a3f72d56cf1c4e490d577dc61af77e43dfe5862bd9da01a \
|
||||
--hash=sha256:de0a8ad86274c6e75ae8b37dd055ba68f19818c813108642263227b20775b48e
|
||||
gitpython==3.1.50 \
|
||||
--hash=sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc \
|
||||
--hash=sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9
|
||||
# via mkdocs-git-revision-date-localized-plugin
|
||||
griffelib==2.1.0 \
|
||||
--hash=sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813 \
|
||||
--hash=sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00
|
||||
griffelib==2.0.0 \
|
||||
--hash=sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f
|
||||
# via mkdocstrings-python
|
||||
h11==0.16.0 \
|
||||
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||
|
|
@ -246,9 +246,9 @@ jinja2==3.1.6 \
|
|||
# mkdocstrings
|
||||
# neoteroi-mkdocs
|
||||
# properdocs
|
||||
jsbeautifier==2.0.3 \
|
||||
--hash=sha256:9579d4e9dbaa00383f3efdff4c98c8140bb85ba319398e8b97cdaba27abd6ba3 \
|
||||
--hash=sha256:f0190e279a2cdb827556ada63f41c9c63c11f8116ee06e264b24aa50311cbee3
|
||||
jsbeautifier==1.15.4 \
|
||||
--hash=sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592 \
|
||||
--hash=sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528
|
||||
# via mkdocs-mermaid2-plugin
|
||||
markdown==3.10.2 \
|
||||
--hash=sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950 \
|
||||
|
|
@ -436,9 +436,9 @@ mkdocstrings[python]==1.0.4 \
|
|||
# via
|
||||
# -r docs/requirements.in
|
||||
# mkdocstrings-python
|
||||
mkdocstrings-python==2.0.5 \
|
||||
--hash=sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d \
|
||||
--hash=sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594
|
||||
mkdocstrings-python==2.0.3 \
|
||||
--hash=sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 \
|
||||
--hash=sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8
|
||||
# via mkdocstrings
|
||||
neoteroi-mkdocs==1.2.0 \
|
||||
--hash=sha256:58e25cb1b9db093ffa8d12bdb33264bf567cac30fb964b56e0a493efa749ad6e \
|
||||
|
|
@ -456,9 +456,9 @@ paginate==0.5.7 \
|
|||
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
|
||||
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
|
||||
# via mkdocs-material
|
||||
pathspec==1.1.1 \
|
||||
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
|
||||
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
|
||||
pathspec==1.0.4 \
|
||||
--hash=sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645 \
|
||||
--hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723
|
||||
# via
|
||||
# mkdocs
|
||||
# mkdocs-macros-plugin
|
||||
|
|
@ -480,9 +480,9 @@ pygments==2.20.0 \
|
|||
# via
|
||||
# mkdocs-material
|
||||
# rich
|
||||
pymdown-extensions==11.0.1 \
|
||||
--hash=sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 \
|
||||
--hash=sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0
|
||||
pymdown-extensions==10.21.3 \
|
||||
--hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \
|
||||
--hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6
|
||||
# via
|
||||
# mkdocs-material
|
||||
# mkdocs-mermaid2-plugin
|
||||
|
|
@ -606,14 +606,15 @@ six==1.17.0 \
|
|||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# jsbeautifier
|
||||
# python-dateutil
|
||||
smmap==5.0.3 \
|
||||
--hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \
|
||||
--hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f
|
||||
# via gitdb
|
||||
soupsieve==2.8.4 \
|
||||
--hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \
|
||||
--hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65
|
||||
soupsieve==2.8.3 \
|
||||
--hash=sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349 \
|
||||
--hash=sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95
|
||||
# via beautifulsoup4
|
||||
super-collections==0.6.2 \
|
||||
--hash=sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a \
|
||||
|
|
@ -670,7 +671,7 @@ watchdog==6.0.0 \
|
|||
# via
|
||||
# mkdocs
|
||||
# properdocs
|
||||
wcmatch==11.0 \
|
||||
--hash=sha256:3a5977ace27e075eef67eb03d539563f1a19018b62881949a42932cf66926934 \
|
||||
--hash=sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96
|
||||
wcmatch==10.1 \
|
||||
--hash=sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a \
|
||||
--hash=sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af
|
||||
# via mkdocs-include-markdown-plugin
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ skip-magic-trailing-comma = true
|
|||
line-ending = "auto"
|
||||
|
||||
[tool.uv.pip]
|
||||
python-version = "3.12"
|
||||
python-version = "3.11"
|
||||
no-strip-extras=true
|
||||
generate-hashes=true
|
||||
|
||||
|
|
@ -105,16 +105,17 @@ generate-hashes=true
|
|||
extra-paths = ["src/backend/InvenTree"]
|
||||
|
||||
[tool.ty.rules]
|
||||
unresolved-attribute="ignore" # 837 # need Plugin Mixin typing
|
||||
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
|
||||
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
|
||||
call-non-callable="ignore" # 8 ##
|
||||
invalid-assignment="ignore" # 40 # need to wait for better django field stubs
|
||||
invalid-method-override="ignore" # 103
|
||||
invalid-return-type="ignore" # 44 ##
|
||||
invalid-assignment="ignore" # 17 # need to wait for better django field stubs
|
||||
invalid-method-override="ignore" # 104
|
||||
invalid-return-type="ignore" # 22 ##
|
||||
# possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
|
||||
unknown-argument="ignore" # 3 # need to wait for better django field stubs
|
||||
invalid-argument-type="ignore" # 49
|
||||
no-matching-overload="ignore" # 10 # need to wait for betterdjango field stubs
|
||||
|
||||
# warn about unused ignore comments
|
||||
unused-ignore-comment = "error"
|
||||
no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
|
||||
# possibly-unbound-attribute="ignore" # 21
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/backend/InvenTree", "InvenTree"]
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ python:
|
|||
- requirements: src/backend/requirements.txt
|
||||
|
||||
build:
|
||||
os: "ubuntu-24.04"
|
||||
os: "ubuntu-22.04"
|
||||
tools:
|
||||
python: "3.12"
|
||||
python: "3.11"
|
||||
jobs:
|
||||
post_install:
|
||||
- pip install -U invoke
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from pathlib import Path
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.urls import path, reverse
|
||||
|
|
@ -31,8 +30,6 @@ from common.settings import get_global_setting
|
|||
from InvenTree import helpers, ready
|
||||
from InvenTree.auth_overrides import registration_enabled
|
||||
from InvenTree.mixins import ListCreateAPI
|
||||
from InvenTree.tasks import batch_offload_tasks
|
||||
from plugin.base.event.events import batch_events
|
||||
from plugin.serializers import MetadataSerializer
|
||||
from users.models import ApiToken
|
||||
from users.permissions import check_user_permission, prefetch_rule_sets
|
||||
|
|
@ -513,7 +510,7 @@ class BulkCreateMixin:
|
|||
if has_unique_errors:
|
||||
raise ValidationError(unique_errors)
|
||||
|
||||
with transaction.atomic(), batch_events(), batch_offload_tasks():
|
||||
with transaction.atomic():
|
||||
for item in data:
|
||||
serializer = self.get_serializer(data=item)
|
||||
if serializer.is_valid():
|
||||
|
|
@ -537,12 +534,8 @@ class BulkUpdateMixin(BulkOperationMixin):
|
|||
|
||||
Bulk update allows for multiple items to be updated in a single API query,
|
||||
rather than using multiple API calls to the various detail endpoints.
|
||||
|
||||
Each instance is validated and saved individually, so that any custom save methods are triggered.
|
||||
"""
|
||||
|
||||
BULK_ID_FIELD: str = 'pk'
|
||||
|
||||
def validate_update(self, queryset, request) -> None:
|
||||
"""Perform validation right before updating.
|
||||
|
||||
|
|
@ -586,35 +579,17 @@ class BulkUpdateMixin(BulkOperationMixin):
|
|||
# Perform the update operation
|
||||
data = request.data
|
||||
|
||||
# Extract the primary key values up-front:
|
||||
# Each instance is re-fetched from the database immediately before it is
|
||||
# updated, as saving one instance may alter database state which other
|
||||
# instances in the queryset depend on (e.g. MPTT tree structure fields).
|
||||
# Saving a stale instance can result in database corruption (and it must
|
||||
# be the *instance* that is fresh - refresh_from_db is not sufficient here,
|
||||
# as MPTT caches original field values when the instance is loaded).
|
||||
pk_values = sorted(queryset.values_list(self.BULK_ID_FIELD, flat=True))
|
||||
n = queryset.count()
|
||||
|
||||
instance_data = []
|
||||
|
||||
with transaction.atomic(), batch_events(), batch_offload_tasks():
|
||||
with transaction.atomic():
|
||||
# Perform object update
|
||||
# Note that we do not perform a bulk-update operation here,
|
||||
# as we want to trigger any custom post_save methods on the model
|
||||
|
||||
# Run validation first
|
||||
for pk in pk_values:
|
||||
try:
|
||||
instance = queryset.select_for_update(of=('self',)).get(**{
|
||||
self.BULK_ID_FIELD: pk
|
||||
})
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _(
|
||||
'Item no longer matches the provided criteria'
|
||||
)
|
||||
})
|
||||
|
||||
for instance in queryset:
|
||||
serializer = self.get_serializer(instance, data=data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
|
@ -622,7 +597,7 @@ class BulkUpdateMixin(BulkOperationMixin):
|
|||
instance_data.append(serializer.data)
|
||||
|
||||
return Response(
|
||||
{'success': 'Updated multiple items', 'items': instance_data}, status=200
|
||||
{'success': f'Updated {n} items', 'items': instance_data}, status=200
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -694,7 +669,7 @@ class CommonBulkDeleteMixin(BulkOperationMixin):
|
|||
# Keep track of how many items we deleted
|
||||
n_deleted = queryset.count()
|
||||
|
||||
with transaction.atomic(), batch_events(), batch_offload_tasks():
|
||||
with transaction.atomic():
|
||||
# Perform object deletion
|
||||
# Note that we do not perform a bulk-delete operation here,
|
||||
# as we want to trigger any custom post_delete methods on the model
|
||||
|
|
|
|||
|
|
@ -1,54 +1,11 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 523
|
||||
INVENTREE_API_VERSION = 511
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v523 -> 2026-07-14 : https://github.com/inventree/InvenTree/pull/12391
|
||||
- Adds "bulk delete" support for order line item API endpoints (PurchaseOrder / SalesOrder / ReturnOrder / TransferOrder)
|
||||
- Adds "bulk delete" support for order extra line item API endpoints
|
||||
- Completed TransferOrder objects are now "locked" (controlled by the new TRANSFERORDER_EDIT_COMPLETED_ORDERS global setting)
|
||||
|
||||
v522 -> 2026-07-14 : https://github.com/inventree/InvenTree/pull/12388
|
||||
- Adds "unique" field to the ParameterTemplate model
|
||||
|
||||
v521 -> 2026-07-12 : https://github.com/inventree/InvenTree/pull/12360
|
||||
- Removes the MPTT mixin from the StockItem model, and removes the self-referential tree structure from the database.
|
||||
|
||||
v520 -> 2026-07-11 : https://github.com/inventree/InvenTree/pull/12310
|
||||
- Adds new "disassemble" API endpoint for stock items
|
||||
- Allows a stock item to be broken down into component parts, based on its Bill of Materials
|
||||
|
||||
v519 -> 2026-07-09 : https://github.com/inventree/InvenTree/pull/TODO
|
||||
- Adds optional "roles" and "permissions" fields to the /user/me/ API endpoint, via the "?roles=true" query parameter
|
||||
|
||||
v518 -> 2026-07-09 : https://github.com/inventree/InvenTree/pull/12341
|
||||
- Enable import of internal part prices via the API
|
||||
|
||||
v517 -> 2026-07-08 : https://github.com/inventree/InvenTree/pull/12336
|
||||
- Fix currency code options for the PartPricing model and API endpoints
|
||||
|
||||
v516 -> 2026-07-03 : https://github.com/inventree/InvenTree/pull/12295
|
||||
- Adds "consumable" field to the Part model and API endpoints
|
||||
|
||||
v515 -> 2026-07-03 : https://github.com/inventree/InvenTree/pull/12298
|
||||
- Change the file fields definition to binary (from uri) in the upload requests
|
||||
|
||||
v514 -> 2026-07-02 : https://github.com/inventree/InvenTree/pull/12294
|
||||
- Adds "duplicate" field to the BuildOrder, Company, ManufacturerPart, SupplierPart and SalesOrderShipment API endpoints
|
||||
- Order duplication options: renames "order_id" field to "original", which now performs primary-key validation
|
||||
- Part duplication options: renames "part" field to "original"
|
||||
|
||||
v513 -> 2026-06-25 : https://github.com/inventree/InvenTree/pull/12250
|
||||
- Adds "active" field to the ProjectCode model and API endpoints
|
||||
|
||||
v512 -> 2026-06-20 : https://github.com/inventree/InvenTree/pull/12022
|
||||
- Adds optional "merge" field to each item in the Stock Transfer API endpoint
|
||||
- When merge is enabled, transferred stock is combined into compatible existing stock at the destination
|
||||
- Stock merge tracking entries now include an "added" delta field.
|
||||
|
||||
v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204
|
||||
- Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ def log_error(
|
|||
error_data: Optional[str] = None,
|
||||
scope: Optional[str] = None,
|
||||
plugin: Optional[str] = None,
|
||||
user_id: Optional[int] = None,
|
||||
):
|
||||
"""Log an error to the database.
|
||||
|
||||
|
|
@ -38,7 +37,6 @@ def log_error(
|
|||
error_data: The error data (optional, overrides 'data')
|
||||
scope: The scope of the error (optional)
|
||||
plugin: The plugin name associated with this error (optional)
|
||||
user_id: The user ID associated with this error (optional)
|
||||
"""
|
||||
import InvenTree.ready
|
||||
|
||||
|
|
@ -73,9 +71,6 @@ def log_error(
|
|||
except AttributeError:
|
||||
data = 'No traceback information available'
|
||||
|
||||
if user_id:
|
||||
data = f'User ID: {user_id}\n{data}'
|
||||
|
||||
# Log error to stderr
|
||||
logger.error(info)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""Custom fields used in InvenTree."""
|
||||
|
||||
import sys
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
|
|
@ -14,7 +13,6 @@ from djmoney.models.fields import MoneyField as ModelMoneyField
|
|||
from djmoney.models.validators import MinMoneyValidator
|
||||
from rest_framework.fields import URLField as RestURLField
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.relations import PrimaryKeyRelatedField
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.ready
|
||||
|
|
@ -49,42 +47,6 @@ class InvenTreeRestURLField(RestURLField):
|
|||
return super().run_validation(data=data)
|
||||
|
||||
|
||||
class PrefetchedPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
||||
"""A PrimaryKeyRelatedField which resolves against a pre-fetched {pk: instance} map.
|
||||
|
||||
PrimaryKeyRelatedField normally issues one .get() query per list entry when used
|
||||
inside a many=True nested serializer - for large lists (hundreds of related objects)
|
||||
that becomes an O(n) query cost just to validate the request. The parent serializer
|
||||
should instead bulk-fetch all referenced objects in a single query and stash the
|
||||
{pk: instance} map in self.context[cache_key] (typically from an overridden
|
||||
to_internal_value()); this field then does an O(1) dict lookup instead of hitting
|
||||
the database.
|
||||
|
||||
Falls back to the default per-item query if no cache has been populated (or the pk
|
||||
is missing from it), so this field remains safe to use standalone - e.g. in tests
|
||||
constructing the child serializer directly, or for a pk that's genuinely invalid.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_key: str, **kwargs):
|
||||
"""Store the context key under which the parent serializer stashes its prefetch cache."""
|
||||
self.cache_key = cache_key
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""Resolve 'data' (a raw pk value) against the prefetch cache, if available."""
|
||||
cache = self.context.get(self.cache_key)
|
||||
|
||||
try:
|
||||
pk = int(data)
|
||||
except (TypeError, ValueError):
|
||||
pk = None
|
||||
|
||||
if not cache or pk not in cache:
|
||||
return super().to_internal_value(data)
|
||||
|
||||
return cache[pk]
|
||||
|
||||
|
||||
class InvenTreeURLField(models.URLField):
|
||||
"""Custom URL field which has custom scheme validators."""
|
||||
|
||||
|
|
@ -97,34 +59,6 @@ class InvenTreeURLField(models.URLField):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class InvenTreeUUIDField(models.UUIDField):
|
||||
"""UUIDField which is always stored as a char(32) column on MySQL / MariaDB.
|
||||
|
||||
On MariaDB 10.7+, Django maps UUIDField to the native 'uuid' column type,
|
||||
and writes 36-character (hyphenated) values to match.
|
||||
However, databases migrated under older Django / MariaDB versions retain
|
||||
their original char(32) columns, into which a 36-character value does not fit.
|
||||
|
||||
To ensure consistent behavior across all supported database backends,
|
||||
we force the legacy char(32) storage format on MySQL / MariaDB.
|
||||
|
||||
Ref: https://docs.djangoproject.com/en/5.2/releases/5.0/#migrating-existing-uuidfield-on-mariadb-10-7
|
||||
"""
|
||||
|
||||
def db_type(self, connection):
|
||||
"""Force a char(32) column type on MySQL / MariaDB backends."""
|
||||
if connection.vendor == 'mysql':
|
||||
return 'char(32)'
|
||||
return super().db_type(connection)
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
"""Store values in 32-character hex format on MySQL / MariaDB backends."""
|
||||
value = super().get_db_prep_value(value, connection, prepared)
|
||||
if connection.vendor == 'mysql' and isinstance(value, uuid.UUID):
|
||||
value = value.hex
|
||||
return value
|
||||
|
||||
|
||||
def money_kwargs(**kwargs):
|
||||
"""Returns the database settings for MoneyFields."""
|
||||
from common.currency import currency_code_mappings
|
||||
|
|
@ -137,9 +71,7 @@ def money_kwargs(**kwargs):
|
|||
kwargs['decimal_places'] = 6
|
||||
|
||||
if 'currency_choices' not in kwargs:
|
||||
# Pass the function itself (not the evaluated result) so that the
|
||||
# available currency options are resolved dynamically.
|
||||
kwargs['currency_choices'] = currency_code_mappings
|
||||
kwargs['currency_choices'] = currency_code_mappings()
|
||||
|
||||
if InvenTree.ready.isRunningMigrations():
|
||||
# During migrations, avoid setting a default currency
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
"""Database helper functions for InvenTree."""
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def bulk_create_and_fetch(
|
||||
model, items, id_field: str = 'pk', filters: Optional[dict] = None
|
||||
) -> QuerySet:
|
||||
"""Bulk create items in the database, and return a queryset of the created items.
|
||||
|
||||
Arguments:
|
||||
model: The Django model class to create instances of.
|
||||
items: A list of dictionaries containing the data for each item to be created.
|
||||
id_field: The name of the field to use as the unique identifier for the created items.
|
||||
filters: Optional dictionary of filters to apply when fetching the created items.
|
||||
|
||||
Returns:
|
||||
A Django QuerySet containing the created items.
|
||||
|
||||
This helper method is required because the Django bulk_create() method
|
||||
does not guarantee that the ID values of the created items will be populated in the returned objects.
|
||||
In particular, MySQL does not support returning the ID values of bulk created items.
|
||||
|
||||
So, we provide temporary metadata to the created items,
|
||||
which can be used to fetch the created items from the database.
|
||||
|
||||
Assumptions:
|
||||
- The provided model type has a "metadata" attribute which can be overloaded for this purpose
|
||||
- No "metadata" is provided in the input items, as this will be overwritten by the method
|
||||
- The model type has an incrementing ID field (default: "pk")
|
||||
|
||||
"""
|
||||
bulk_create_id = uuid.uuid4().hex
|
||||
|
||||
# Generate temporary metadata for bulk fetching
|
||||
metadata = {'bulk_create_id': bulk_create_id}
|
||||
|
||||
lookup_filters = dict(filters) if filters else {}
|
||||
|
||||
lookup_filters['metadata__bulk_create_id'] = bulk_create_id
|
||||
|
||||
if id_field:
|
||||
# Find the "most recent" item in the database, to set a search floor
|
||||
if instance := model.objects.order_by(f'-{id_field}').first():
|
||||
lookup_filters[f'{id_field}__gt'] = getattr(instance, id_field)
|
||||
|
||||
# Overwrite the metadata values
|
||||
for item in items:
|
||||
item.metadata = metadata
|
||||
|
||||
model.objects.bulk_create(items, batch_size=500)
|
||||
|
||||
instances = model.objects.filter(**lookup_filters)
|
||||
|
||||
pks = list(instances.values_list(id_field or 'pk', flat=True))
|
||||
|
||||
# Override the metadata values to remove the temporary bulk_create_id
|
||||
instances.update(metadata=None)
|
||||
|
||||
# Fetch the newly created items (by primary key, as the metadata filter no longer matches)
|
||||
return model.objects.filter(**{f'{id_field or "pk"}__in': pks})
|
||||
|
|
@ -43,6 +43,16 @@ class Command(BaseCommand):
|
|||
except Exception:
|
||||
logger.info('Error rebuilding PartCategory objects')
|
||||
|
||||
# StockItem model
|
||||
try:
|
||||
logger.info('Rebuilding StockItem objects')
|
||||
|
||||
from stock.models import StockItem
|
||||
|
||||
StockItem.objects.rebuild()
|
||||
except Exception:
|
||||
logger.info('Error rebuilding StockItem objects')
|
||||
|
||||
# StockLocation model
|
||||
try:
|
||||
logger.info('Rebuilding StockLocation objects')
|
||||
|
|
|
|||
|
|
@ -597,21 +597,12 @@ class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
|
|||
|
||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||
|
||||
# Skip any parameters which are linked to a template with a uniqueness requirement,
|
||||
# as copying these values would create conflicting (duplicate) values
|
||||
copyable_parameters = [
|
||||
parameter
|
||||
for parameter in other.parameters.all().select_related('template')
|
||||
if parameter.template.unique
|
||||
== common.models.ParameterTemplate.UniqueOptions.NONE
|
||||
]
|
||||
|
||||
template_ids = [parameter.template.pk for parameter in copyable_parameters]
|
||||
template_ids = [parameter.template.pk for parameter in other.parameters.all()]
|
||||
|
||||
# Remove all conflicting parameters first
|
||||
self.parameters_list.filter(template__pk__in=template_ids).delete()
|
||||
|
||||
for parameter in copyable_parameters:
|
||||
for parameter in other.parameters.all():
|
||||
parameter.pk = None
|
||||
parameter.model_id = self.pk
|
||||
parameter.model_type = content_type
|
||||
|
|
@ -744,7 +735,6 @@ class InvenTreeTree(ContentTypeMixin, MPTTModel):
|
|||
|
||||
order_insertion_by = ['name']
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Handle the deletion of a tree node.
|
||||
|
||||
|
|
@ -815,7 +805,18 @@ class InvenTreeTree(ContentTypeMixin, MPTTModel):
|
|||
next_tree_id += 1
|
||||
|
||||
# 3. Rebuild the model tree(s) as required
|
||||
self.__class__.rebuild_trees(trees)
|
||||
# - If any partial rebuilds fail, we will rebuild the entire tree
|
||||
|
||||
result = True
|
||||
|
||||
for tree_id in trees:
|
||||
if tree_id:
|
||||
if not self.partial_rebuild(tree_id):
|
||||
result = False
|
||||
|
||||
if not result:
|
||||
# Rebuild the entire tree (expensive!!!)
|
||||
self.__class__.objects.rebuild()
|
||||
|
||||
def handle_tree_delete(self, delete_children=False, delete_items=False):
|
||||
"""Delete a single instance of the tree, based on provided kwargs.
|
||||
|
|
@ -932,89 +933,40 @@ class InvenTreeTree(ContentTypeMixin, MPTTModel):
|
|||
# New instance, so we need to rebuild the tree (if it has a parent)
|
||||
trees.add(self.tree_id)
|
||||
|
||||
# Flag to indicate that a tree rebuild task was triggered by this save
|
||||
self._tree_rebuild_offloaded = False
|
||||
for tree_id in trees:
|
||||
if tree_id:
|
||||
self.partial_rebuild(tree_id)
|
||||
|
||||
if len(trees) > 0:
|
||||
# Offload the tree rebuild(s) to the background worker.
|
||||
# Note that repeated calls are de-duplicated (per tree),
|
||||
# so a bulk operation results in a single rebuild per affected tree.
|
||||
ran_sync = self.__class__.offload_tree_rebuild(trees)
|
||||
self._tree_rebuild_offloaded = True
|
||||
# A tree update was performed, so we need to refresh the instance
|
||||
try:
|
||||
self.refresh_from_db()
|
||||
except TransactionManagementError:
|
||||
# If we are inside a transaction block, we cannot refresh from db
|
||||
pass
|
||||
except Exception as e:
|
||||
# Any other error is unexpected
|
||||
InvenTree.sentry.report_exception(e)
|
||||
InvenTree.exceptions.log_error(f'{self.__class__.__name__}.save')
|
||||
|
||||
if ran_sync:
|
||||
# The tree was rebuilt synchronously, so refresh the instance
|
||||
try:
|
||||
self.refresh_from_db()
|
||||
except TransactionManagementError:
|
||||
# If we are inside a transaction block, we cannot refresh from db
|
||||
pass
|
||||
except Exception as e:
|
||||
# Any other error is unexpected
|
||||
InvenTree.sentry.report_exception(e)
|
||||
InvenTree.exceptions.log_error(f'{self.__class__.__name__}.save')
|
||||
|
||||
@classmethod
|
||||
def offload_tree_rebuild(cls, tree_ids) -> bool:
|
||||
"""Offload a rebuild of the specified trees to the background worker.
|
||||
|
||||
- The tree structure (and pathstring values, where applicable) are rebuilt for each tree
|
||||
- If the background worker is not running, the rebuild is performed synchronously
|
||||
- Identical pending tasks are skipped, so repeated calls (e.g. during a bulk
|
||||
operation) result in (at most) a single queued rebuild per affected tree
|
||||
|
||||
Returns:
|
||||
bool: True if any rebuild was performed synchronously (in the calling thread)
|
||||
"""
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
ran_sync = False
|
||||
|
||||
for tree_id in tree_ids:
|
||||
if tree_id:
|
||||
result = offload_task(
|
||||
'InvenTree.tasks.rebuild_model_tree', cls._meta.label_lower, tree_id
|
||||
)
|
||||
|
||||
if result is True:
|
||||
# offload_task returns True if the task ran synchronously
|
||||
ran_sync = True
|
||||
|
||||
return ran_sync
|
||||
|
||||
@classmethod
|
||||
def rebuild_trees(cls, tree_ids) -> None:
|
||||
"""Rebuild the specified trees, with fallback to a full rebuild.
|
||||
|
||||
- Perform a partial rebuild for each provided tree_id
|
||||
- If any partial rebuild fails, rebuild the entire tree (expensive!!!)
|
||||
"""
|
||||
result = True
|
||||
|
||||
for tree_id in tree_ids:
|
||||
if tree_id and not cls.partial_rebuild(tree_id):
|
||||
result = False
|
||||
|
||||
if not result:
|
||||
# Rebuild the entire tree (expensive!!!)
|
||||
cls.objects.rebuild()
|
||||
|
||||
@classmethod
|
||||
def partial_rebuild(cls, tree_id: int) -> bool:
|
||||
def partial_rebuild(self, tree_id: int) -> bool:
|
||||
"""Perform a partial rebuild of the tree structure.
|
||||
|
||||
If a failure occurs, log the error and return False.
|
||||
"""
|
||||
try:
|
||||
cls.objects.partial_rebuild(tree_id)
|
||||
self.__class__.objects.partial_rebuild(tree_id)
|
||||
return True
|
||||
except Exception as e:
|
||||
# This is a critical error, explicitly report to sentry
|
||||
InvenTree.sentry.report_exception(e)
|
||||
|
||||
InvenTree.exceptions.log_error(f'{cls.__name__}.partial_rebuild')
|
||||
InvenTree.exceptions.log_error(f'{self.__class__.__name__}.partial_rebuild')
|
||||
logger.exception(
|
||||
'Failed to rebuild tree <%s> for %s: %s', tree_id, cls.__name__, e
|
||||
'Failed to rebuild tree for %s <%s>: %s',
|
||||
self.__class__.__name__,
|
||||
self.pk,
|
||||
e,
|
||||
)
|
||||
return False
|
||||
|
||||
|
|
@ -1117,11 +1069,6 @@ class PathStringMixin(models.Model):
|
|||
# Rebuild upper first, to ensure the lower nodes are updated correctly
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Determine if a tree rebuild task was already triggered by this save
|
||||
# (e.g. if the node was re-parented) - if so, the pathstring values
|
||||
# for any lower nodes are updated by that task
|
||||
rebuild_offloaded = getattr(self, '_tree_rebuild_offloaded', False)
|
||||
|
||||
# Ensure that the pathstring is correctly constructed
|
||||
pathstring = self.construct_pathstring(refresh=True)
|
||||
|
||||
|
|
@ -1132,10 +1079,12 @@ class PathStringMixin(models.Model):
|
|||
self.pathstring = pathstring
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update the pathstring values for any lower nodes,
|
||||
# by offloading the update to the background worker
|
||||
if not rebuild_offloaded and self.get_descendant_count() > 0:
|
||||
self.__class__.offload_tree_rebuild([self.tree_id])
|
||||
# Bulk-update any child nodes, if applicable
|
||||
lower_nodes = list(
|
||||
self.get_descendants(include_self=False).values_list('pk', flat=True)
|
||||
)
|
||||
|
||||
self.rebuild_lower_nodes(lower_nodes)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Custom delete method for PathStringMixin.
|
||||
|
|
@ -1153,7 +1102,9 @@ class PathStringMixin(models.Model):
|
|||
)
|
||||
|
||||
# Store the node ID values for lower nodes, before we delete this one
|
||||
lower_nodes = self.get_lower_nodes()
|
||||
lower_nodes = list(
|
||||
self.get_descendants(include_self=False).values_list('pk', flat=True)
|
||||
)
|
||||
|
||||
# Delete this node - after which we expect the tree structure will be updated
|
||||
super().delete(*args, **kwargs)
|
||||
|
|
@ -1165,12 +1116,6 @@ class PathStringMixin(models.Model):
|
|||
"""String representation of a category is the full path to that category."""
|
||||
return f'{self.pathstring} - {self.description}'
|
||||
|
||||
def get_lower_nodes(self) -> list[int]:
|
||||
"""Return a list of all lower nodes in the tree."""
|
||||
return list(
|
||||
self.get_descendants(include_self=False).values_list('pk', flat=True)
|
||||
)
|
||||
|
||||
def rebuild_lower_nodes(self, lower_nodes: list[int]):
|
||||
"""Rebuild the pathstring for lower nodes in the tree.
|
||||
|
||||
|
|
@ -1191,52 +1136,6 @@ class PathStringMixin(models.Model):
|
|||
if len(nodes_to_update) > 0:
|
||||
self.__class__.objects.bulk_update(nodes_to_update, ['pathstring'])
|
||||
|
||||
@classmethod
|
||||
def rebuild_tree_pathstring_values(cls, tree_ids) -> None:
|
||||
"""Rebuild the 'pathstring' values for all nodes in the specified trees.
|
||||
|
||||
Each tree is processed in a single pass:
|
||||
the pathstring for each node is constructed from its parent node,
|
||||
and any changed values are written back in a single bulk update.
|
||||
"""
|
||||
tree_nodes = list(cls.objects.filter(tree_id__in=tree_ids))
|
||||
node_map = {node.pk: node for node in tree_nodes}
|
||||
|
||||
# Cache of node ID -> list of path elements (from the top level down)
|
||||
path_cache: dict[int, list[str]] = {}
|
||||
|
||||
def path_names(node) -> list[str]:
|
||||
"""Construct the path (list of names) for a node, via its parent chain."""
|
||||
if node.pk in path_cache:
|
||||
return path_cache[node.pk]
|
||||
|
||||
names = [str(getattr(node, cls.PATH_FIELD, node.pk))]
|
||||
|
||||
if node.parent_id:
|
||||
parent = node_map.get(node.parent_id)
|
||||
|
||||
if parent is None:
|
||||
# Parent node exists outside the selected trees
|
||||
parent = cls.objects.get(pk=node.parent_id)
|
||||
node_map[node.parent_id] = parent
|
||||
|
||||
names = [*path_names(parent), *names]
|
||||
|
||||
path_cache[node.pk] = names
|
||||
return names
|
||||
|
||||
nodes_to_update = []
|
||||
|
||||
for node in tree_nodes:
|
||||
pathstring = InvenTree.helpers.constructPathString(path_names(node))
|
||||
|
||||
if pathstring != node.pathstring:
|
||||
node.pathstring = pathstring
|
||||
nodes_to_update.append(node)
|
||||
|
||||
if len(nodes_to_update) > 0:
|
||||
cls.objects.bulk_update(nodes_to_update, ['pathstring'], batch_size=250)
|
||||
|
||||
def construct_pathstring(self, refresh: bool = False) -> str:
|
||||
"""Construct the pathstring for this tree node.
|
||||
|
||||
|
|
@ -1566,15 +1465,11 @@ def after_failed_task(sender, instance: Task, created: bool, **kwargs):
|
|||
# Create a new Error object associated with this failed task
|
||||
# This will, in turn, trigger a notification to staff users via the Error post_save signal
|
||||
|
||||
message = f"Task '{instance.func} ({instance.pk})' failed after {n} attempts"
|
||||
|
||||
logger.error(message)
|
||||
|
||||
log_error(
|
||||
'task_failure',
|
||||
scope='worker',
|
||||
error_name='Task Failure',
|
||||
error_info=message,
|
||||
error_info=f"Task '{instance.pk}' failed after {n} attempts",
|
||||
error_data=str(instance.result) if instance.result else '',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from drf_spectacular.utils import (
|
|||
extend_schema,
|
||||
extend_schema_view,
|
||||
)
|
||||
from rest_framework import serializers
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from InvenTree.permissions import OASTokenMixin
|
||||
|
|
@ -47,20 +46,6 @@ class ExtendedOAuth2Scheme(DjangoOAuthToolkitScheme):
|
|||
class ExtendedAutoSchema(AutoSchema):
|
||||
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
|
||||
|
||||
def _map_serializer_field(self, field, direction, *args, **kwargs):
|
||||
"""Custom field mapping overrides, falling back to default behavior."""
|
||||
schema = super()._map_serializer_field(field, direction, *args, **kwargs)
|
||||
|
||||
direction_value = getattr(direction, 'value', direction)
|
||||
|
||||
# File and image fields in request schemas must be represented as binary
|
||||
# payloads. In response schemas they are still rendered as URLs.
|
||||
if direction_value == 'request' and isinstance(field, serializers.FileField):
|
||||
schema['type'] = 'string'
|
||||
schema['format'] = 'binary'
|
||||
|
||||
return schema
|
||||
|
||||
def is_bulk_action(self, ref: str) -> bool:
|
||||
"""Check the class of the current view for the bulk mixins."""
|
||||
return ref in [c.__name__ for c in type(self.view).__mro__]
|
||||
|
|
@ -223,18 +208,12 @@ def postprocess_schema_enums(result, generator, **kwargs):
|
|||
"""Custom patch to ignore some drf-spectacular warnings.
|
||||
|
||||
- Some warnings are unavoidable due to the way that InvenTree implements generic relationships (via ContentType).
|
||||
- Some warnings are unavoidable due to the way that InvenTree implements custom (database-editable) status codes:
|
||||
multiple serializers legitimately expose a 'status' field backed by the same dynamic StockStatus choice set
|
||||
(e.g. stock adjustment, receiving a purchase order line, disassembling a stock item), and drf-spectacular
|
||||
cannot settle on a single stable name for the shared, runtime-dependent choice set.
|
||||
- The cleanest way to handle this appears to be to override the 'warn' function from drf-spectacular.
|
||||
|
||||
Ref: https://github.com/inventree/InvenTree/pull/10699
|
||||
"""
|
||||
ignore_patterns = [
|
||||
'enum naming encountered a non-optimally resolvable collision for fields named "model_type"',
|
||||
'enum naming encountered a non-optimally resolvable collision for fields named "status"',
|
||||
'encountered multiple names for the same choice set (StatusCustomKeyEnum)',
|
||||
'enum naming encountered a non-optimally resolvable collision for fields named "model_type"'
|
||||
]
|
||||
|
||||
if any(pattern in msg for pattern in ignore_patterns):
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||
|
||||
Default implementation returns an empty list
|
||||
"""
|
||||
return getattr(self, 'SKIP_CREATE_FIELDS', [])
|
||||
return []
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Catch any django ValidationError thrown at the moment `save` is called, and re-throw as a DRF ValidationError."""
|
||||
|
|
@ -822,13 +822,9 @@ class NotesFieldMixin:
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
if hasattr(self, 'context'):
|
||||
request = self.context.get('request', None)
|
||||
method = getattr(request, 'method', None)
|
||||
|
||||
if view := self.context.get('view', None):
|
||||
if (
|
||||
issubclass(view.__class__, ListModelMixin)
|
||||
and method in SAFE_METHODS
|
||||
and not InvenTree.ready.isGeneratingSchema()
|
||||
):
|
||||
self.fields.pop('notes', None)
|
||||
|
|
@ -925,104 +921,3 @@ class ContentTypeField(serializers.ChoiceField):
|
|||
)
|
||||
|
||||
return content_type
|
||||
|
||||
|
||||
class DuplicateOptionsSerializer(serializers.Serializer):
|
||||
"""Generic serializer for specifying copy options when duplicating a model instance.
|
||||
|
||||
Builds its fields dynamically at instantiation time so the same class can be
|
||||
reused for any model without subclassing.
|
||||
"""
|
||||
|
||||
# Special 'shortcut' fields which are used for multiple models
|
||||
DEFAULT_FIELDS = [
|
||||
(
|
||||
'copy_parameters',
|
||||
_('Copy Parameters'),
|
||||
_('Copy parameters from the original item'),
|
||||
False,
|
||||
),
|
||||
(
|
||||
'copy_lines',
|
||||
_('Copy Lines'),
|
||||
_('Copy line items from the original order'),
|
||||
False,
|
||||
),
|
||||
(
|
||||
'copy_extra_lines',
|
||||
_('Copy Extra Lines'),
|
||||
_('Copy extra line items from the original order'),
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queryset: QuerySet,
|
||||
*args,
|
||||
copy_fields: Optional[list[dict]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialise the serializer and dynamically attach fields.
|
||||
|
||||
Arguments:
|
||||
queryset: Queryset used for the `original` PrimaryKeyRelatedField.
|
||||
copy_fields: Optional list of dicts, each describing one boolean copy toggle.
|
||||
Keys:
|
||||
name: (str, required)
|
||||
label: (str, optional)
|
||||
help_text: (str, optional)
|
||||
default: (bool, optional, default True)
|
||||
"""
|
||||
# Enforce certain properties onto this serializer
|
||||
kwargs['label'] = kwargs.get('label', _('Duplication Options'))
|
||||
kwargs['help_text'] = kwargs.get(
|
||||
'help_text', _('Specify options for duplicating this item')
|
||||
)
|
||||
kwargs['required'] = False
|
||||
kwargs['write_only'] = True
|
||||
|
||||
copy_fields = copy_fields or []
|
||||
copy_field_names = [spec['name'] for spec in copy_fields]
|
||||
|
||||
# Apply "default" fields
|
||||
for name, label, help_text, default_value in self.DEFAULT_FIELDS:
|
||||
popped_value = kwargs.pop(name, default_value)
|
||||
|
||||
if name in copy_field_names:
|
||||
# Manually supplied field, continue
|
||||
continue
|
||||
|
||||
if popped_value:
|
||||
copy_fields.append({
|
||||
'name': name,
|
||||
'label': label,
|
||||
'help_text': help_text,
|
||||
'default': True,
|
||||
})
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Re-class the instance with a model-specific subclass,
|
||||
# so that each model generates a unique schema component name
|
||||
if self.__class__ is DuplicateOptionsSerializer:
|
||||
self.__class__ = type(
|
||||
f'{queryset.model.__name__}DuplicateOptionsSerializer',
|
||||
(DuplicateOptionsSerializer,),
|
||||
{},
|
||||
)
|
||||
|
||||
self.fields['original'] = serializers.PrimaryKeyRelatedField(
|
||||
queryset=queryset,
|
||||
required=True,
|
||||
label=_('Original'),
|
||||
help_text=_('Select instance to duplicate'),
|
||||
)
|
||||
|
||||
for spec in copy_fields or []:
|
||||
self.fields[spec['name']] = serializers.BooleanField(
|
||||
required=False,
|
||||
default=spec.get('default', True),
|
||||
label=spec.get('label', spec['name']),
|
||||
help_text=spec.get('help_text', ''),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
"""Functions for tasks and a few general async tasks."""
|
||||
|
||||
import contextvars
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
|
@ -16,7 +13,7 @@ from django.conf import settings
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import AppRegistryNotReady, ValidationError
|
||||
from django.core.management import call_command
|
||||
from django.db import DEFAULT_DB_ALIAS, connections, transaction
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db.utils import NotSupportedError, OperationalError, ProgrammingError
|
||||
from django.utils import timezone
|
||||
|
|
@ -204,86 +201,6 @@ def check_existing_task(taskname, group: str, *args, **kwargs) -> Optional[str]:
|
|||
return task_id
|
||||
|
||||
|
||||
# Context-local batch of pending offload_task() calls (see batch_offload_tasks())
|
||||
_task_batch: contextvars.ContextVar = contextvars.ContextVar('task_batch', default=None)
|
||||
|
||||
|
||||
class TaskBatch:
|
||||
"""Collects offload_task() calls made within a batch_offload_tasks() scope.
|
||||
|
||||
Entries are grouped by (taskname, group, force_async), so that each distinct
|
||||
combination triggered within the batch is flushed via its own bulk_offload_task() call.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty batch."""
|
||||
self.entries: dict[tuple, list] = defaultdict(list)
|
||||
|
||||
def add(
|
||||
self, taskname, group: str, force_async: bool, args: tuple, kwargs: dict
|
||||
) -> None:
|
||||
"""Record a single offload_task() call against this batch."""
|
||||
self.entries[taskname, group, force_async].append((args, kwargs))
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Fire a bulk_offload_task() call for each (taskname, group, force_async) group collected so far."""
|
||||
entries, self.entries = self.entries, defaultdict(list)
|
||||
|
||||
for (taskname, group, force_async), task_entries in entries.items():
|
||||
bulk_offload_task(
|
||||
taskname, task_entries, group=group, force_async=force_async
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def batch_offload_tasks():
|
||||
"""Batch offload_task() calls made within this scope into bulk_offload_task() calls.
|
||||
|
||||
Any offload_task() call made (directly, or indirectly via a nested function call) while
|
||||
this context is active is queued instead of immediately offloaded - *except* for calls
|
||||
which pass force_sync=True, which always run immediately and synchronously as before.
|
||||
Excluding these is necessary because a forced-sync call is relied upon to have completed,
|
||||
with its side effects visible, by the time offload_task() returns control to the caller -
|
||||
deferring it would silently break that contract.
|
||||
|
||||
The queued calls are flushed - grouped by (taskname, group, force_async), one
|
||||
bulk_offload_task() call per group - when the current database transaction commits (or
|
||||
immediately, if no transaction is active). If the transaction is instead rolled back, the
|
||||
queued calls are discarded, rather than being fired for a write that never happened.
|
||||
|
||||
Note: bulk_offload_task() does not perform duplicate-task checking, unlike offload_task()'s
|
||||
default (check_duplicates=True) behavior - queued calls are never deduplicated, regardless
|
||||
of the check_duplicates value passed to offload_task().
|
||||
|
||||
A batched offload_task() call always returns True immediately, rather than a task ID -
|
||||
the actual task ID is not known until the batch is flushed, possibly well after the
|
||||
call returns. Callers which depend on the returned task ID should not use this context.
|
||||
|
||||
Nesting is not supported: a nested batch_offload_tasks() call reuses the outer batch,
|
||||
and only the outermost call schedules a flush.
|
||||
|
||||
This mirrors plugin.base.event.events.batch_events() and stock.models.batch_tracking_entries()
|
||||
- see batch_events()'s docstring for the reasoning behind the on-commit flush and the
|
||||
context-local (rather than parameter-based) design.
|
||||
|
||||
Yields:
|
||||
The current TaskBatch instance
|
||||
"""
|
||||
if _task_batch.get() is not None:
|
||||
# Already inside a batch - extend it, rather than creating a nested one
|
||||
yield _task_batch.get()
|
||||
return
|
||||
|
||||
batch = TaskBatch()
|
||||
token = _task_batch.set(batch)
|
||||
|
||||
try:
|
||||
yield batch
|
||||
finally:
|
||||
_task_batch.reset(token)
|
||||
transaction.on_commit(batch.flush)
|
||||
|
||||
|
||||
def offload_task(
|
||||
taskname,
|
||||
*args,
|
||||
|
|
@ -307,18 +224,11 @@ def offload_task(
|
|||
Returns:
|
||||
str | bool: Task ID if the task was offloaded, True if ran synchronously, False otherwise
|
||||
"""
|
||||
from InvenTree.exceptions import log_error
|
||||
|
||||
# Extract group information from kwargs
|
||||
group = kwargs.pop('group', 'inventree')
|
||||
|
||||
if not force_sync and (batch := _task_batch.get()) is not None:
|
||||
# A batch_offload_tasks() context is active - queue this task rather than
|
||||
# offloading it immediately (force_sync=True calls never reach this branch -
|
||||
# see batch_offload_tasks() for why they are excluded from batching)
|
||||
batch.add(taskname, group, force_async, args, kwargs)
|
||||
return True
|
||||
|
||||
from InvenTree.exceptions import log_error
|
||||
|
||||
try:
|
||||
import importlib
|
||||
|
||||
|
|
@ -413,96 +323,6 @@ def offload_task(
|
|||
return True
|
||||
|
||||
|
||||
def bulk_offload_task(
|
||||
taskname,
|
||||
entries: list,
|
||||
group: str = 'inventree',
|
||||
force_sync: bool = False,
|
||||
force_async: bool = False,
|
||||
) -> bool:
|
||||
"""Queue the same background task many times, in a single bulk database write.
|
||||
|
||||
Equivalent to calling offload_task() once per (args, kwargs) pair in 'entries', but
|
||||
writes all of the queued tasks to the django-q2 ORM broker table (OrmQ) in a single
|
||||
bulk_create() call, rather than one INSERT per task.
|
||||
|
||||
Note: InvenTree always configures django-q2 to use the ORM broker (see
|
||||
InvenTree.setting.worker.get_worker_config), so this does not need to handle any
|
||||
other broker backend.
|
||||
|
||||
Arguments:
|
||||
taskname: The name of the task to be run, in the format 'app.module.function'
|
||||
entries: List of (args, kwargs) tuples, one per task instance to queue
|
||||
group: The task group to assign to each queued task
|
||||
force_sync: If True, run all tasks synchronously (even if workers are running)
|
||||
force_async: If True, force all tasks to be queued (even if workers are not running)
|
||||
|
||||
Returns:
|
||||
bool: True if the tasks were queued (or run synchronously), False otherwise
|
||||
"""
|
||||
if not entries:
|
||||
return False
|
||||
|
||||
try:
|
||||
from django_q.brokers import get_broker
|
||||
from django_q.humanhash import uuid
|
||||
from django_q.models import OrmQ
|
||||
from django_q.signing import SignedPackage
|
||||
|
||||
from InvenTree.status import is_worker_running
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
logger.warning(
|
||||
"Could not offload bulk task '%s' - app registry not ready", taskname
|
||||
)
|
||||
force_sync = True
|
||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||
raise_warning(f"Could not offload bulk task '{taskname}' - database not ready")
|
||||
force_sync = True
|
||||
|
||||
if not force_async and (force_sync or not is_worker_running()):
|
||||
# Workers are not available - fall back to running each task synchronously
|
||||
for args, kwargs in entries:
|
||||
offload_task(
|
||||
taskname,
|
||||
*args,
|
||||
group=group,
|
||||
force_sync=True,
|
||||
check_duplicates=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
broker = get_broker()
|
||||
|
||||
tasks = []
|
||||
|
||||
for args, kwargs in entries:
|
||||
name, task_id = uuid()
|
||||
|
||||
task = {
|
||||
'id': task_id,
|
||||
'name': name,
|
||||
'func': taskname,
|
||||
'args': args,
|
||||
'kwargs': kwargs,
|
||||
'group': group,
|
||||
'started': timezone.now(),
|
||||
}
|
||||
|
||||
tasks.append(
|
||||
OrmQ(
|
||||
key=broker.list_key or 'inventree',
|
||||
payload=SignedPackage.dumps(task),
|
||||
lock=timezone.now(),
|
||||
)
|
||||
)
|
||||
|
||||
OrmQ.objects.bulk_create(tasks)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_queued_task(task_id: str):
|
||||
"""Find the task in the queue, if it exists.
|
||||
|
||||
|
|
@ -617,40 +437,6 @@ def scheduled_task(
|
|||
return _task_wrapper
|
||||
|
||||
|
||||
@tracer.start_as_current_span('rebuild_model_tree')
|
||||
def rebuild_model_tree(model: str, tree_id: int) -> None:
|
||||
"""Rebuild the tree structure (and pathstring values) for a tree model.
|
||||
|
||||
This task is offloaded to the background worker whenever nodes are
|
||||
restructured (e.g. re-parented), to avoid expensive tree rebuild
|
||||
operations blocking the calling thread.
|
||||
|
||||
Arguments:
|
||||
model: Label of the model class to rebuild, e.g. 'stock.stocklocation'
|
||||
tree_id: ID of the tree to rebuild
|
||||
"""
|
||||
from django.apps import apps
|
||||
|
||||
import InvenTree.models
|
||||
|
||||
try:
|
||||
model_class = apps.get_model(model)
|
||||
except (LookupError, ValueError):
|
||||
logger.warning("rebuild_model_tree: Model '%s' does not exist", model)
|
||||
return
|
||||
|
||||
if not issubclass(model_class, InvenTree.models.InvenTreeTree):
|
||||
logger.warning("rebuild_model_tree: Model '%s' is not a tree model", model)
|
||||
return
|
||||
|
||||
# Rebuild the tree structure, based on the parent-child relationships
|
||||
model_class.rebuild_trees([tree_id])
|
||||
|
||||
# Rebuild the 'pathstring' values for the entire tree (if applicable)
|
||||
if issubclass(model_class, InvenTree.models.PathStringMixin):
|
||||
model_class.rebuild_tree_pathstring_values([tree_id])
|
||||
|
||||
|
||||
@tracer.start_as_current_span('heartbeat')
|
||||
@scheduled_task(ScheduledTask.MINUTES, 1)
|
||||
def heartbeat():
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for custom InvenTree management commands."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -17,48 +15,6 @@ from InvenTree.config import get_testfolder_dir
|
|||
class CommandTestCase(TestCase):
|
||||
"""Test case for custom management commands."""
|
||||
|
||||
def test_makemigrations_currency_overrides_no_changes(self):
|
||||
"""Ensure currency list changes do not cause migration drift."""
|
||||
currency_sets = ['USD,EUR,GBP', 'JPY,CNY,KRW']
|
||||
|
||||
for currency_codes in currency_sets:
|
||||
with self.subTest(currency_codes=currency_codes):
|
||||
old_value = os.environ.get('INVENTREE_CURRENCY_CODES')
|
||||
|
||||
try:
|
||||
os.environ['INVENTREE_CURRENCY_CODES'] = currency_codes
|
||||
project_dir = Path(__file__).resolve().parents[1]
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
'python3',
|
||||
'manage.py',
|
||||
'makemigrations',
|
||||
'--check',
|
||||
'--dry-run',
|
||||
'--verbosity',
|
||||
'0',
|
||||
],
|
||||
cwd=project_dir,
|
||||
env=os.environ.copy(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
self.fail(
|
||||
'makemigrations reported schema changes '
|
||||
f'for INVENTREE_CURRENCY_CODES={currency_codes}\n'
|
||||
f'stdout:\n{result.stdout}\n'
|
||||
f'stderr:\n{result.stderr}'
|
||||
)
|
||||
finally:
|
||||
if old_value is None:
|
||||
os.environ.pop('INVENTREE_CURRENCY_CODES', None)
|
||||
else:
|
||||
os.environ['INVENTREE_CURRENCY_CODES'] = old_value
|
||||
|
||||
def test_schema(self):
|
||||
"""Test the schema generation command."""
|
||||
output = call_command('schema', file='schema.yml', verbosity=0)
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
"""Functional tests for the InvenTree.helpers_db module."""
|
||||
|
||||
from django.db import connection
|
||||
from django.test import TestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from company.models import Company, Contact
|
||||
from InvenTree.helpers_db import bulk_create_and_fetch
|
||||
from order.models import PurchaseOrder
|
||||
from part.models import Part
|
||||
from stock.models import StockItem
|
||||
|
||||
|
||||
class BulkCreateAndFetchTest(TestCase):
|
||||
"""Tests for the bulk_create_and_fetch helper function."""
|
||||
|
||||
def test_basic(self):
|
||||
"""Created items are returned with populated, unique primary keys."""
|
||||
company = Company.objects.create(name='ACME')
|
||||
|
||||
items = [Contact(company=company, name=f'Contact {idx}') for idx in range(10)]
|
||||
|
||||
result = bulk_create_and_fetch(Contact, items)
|
||||
|
||||
self.assertEqual(result.count(), 10)
|
||||
|
||||
pks = [c.pk for c in result]
|
||||
|
||||
# Every item has a valid, unique primary key
|
||||
self.assertTrue(all(pk is not None for pk in pks))
|
||||
self.assertEqual(len(pks), len(set(pks)))
|
||||
|
||||
# Returned items exactly match what is actually in the database
|
||||
db_pks = set(
|
||||
Contact.objects.filter(company=company).values_list('pk', flat=True)
|
||||
)
|
||||
self.assertEqual(set(pks), db_pks)
|
||||
|
||||
# Temporary bulk-create metadata should have been cleared again
|
||||
for contact in Contact.objects.filter(pk__in=pks):
|
||||
self.assertIsNone(contact.metadata)
|
||||
|
||||
def test_with_filters(self):
|
||||
"""Additional filters correctly scope the returned queryset."""
|
||||
company_a = Company.objects.create(name='Company A')
|
||||
company_b = Company.objects.create(name='Company B')
|
||||
|
||||
items_a = [Contact(company=company_a, name=f'A{i}') for i in range(3)]
|
||||
items_b = [Contact(company=company_b, name=f'B{i}') for i in range(4)]
|
||||
|
||||
result_a = bulk_create_and_fetch(
|
||||
Contact, items_a, filters={'company': company_a}
|
||||
)
|
||||
result_b = bulk_create_and_fetch(
|
||||
Contact, items_b, filters={'company': company_b}
|
||||
)
|
||||
|
||||
self.assertEqual(result_a.count(), 3)
|
||||
self.assertEqual(result_b.count(), 4)
|
||||
|
||||
self.assertTrue(all(c.company_id == company_a.pk for c in result_a))
|
||||
self.assertTrue(all(c.company_id == company_b.pk for c in result_b))
|
||||
|
||||
def test_ignores_pre_existing_rows(self):
|
||||
"""The 'search floor' should exclude pre-existing rows in the table."""
|
||||
company = Company.objects.create(name='ACME')
|
||||
|
||||
# Pre-existing item, *not* created via the helper
|
||||
Contact.objects.create(company=company, name='Existing')
|
||||
|
||||
items = [Contact(company=company, name=f'New {i}') for i in range(2)]
|
||||
result = bulk_create_and_fetch(Contact, items)
|
||||
|
||||
self.assertEqual(result.count(), 2)
|
||||
self.assertNotIn('Existing', [c.name for c in result])
|
||||
|
||||
def test_empty_list(self):
|
||||
"""Calling with an empty list of items should not raise, and return no items."""
|
||||
result = bulk_create_and_fetch(Contact, [])
|
||||
self.assertEqual(result.count(), 0)
|
||||
|
||||
def test_does_not_mutate_caller_filters(self):
|
||||
"""The caller-provided 'filters' dict should not be mutated as a side effect."""
|
||||
company = Company.objects.create(name='ACME')
|
||||
filters = {'company': company}
|
||||
|
||||
items = [Contact(company=company, name='Contact')]
|
||||
bulk_create_and_fetch(Contact, items, filters=filters)
|
||||
|
||||
def test_purchase_order(self):
|
||||
"""The helper works for the PurchaseOrder model."""
|
||||
supplier = Company.objects.create(name='Supplier Co', is_supplier=True)
|
||||
|
||||
references = [f'PO-{idx:04d}' for idx in range(10)]
|
||||
|
||||
items = [
|
||||
PurchaseOrder(supplier=supplier, reference=ref, description=f'Order {ref}')
|
||||
for ref in references
|
||||
]
|
||||
|
||||
result = bulk_create_and_fetch(PurchaseOrder, items)
|
||||
|
||||
self.assertEqual(result.count(), 10)
|
||||
|
||||
pks = [o.pk for o in result]
|
||||
self.assertTrue(all(pk is not None for pk in pks))
|
||||
self.assertEqual(len(pks), len(set(pks)))
|
||||
|
||||
self.assertEqual(
|
||||
set(result.values_list('reference', flat=True)), set(references)
|
||||
)
|
||||
|
||||
for order in PurchaseOrder.objects.filter(pk__in=pks):
|
||||
self.assertIsNone(order.metadata)
|
||||
|
||||
def test_stock_item(self):
|
||||
"""The helper works for the StockItem model."""
|
||||
part = Part.objects.create(name='Widget', description='A widget')
|
||||
|
||||
items = [StockItem(part=part, quantity=idx + 1) for idx in range(10)]
|
||||
|
||||
result = bulk_create_and_fetch(StockItem, items)
|
||||
|
||||
self.assertEqual(result.count(), 10)
|
||||
|
||||
pks = [si.pk for si in result]
|
||||
self.assertTrue(all(pk is not None for pk in pks))
|
||||
self.assertEqual(len(pks), len(set(pks)))
|
||||
|
||||
self.assertEqual(
|
||||
sorted(result.values_list('quantity', flat=True)),
|
||||
sorted(idx + 1 for idx in range(10)),
|
||||
)
|
||||
|
||||
for stock_item in StockItem.objects.filter(pk__in=pks):
|
||||
self.assertIsNone(stock_item.metadata)
|
||||
|
||||
def _create_and_count_queries(self, n: int) -> int:
|
||||
"""Bulk-create 'n' Contact items, and return the number of queries used."""
|
||||
company = Company.objects.create(name=f'Company {n}')
|
||||
|
||||
items = [Contact(company=company, name=f'Contact {i}') for i in range(n)]
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
result = bulk_create_and_fetch(Contact, items)
|
||||
|
||||
self.assertEqual(result.count(), n)
|
||||
|
||||
MAX_QUERIES = 25 if n > 100 else 10
|
||||
|
||||
self.assertLess(len(ctx.captured_queries), MAX_QUERIES)
|
||||
|
||||
return len(ctx.captured_queries)
|
||||
|
||||
def test_query_count_is_constant(self):
|
||||
"""The number of queries used should not depend on the number of created items."""
|
||||
small_query_count = self._create_and_count_queries(5)
|
||||
large_query_count = self._create_and_count_queries(100)
|
||||
|
||||
self.assertEqual(small_query_count, large_query_count)
|
||||
|
||||
# Perform a very large bulk-create - this will be batched
|
||||
self._create_and_count_queries(2000)
|
||||
|
|
@ -6,7 +6,6 @@ from datetime import timedelta
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.db import transaction
|
||||
from django.db.utils import NotSupportedError
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
|
@ -412,175 +411,3 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
|
|||
|
||||
# 20 more tasks should have been added
|
||||
self.assertEqual(OrmQ.objects.count(), 41)
|
||||
|
||||
def test_bulk_offload(self):
|
||||
"""Test the bulk_offload_task function."""
|
||||
# Start with a blank slate
|
||||
OrmQ.objects.all().delete()
|
||||
|
||||
entries = [
|
||||
((idx, idx + 1), {'animal': f'animal_{idx}', 'count': idx})
|
||||
for idx in range(10)
|
||||
]
|
||||
|
||||
# Queuing all 10 tasks should only take a single database write (bulk_create)
|
||||
with self.assertNumQueries(1):
|
||||
result = InvenTree.tasks.bulk_offload_task(
|
||||
'dummy_module.dummy_function', entries, force_async=True
|
||||
)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(OrmQ.objects.count(), 10)
|
||||
|
||||
# Read out the pending tasks, and check that the args / kwargs match
|
||||
queued_tasks = OrmQ.objects.all().order_by('id')
|
||||
|
||||
for task, (args, kwargs) in zip(queued_tasks, entries, strict=True):
|
||||
self.assertEqual(task.func(), 'dummy_module.dummy_function')
|
||||
self.assertEqual(task.group(), 'inventree')
|
||||
self.assertEqual(task.args(), args)
|
||||
self.assertEqual(task.kwargs(), kwargs)
|
||||
|
||||
|
||||
class TaskBatchTests(TestCase):
|
||||
"""Unit tests for the batch_offload_tasks() context manager."""
|
||||
|
||||
def setUp(self):
|
||||
"""Start each test with an empty task queue."""
|
||||
super().setUp()
|
||||
OrmQ.objects.all().delete()
|
||||
|
||||
def test_tasks_queued_and_flushed_on_commit(self):
|
||||
"""Tasks offloaded inside batch_offload_tasks() are queued and flushed as one bulk write on commit."""
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
for idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function',
|
||||
idx,
|
||||
force_async=True,
|
||||
animal=f'animal_{idx}',
|
||||
)
|
||||
|
||||
# Nothing should be queued yet - the batch only flushes on commit
|
||||
self.assertEqual(OrmQ.objects.count(), 0)
|
||||
|
||||
self.assertEqual(OrmQ.objects.count(), 10)
|
||||
|
||||
queued_tasks = OrmQ.objects.all().order_by('id')
|
||||
|
||||
for idx, task in enumerate(queued_tasks):
|
||||
self.assertEqual(task.func(), 'dummy_module.dummy_function')
|
||||
self.assertEqual(task.group(), 'inventree')
|
||||
self.assertEqual(task.args(), (idx,))
|
||||
self.assertEqual(task.kwargs(), {'animal': f'animal_{idx}'})
|
||||
|
||||
def test_tasks_grouped_by_name_and_group(self):
|
||||
"""Tasks with different (taskname, group) combinations are flushed as separate bulk writes."""
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
for idx in range(5):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.task_a', idx, force_async=True, group='alpha'
|
||||
)
|
||||
for idx in range(3):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.task_b', idx, force_async=True, group='beta'
|
||||
)
|
||||
|
||||
self.assertEqual(OrmQ.objects.count(), 8)
|
||||
self.assertEqual(
|
||||
sum(
|
||||
1
|
||||
for t in OrmQ.objects.all()
|
||||
if t.func() == 'dummy_module.task_a' and t.group() == 'alpha'
|
||||
),
|
||||
5,
|
||||
)
|
||||
self.assertEqual(
|
||||
sum(
|
||||
1
|
||||
for t in OrmQ.objects.all()
|
||||
if t.func() == 'dummy_module.task_b' and t.group() == 'beta'
|
||||
),
|
||||
3,
|
||||
)
|
||||
|
||||
def test_tasks_discarded_on_rollback(self):
|
||||
"""Tasks queued in a batch are discarded, not fired, if the transaction rolls back."""
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
try:
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function', force_async=True
|
||||
)
|
||||
raise ValueError('boom')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.assertEqual(OrmQ.objects.count(), 0)
|
||||
|
||||
def test_tasks_outside_batch_fire_immediately(self):
|
||||
"""Tasks offloaded outside any batch_offload_tasks() context are unaffected - fired immediately."""
|
||||
InvenTree.tasks.offload_task('dummy_module.dummy_function', force_async=True)
|
||||
|
||||
# No transaction commit or captureOnCommitCallbacks needed - it was never queued
|
||||
self.assertEqual(OrmQ.objects.count(), 1)
|
||||
|
||||
def test_nested_batch_share_one_flush(self):
|
||||
"""A nested batch_offload_tasks() call reuses the outer batch, rather than flushing twice."""
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function', 1, force_async=True
|
||||
)
|
||||
with InvenTree.tasks.batch_offload_tasks():
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function', 2, force_async=True
|
||||
)
|
||||
self.assertEqual(OrmQ.objects.count(), 0)
|
||||
|
||||
self.assertEqual(OrmQ.objects.count(), 2)
|
||||
|
||||
def test_force_sync_excluded_from_batch(self):
|
||||
"""force_sync=True calls bypass batch_offload_tasks() entirely and run immediately."""
|
||||
calls = []
|
||||
|
||||
def sync_target():
|
||||
calls.append('ran')
|
||||
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
InvenTree.tasks.offload_task(sync_target, force_sync=True)
|
||||
|
||||
# Ran immediately - not deferred to flush, and never touched the task queue
|
||||
self.assertEqual(calls, ['ran'])
|
||||
self.assertEqual(OrmQ.objects.count(), 0)
|
||||
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function', force_async=True
|
||||
)
|
||||
|
||||
# The (non-force_sync) async call is queued, not yet in OrmQ
|
||||
self.assertEqual(OrmQ.objects.count(), 0)
|
||||
|
||||
# After commit: only the batched async call produced an OrmQ entry
|
||||
self.assertEqual(OrmQ.objects.count(), 1)
|
||||
self.assertEqual(calls, ['ran'])
|
||||
|
||||
def test_batched_calls_skip_duplicate_check(self):
|
||||
"""Unlike individual offload_task() calls, batched calls are never deduplicated."""
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
with transaction.atomic(), InvenTree.tasks.batch_offload_tasks():
|
||||
for _ in range(3):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function_dup',
|
||||
1,
|
||||
2,
|
||||
animal='cat',
|
||||
force_async=True,
|
||||
)
|
||||
|
||||
# All 3 identical calls were queued, unlike the non-batched dedup behavior
|
||||
# exercised in InvenTreeTaskTests.test_duplicate_tasks
|
||||
self.assertEqual(OrmQ.objects.count(), 3)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||
from djmoney.contrib.exchange.models import Rate, convert_money
|
||||
from djmoney.money import Money
|
||||
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
|
||||
from rest_framework import serializers
|
||||
from sesame.utils import get_user
|
||||
from stdimage.models import StdImageFieldFile
|
||||
|
||||
|
|
@ -104,9 +103,10 @@ class TreeFixtureTest(TestCase):
|
|||
self.run_tree_test(Build)
|
||||
|
||||
def test_stock(self):
|
||||
"""Test MPTT tree structure for StockLocation model."""
|
||||
from stock.models import StockLocation
|
||||
"""Test MPTT tree structure for Stock model."""
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
self.run_tree_test(StockItem)
|
||||
self.run_tree_test(StockLocation)
|
||||
|
||||
|
||||
|
|
@ -1101,7 +1101,7 @@ class CurrencyTests(TestCase):
|
|||
update_successful = False
|
||||
|
||||
# Note: the update sometimes fails in CI, let's give it a few chances
|
||||
for idx in range(10):
|
||||
for _ in range(10):
|
||||
InvenTree.tasks.update_exchange_rates()
|
||||
|
||||
rates = Rate.objects.all()
|
||||
|
|
@ -1113,7 +1113,7 @@ class CurrencyTests(TestCase):
|
|||
else: # pragma: no cover
|
||||
print('Exchange rate update failed - retrying')
|
||||
print(f'Expected {currency_codes()}, got {[a.currency for a in rates]}')
|
||||
time.sleep(1 + idx)
|
||||
time.sleep(1)
|
||||
|
||||
self.assertTrue(update_successful)
|
||||
|
||||
|
|
@ -1817,35 +1817,6 @@ class SchemaPostprocessingTest(TestCase):
|
|||
# required key removed when empty
|
||||
self.assertNotIn('required', schemas_out.get('SalesOrderShipment'))
|
||||
|
||||
def test_file_field_request_schema_binary(self):
|
||||
"""Verify only request file fields are exposed as binary."""
|
||||
auto_schema = object.__new__(schema.ExtendedAutoSchema)
|
||||
|
||||
mapped_schemas = [
|
||||
{'type': 'string', 'format': 'uri', 'nullable': True},
|
||||
{'type': 'string', 'format': 'uri'},
|
||||
{'type': 'string', 'format': 'uri', 'nullable': True},
|
||||
]
|
||||
|
||||
with mock.patch(
|
||||
'drf_spectacular.openapi.AutoSchema._map_serializer_field',
|
||||
side_effect=mapped_schemas,
|
||||
):
|
||||
file_request = auto_schema._map_serializer_field(
|
||||
serializers.FileField(allow_null=True), 'request'
|
||||
)
|
||||
url_request = auto_schema._map_serializer_field(
|
||||
serializers.URLField(), 'request'
|
||||
)
|
||||
file_response = auto_schema._map_serializer_field(
|
||||
serializers.FileField(allow_null=True), 'response'
|
||||
)
|
||||
|
||||
self.assertEqual(file_request['format'], 'binary')
|
||||
self.assertTrue(file_request['nullable'])
|
||||
self.assertEqual(url_request['format'], 'uri')
|
||||
self.assertEqual(file_response['format'], 'uri')
|
||||
|
||||
|
||||
class URLCompatibilityTest(InvenTreeTestCase):
|
||||
"""Unit test for legacy URL compatibility."""
|
||||
|
|
|
|||
|
|
@ -380,23 +380,13 @@ class TestQueryMixin:
|
|||
):
|
||||
"""Context manager to check that the number of queries is less than a certain value.
|
||||
|
||||
Arguments:
|
||||
value: The maximum number of queries allowed
|
||||
using: The database connection to use (default = 'default')
|
||||
verbose: If True, print the queries to the console (default = False)
|
||||
url: Optional URL to print in the output (default = None)
|
||||
log_to_file: If True, log the queries to a file (default = False)
|
||||
|
||||
Yields:
|
||||
The CaptureQueriesContext object, which contains the captured queries
|
||||
|
||||
Example:
|
||||
with self.assertNumQueriesLessThan(10):
|
||||
# Do some stuff
|
||||
Ref: https://stackoverflow.com/questions/1254170/django-is-there-a-way-to-count-sql-queries-from-an-unit-test/59089020#59089020
|
||||
"""
|
||||
with CaptureQueriesContext(connections[using]) as context:
|
||||
yield context # your test will be run here
|
||||
yield # your test will be run here
|
||||
|
||||
n = len(context.captured_queries)
|
||||
|
||||
|
|
@ -502,16 +492,12 @@ class InvenTreeAPITestCase(
|
|||
|
||||
expected_code = kwargs.pop('expected_code', None)
|
||||
msg = kwargs.pop('msg', None)
|
||||
max_query_count = kwargs.pop('max_query_count', self.MAX_QUERY_COUNT)
|
||||
max_queries = kwargs.pop('max_query_count', self.MAX_QUERY_COUNT)
|
||||
max_query_time = kwargs.pop('max_query_time', self.MAX_QUERY_TIME)
|
||||
benchmark = kwargs.pop('benchmark', False)
|
||||
|
||||
t1 = time.time()
|
||||
|
||||
with (
|
||||
self.assertNumQueriesLessThan(max_query_count, url=url) as context,
|
||||
self.captureOnCommitCallbacks(execute=True),
|
||||
):
|
||||
with self.assertNumQueriesLessThan(max_queries, url=url):
|
||||
response = method(url, data, **kwargs)
|
||||
|
||||
t2 = time.time()
|
||||
|
|
@ -526,11 +512,6 @@ class InvenTreeAPITestCase(
|
|||
|
||||
self.assertLessEqual(dt, max_query_time)
|
||||
|
||||
if benchmark:
|
||||
print(
|
||||
f"Benchmark @ '{url}': {len(context.captured_queries)} queries (of {max_query_count}) in {dt:.4f}s (of {max_query_time}s max)"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def get(self, url, data=None, expected_code=200, **kwargs):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue