Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot] 9793bba77a
Fix stocktake bug for counting serialized items (#12280) (#12282)
* Fix stocktake bug for counting serialized items

* Add unit test

(cherry picked from commit 414aac0224)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-06-30 17:09:23 +10:00
github-actions[bot] ffc60cb189
[UI] Stock column fix (#12268) (#12269)
* [UI] Fix StockColumn component

* stock table rendering tweaks

(cherry picked from commit 1da71ca3b9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-06-27 12:25:10 +10:00
github-actions[bot] 2ac8f608d8
Bug fix for exception handler (#12257) (#12261)
(cherry picked from commit e847d96a88)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-06-26 09:55:52 +10:00
Oliver 9ea33df847
Bump version to 1.4.1 (#12242) 2026-06-24 15:47:31 +10:00
Oliver 0a9a8b1c54
Add release entry for 1.4.0 (#12237)
* Add release entry for 1.4.0

* Mark version as 1.4.0
2026-06-24 13:53:55 +10:00
379 changed files with 140088 additions and 149221 deletions

View File

@ -1,7 +1,9 @@
# Dockerfile for the InvenTree devcontainer # 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 # InvenTree paths
ENV INVENTREE_HOME="/home/inventree" ENV INVENTREE_HOME="/home/inventree"
@ -23,10 +25,10 @@ RUN chmod +x init.sh
# Install required base packages # Install required base packages
RUN apt update && apt install -y \ RUN apt update && apt install -y \
python3-dev python3-venv \ python3.11-dev python3.11-venv \
postgresql-client \ postgresql-client \
libldap2-dev libsasl2-dev \ libldap2-dev libsasl2-dev \
libpango-1.0-0 libcairo2 \ libpango1.0-0 libcairo2 \
poppler-utils weasyprint poppler-utils weasyprint
# Install packages required for frontend development # Install packages required for frontend development

79
.devops/test_stats.yml Normal file
View File

@ -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'

View File

@ -39,14 +39,14 @@ runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
with: with:
persist-credentials: false persist-credentials: false
# Python installs # Python installs
- name: Set up Python ${{ env.python_version }} - name: Set up Python ${{ env.python_version }}
if: ${{ inputs.python == 'true' && env.python_version != '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: with:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
cache: pip cache: pip
@ -57,7 +57,7 @@ runs:
contrib/dev_reqs/requirements.txt contrib/dev_reqs/requirements.txt
- name: Setup Python 3.14 - name: Setup Python 3.14
if: ${{ inputs.python == 'true' && env.python_version == '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: with:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
- name: Install Base Python Dependencies - name: Install Base Python Dependencies

View File

@ -25,7 +25,7 @@ jobs:
) )
steps: steps:
- name: Backport Action - name: Backport Action
uses: sorenlouv/backport-github-action@8a6c0381851f43f9f1fddc7303f0e9015eb57b62 # v12.0.4 uses: sqren/backport-github-action@ad888e978060bc1b2798690dd9d03c4036560947 # pin@v9.2.2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to- auto_backport_label_prefix: backport-to-

View File

@ -9,7 +9,7 @@ on:
- l10 - l10
env: env:
python_version: 3.12 python_version: 3.11
permissions: permissions:
contents: read contents: read
@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
@ -46,6 +46,4 @@ jobs:
run: | run: |
python ./.github/scripts/check_source_strings.py --frontend --backend python ./.github/scripts/check_source_strings.py --frontend --backend
- name: Check Migration Files - name: Check Migration Files
run: | run: python3 .github/scripts/check_migration_files.py
invoke migrate --detect
python3 .github/scripts/check_migration_files.py

View File

@ -39,10 +39,10 @@ jobs:
docker: ${{ steps.filter.outputs.docker }} docker: ${{ steps.filter.outputs.docker }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
id: filter id: filter
with: with:
filters: | filters: |
@ -62,12 +62,12 @@ jobs:
contents: read contents: read
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Test Docker Image - name: Test Docker Image
@ -134,12 +134,12 @@ jobs:
contents: read contents: read
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Run Migration Tests - name: Run Migration Tests
@ -158,16 +158,16 @@ jobs:
id-token: write id-token: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set Up Python ${{ env.python_version }} - 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: with:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
- name: Version Check - name: Version Check
@ -178,13 +178,13 @@ jobs:
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
if: github.event_name != 'pull_request' 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 - name: Set up Docker Buildx
if: github.event_name != 'pull_request' 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 - name: Set up cosign
if: github.event_name != 'pull_request' 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 - name: Check if Dockerhub login is required
id: docker_login id: docker_login
run: | run: |
@ -195,14 +195,14 @@ jobs:
fi fi
- name: Login to Dockerhub - name: Login to Dockerhub
if: github.event_name != 'pull_request' && steps.docker_login.outputs.skip_dockerhub_login != 'true' 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: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log into registry ghcr.io - name: Log into registry ghcr.io
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # pin@v4.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -211,16 +211,16 @@ jobs:
- name: Extract Docker metadata - name: Extract Docker metadata
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
id: meta id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # pin@v6.1.0
with: with:
images: | images: |
inventree/inventree inventree/inventree
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1 - uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1
- name: Push Docker Images - name: Push Docker Images
id: push-docker id: push-docker
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1 uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # pin@v1
with: with:
project: jczzbjkk68 project: jczzbjkk68
context: . context: .

View File

@ -8,14 +8,13 @@ name: Frontend
on: on:
push: push:
branches-ignore: ["l10*", "dependabot/**", "backport/**"] branches-ignore: ["l10*", "dependabot/*", "backport/*"]
pull_request: pull_request:
branches-ignore: ["l10*"] branches-ignore: ["l10*"]
env: env:
python_version: 3.12 python_version: 3.11
node_version: 24 node_version: 24
plugin_creator_version: 1.20.0
# The OS version must be set per job # The OS version must be set per job
server_start_sleep: 60 server_start_sleep: 60
@ -46,10 +45,10 @@ jobs:
force: ${{ steps.force.outputs.force }} force: ${{ steps.force.outputs.force }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
id: filter id: filter
with: with:
filters: | filters: |
@ -69,7 +68,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -86,7 +85,7 @@ jobs:
run: | run: |
cd src/backend/InvenTree/web/static cd src/backend/InvenTree/web/static
zip -r frontend-build.zip web/ web/.vite 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: with:
name: frontend-build name: frontend-build
path: src/backend/InvenTree/web/static/web path: src/backend/InvenTree/web/static/web
@ -123,7 +122,7 @@ jobs:
VITE_COVERAGE: false VITE_COVERAGE: false
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -141,7 +140,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: invoke int.frontend-compile --extract run: invoke int.frontend-compile --extract
- name: Cache Playwright browsers - name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
id: playwright-cache id: playwright-cache
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
@ -154,7 +153,7 @@ jobs:
run: cd src/frontend && npx playwright install-deps run: cd src/frontend && npx playwright install-deps
- name: Install Sample Plugin - name: Install Sample Plugin
run: | run: |
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }} pip install -U inventree-plugin-creator
create-inventree-plugin --default create-inventree-plugin --default
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
- name: Run Playwright tests - 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 cp ./tests/fixtures/playwright_custom_splash.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_splash.png
invoke static 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 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' }} if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with: with:
name: playwright-report-firefox-${{ matrix.shard }} name: playwright-report-firefox-${{ matrix.shard }}
@ -205,7 +204,7 @@ jobs:
VITE_COVERAGE: true VITE_COVERAGE: true
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -223,7 +222,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: invoke int.frontend-compile --extract run: invoke int.frontend-compile --extract
- name: Cache Playwright browsers - name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
id: playwright-cache id: playwright-cache
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
@ -236,7 +235,7 @@ jobs:
run: cd src/frontend && npx playwright install-deps run: cd src/frontend && npx playwright install-deps
- name: Install Sample Plugin - name: Install Sample Plugin
run: | run: |
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }} pip install -U inventree-plugin-creator
create-inventree-plugin --default create-inventree-plugin --default
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
- name: Playwright [${{ matrix.shard }} / 4] - name: Playwright [${{ matrix.shard }} / 4]
@ -245,7 +244,7 @@ jobs:
cd src/frontend cd src/frontend
npx nyc playwright test --project=chromium --shard=${{ matrix.shard }}/4 npx nyc playwright test --project=chromium --shard=${{ matrix.shard }}/4
- name: Playwright Report [${{ 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' }} if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with: with:
name: playwright-report-chromium-${{ matrix.shard }} name: playwright-report-chromium-${{ matrix.shard }}
@ -253,7 +252,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 7 retention-days: 7
- name: Upload Coverage Artifact [${{ matrix.shard }} / 4] - 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 id: coverage-upload
if: ${{ !cancelled() && steps.tests.outcome != 'failure' }} if: ${{ !cancelled() && steps.tests.outcome != 'failure' }}
with: with:
@ -273,7 +272,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
@ -285,7 +284,7 @@ jobs:
update: false update: false
- name: Download Coverage Artifacts - name: Download Coverage Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
with: with:
pattern: coverage-* pattern: coverage-*
path: all-coverage/ path: all-coverage/
@ -304,7 +303,7 @@ jobs:
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
if: ${{ !cancelled() && github.ref == 'refs/heads/master' }} 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: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
slug: inventree/InvenTree slug: inventree/InvenTree

View File

@ -7,7 +7,7 @@ name: Import / Export
on: on:
push: push:
branches-ignore: ["l10*", "dependabot/**", "backport/**"] branches-ignore: ["l10*", "dependabot/*", "backport/*"]
pull_request: pull_request:
branches-ignore: ["l10*"] branches-ignore: ["l10*"]
@ -15,7 +15,7 @@ permissions:
contents: read contents: read
env: env:
python_version: 3.12 python_version: 3.11
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -48,10 +48,10 @@ jobs:
outputs: outputs:
server: ${{ steps.filter.outputs.server }} server: ${{ steps.filter.outputs.server }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
id: filter id: filter
with: with:
filters: | filters: |
@ -76,7 +76,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false

View File

@ -4,12 +4,12 @@ name: QC
on: on:
push: push:
branches-ignore: ["l10*", "dependabot/**", "backport/**"] branches-ignore: ["l10*", "dependabot/*", "backport/*"]
pull_request: pull_request:
branches-ignore: ["l10*"] branches-ignore: ["l10*"]
env: env:
python_version: 3.12 python_version: 3.11
node_version: 24 node_version: 24
# The OS version must be set per job # The OS version must be set per job
server_start_sleep: 60 server_start_sleep: 60
@ -44,10 +44,10 @@ jobs:
submit-performance: ${{ steps.runner-perf.outputs.submit-performance }} submit-performance: ${{ steps.runner-perf.outputs.submit-performance }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
id: filter id: filter
with: with:
filters: | 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' 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: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ env.python_version }} - 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: with:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
cache: "pip" cache: "pip"
- name: Run pre commit hook Checks - name: Run pre commit hook Checks
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5 uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # pin@v2
- name: Check Version - name: Check Version
run: | run: |
pip install --require-hashes -r contrib/dev_reqs/requirements.txt 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' if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -152,11 +152,11 @@ jobs:
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ env.python_version }} - 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: with:
python-version: ${{ env.python_version }} python-version: ${{ env.python_version }}
- name: Check Config - name: Check Config
@ -165,7 +165,7 @@ jobs:
pip install --require-hashes -r docs/requirements.txt pip install --require-hashes -r docs/requirements.txt
python docs/ci/check_mkdocs_config.py python docs/ci/check_mkdocs_config.py
- name: Check Links - name: Check Links
uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1 uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # pin@v1
with: with:
folder-path: docs folder-path: docs
config-file: docs/mlc_config.json config-file: docs/mlc_config.json
@ -190,7 +190,7 @@ jobs:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -202,7 +202,7 @@ jobs:
- name: Export API Documentation - name: Export API Documentation
run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
- name: Upload schema - name: Upload schema
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: schema.yml name: schema.yml
path: src/backend/InvenTree/schema.yml path: src/backend/InvenTree/schema.yml
@ -222,7 +222,7 @@ jobs:
echo "Downloaded api.yaml" echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action - name: Running OpenAPI Spec diff action
id: breaking_changes id: breaking_changes
uses: oasdiff/oasdiff-action/diff@3e9d440d37f468355457604348009f50e0cddbf3 # v0.1.4 uses: oasdiff/oasdiff-action/diff@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
with: with:
base: "api.yaml" base: "api.yaml"
revision: "src/backend/InvenTree/schema.yml" revision: "src/backend/InvenTree/schema.yml"
@ -251,17 +251,17 @@ jobs:
- name: Extract settings / tags - name: Extract settings / tags
run: invoke int.export-definitions --basedir docs run: invoke int.export-definitions --basedir docs
- name: Upload settings - name: Upload settings
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: inventree_settings.json name: inventree_settings.json
path: docs/generated/inventree_settings.json path: docs/generated/inventree_settings.json
- name: Upload tags - name: Upload tags
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: inventree_tags.yml name: inventree_tags.yml
path: docs/generated/inventree_tags.yml path: docs/generated/inventree_tags.yml
- name: Upload filters - name: Upload filters
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: inventree_filters.yml name: inventree_filters.yml
path: docs/generated/inventree_filters.yml path: docs/generated/inventree_filters.yml
@ -275,7 +275,7 @@ jobs:
version: ${{ needs.schema.outputs.version }} version: ${{ needs.schema.outputs.version }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
name: Checkout Code name: Checkout Code
with: with:
repository: inventree/schema repository: inventree/schema
@ -284,7 +284,7 @@ jobs:
- name: Create artifact directory - name: Create artifact directory
run: mkdir -p artifact run: mkdir -p artifact
- name: Download schema artifact - name: Download schema artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
with: with:
path: artifact path: artifact
merge-multiple: true merge-multiple: true
@ -301,7 +301,7 @@ jobs:
echo "after move" echo "after move"
ls -la artifact ls -la artifact
rm -rf 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 name: Commit schema changes
with: with:
commit_message: "Update API schema for ${{ env.version }} / ${{ github.sha }}" commit_message: "Update API schema for ${{ env.version }} / ${{ github.sha }}"
@ -332,7 +332,7 @@ jobs:
node_version: '>=24' node_version: '>=24'
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -363,7 +363,7 @@ jobs:
pip install . pip install .
if: needs.paths-filter.outputs.submit-performance == 'true' if: needs.paths-filter.outputs.submit-performance == 'true'
- name: Performance Reporting - 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 # 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' if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
with: with:
@ -379,7 +379,7 @@ jobs:
continue-on-error: true # continue if a step fails so that coverage gets pushed continue-on-error: true # continue if a step fails so that coverage gets pushed
strategy: strategy:
matrix: matrix:
python_version: [3.12, 3.14] python_version: [3.11, 3.14]
env: env:
INVENTREE_DB_NAME: ./inventree.sqlite INVENTREE_DB_NAME: ./inventree.sqlite
@ -390,7 +390,7 @@ jobs:
python_version: ${{ matrix.python_version }} python_version: ${{ matrix.python_version }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -405,19 +405,17 @@ jobs:
- name: Test Translations - name: Test Translations
run: invoke dev.translate run: invoke dev.translate
- name: Check Migration Files - name: Check Migration Files
run: | run: python3 .github/scripts/check_migration_files.py
invoke migrate --detect
python3 .github/scripts/check_migration_files.py
- name: Coverage Tests - name: Coverage Tests
run: invoke dev.test --check --coverage --translations run: invoke dev.test --check --coverage --translations
- name: Upload raw coverage to artifacts - name: Upload raw coverage to artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: coverage name: coverage
path: .coverage path: .coverage
retention-days: 14 retention-days: 14
- name: Upload coverage reports to Codecov - 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() if: always()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -443,7 +441,7 @@ jobs:
INVENTREE_AUTO_UPDATE: true INVENTREE_AUTO_UPDATE: true
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -456,7 +454,7 @@ jobs:
env: env:
node_version: '>=24' node_version: '>=24'
- name: Performance Reporting - name: Performance Reporting
uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1 uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
with: with:
mode: walltime mode: walltime
run: inv dev.test --pytest run: inv dev.test --pytest
@ -494,7 +492,7 @@ jobs:
- 6379:6379 - 6379:6379
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -543,7 +541,7 @@ jobs:
- 3306:3306 - 3306:3306
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -586,7 +584,7 @@ jobs:
- 5432:5432 - 5432:5432
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -599,7 +597,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: invoke dev.test --check --migrations --report --coverage --translations run: invoke dev.test --check --migrations --report --coverage --translations
- name: Upload coverage reports to Codecov - 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() if: always()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -620,7 +618,7 @@ jobs:
INVENTREE_PLUGINS_ENABLED: false INVENTREE_PLUGINS_ENABLED: false
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
name: Checkout Code name: Checkout Code
@ -676,8 +674,8 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor 🌈 - name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6

View File

@ -7,7 +7,7 @@ on:
permissions: permissions:
contents: read contents: read
env: env:
python_version: 3.12 python_version: 3.11
jobs: jobs:
stable: stable:
@ -20,7 +20,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Version Check - name: Version Check
@ -28,7 +28,7 @@ jobs:
pip install --require-hashes -r contrib/dev_reqs/requirements.txt pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py python3 .github/scripts/version_check.py
- name: Push to Stable Branch - 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' if: env.stable_release == 'true'
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
@ -45,7 +45,7 @@ jobs:
artifact-metadata: write artifact-metadata: write
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -57,7 +57,7 @@ jobs:
- name: Build frontend - name: Build frontend
run: cd src/frontend && npm run compile && npm run build run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend - name: Create SBOM for frontend
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # pin@v0
with: with:
artifact-name: frontend-build.spdx artifact-name: frontend-build.spdx
path: src/frontend path: src/frontend
@ -75,7 +75,7 @@ jobs:
zip -r ../frontend-build.zip * .vite zip -r ../frontend-build.zip * .vite
- name: Attest Build Provenance - name: Attest Build Provenance
id: attest id: attest
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # pin@v4
with: with:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip" subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
@ -85,7 +85,7 @@ jobs:
REF: ${{ github.ref_name }} REF: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload frontend to artifacts - name: Upload frontend to artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1
with: with:
name: frontend-build name: frontend-build
path: src/backend/InvenTree/web/static/frontend-build.zip path: src/backend/InvenTree/web/static/frontend-build.zip
@ -115,7 +115,7 @@ jobs:
INVENTREE_DEBUG: true INVENTREE_DEBUG: true
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -151,17 +151,17 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
target: target:
- ubuntu:22.04
- ubuntu:24.04 - ubuntu:24.04
- ubuntu:26.04 - debian:12
- debian:13
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Get frontend artifact - name: Get frontend artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
with: with:
name: frontend-build name: frontend-build
- name: Setup - name: Setup
@ -216,7 +216,7 @@ jobs:
pip install --require-hashes -r contrib/dev_reqs/requirements.txt pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py python3 .github/scripts/version_check.py
- name: Package - current release channel - name: Package - current release channel
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
id: package id: package
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
@ -234,7 +234,7 @@ jobs:
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
APP_REPO=inventree/InvenTree APP_REPO=inventree/InvenTree
- name: Publish to go.packager.io - current release channel - name: Publish to go.packager.io - current release channel
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }} token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
@ -249,7 +249,7 @@ jobs:
PACKAGE_NAME: ${{ matrix.target }}-${{ steps.setup.outputs.version }}.tar.gz PACKAGE_NAME: ${{ matrix.target }}-${{ steps.setup.outputs.version }}.tar.gz
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package - stable release channel - name: Package - stable release channel
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
id: package-stable id: package-stable
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
@ -267,7 +267,7 @@ jobs:
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
APP_REPO=inventree/InvenTree APP_REPO=inventree/InvenTree
- name: Publish to go.packager.io - stable release channel - name: Publish to go.packager.io - stable release channel
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }} token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}

View File

@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false

View File

@ -16,7 +16,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # pin@v10.3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue seems stale. Please react to show this is still important." stale-issue-message: "This issue seems stale. Please react to show this is still important."

View File

@ -6,7 +6,7 @@ on:
- master - master
env: env:
python_version: 3.12 python_version: 3.11
node_version: 24 node_version: 24
permissions: permissions:
@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Environment Setup - name: Environment Setup
@ -56,7 +56,7 @@ jobs:
echo "Resetting to HEAD~" echo "Resetting to HEAD~"
git reset HEAD~ || true git reset HEAD~ || true
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2 uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # pin@v2
with: with:
upload_sources: true upload_sources: true
upload_translations: false upload_translations: false

View File

@ -9,7 +9,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup - name: Setup
@ -18,7 +18,7 @@ jobs:
run: pip-compile --output-file=requirements.txt requirements.in -U run: pip-compile --output-file=requirements.txt requirements.in -U
- name: Update requirements-dev.txt - name: Update requirements-dev.txt
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U 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: with:
commit_message: "[Bot] Updated dependency" commit_message: "[Bot] Updated dependency"
branch: dep-update branch: dep-update

View File

@ -21,9 +21,9 @@ before:
dependencies: dependencies:
- curl - curl
- poppler-utils - poppler-utils
- "python3.12 | python3.13 | python3.14" - "python3.11 | python3.12 | python3.13 | python3.14"
- "python3.12-venv | python3.13-venv | python3.14-venv" - "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
- "python3.12-dev | python3.13-dev | python3.14-dev" - "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
- python3-pip - python3-pip
- python3-cffi - python3-cffi
- python3-brotli - python3-brotli
@ -36,6 +36,6 @@ dependencies:
- jq - jq
- "libffi7 | libffi8" - "libffi7 | libffi8"
targets: targets:
ubuntu-22.04: true
ubuntu-24.04: true ubuntu-24.04: true
ubuntu-26.04: true debian-12: true
debian-13: true

View File

@ -97,4 +97,4 @@ repos:
rev: 0.4.3 rev: 0.4.3
hooks: hooks:
- id: teyit - id: teyit
language_version: python3.12 language_version: python3.11

2
.vscode/launch.json vendored
View File

@ -47,7 +47,7 @@
"name": "InvenTree invoke schema", "name": "InvenTree invoke schema",
"type": "debugpy", "type": "debugpy",
"request": "launch", "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}", "cwd": "${workspaceFolder}",
"args": [ "args": [
"dev.schema","--ignore-warnings" "dev.schema","--ignore-warnings"

View File

@ -5,25 +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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased - xxxx.xx.xx
### Breaking Changes
- [#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
- [#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 ## 1.4.0 - 2026-06-24
### Breaking Changes ### Breaking Changes

View File

@ -23,6 +23,7 @@ The InvenTree project is split into two main components: frontend and backend. T
``` ```
InvenTree/ InvenTree/
├─ .devops/ # Files for Azure DevOps
├─ .github/ # Files for GitHub ├─ .github/ # Files for GitHub
│ ├─ actions/ # Reused actions │ ├─ actions/ # Reused actions
│ ├─ ISSUE_TEMPLATE/ # Templates for issues and pull requests │ ├─ 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 # One-time setup: creates venv at dev/venv/, installs deps, sets up pre-commit hooks
invoke dev.setup-dev invoke dev.setup-dev
# Apply database migrations (and detect/create new migration files if required) # Apply database migrations
invoke migrate --detect invoke migrate
# Create an admin account (required to log in) # Create an admin account (required to log in)
invoke superuser invoke superuser

View File

@ -9,6 +9,7 @@
[![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg) ![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
[![Netlify Status](https://api.netlify.com/api/v1/badges/9bbb2101-0a4d-41e7-ad56-b63fb6053094/deploy-status)](https://app.netlify.com/sites/inventree/deploys) [![Netlify Status](https://api.netlify.com/api/v1/badges/9bbb2101-0a4d-41e7-ad56-b63fb6053094/deploy-status)](https://app.netlify.com/sites/inventree/deploys)
[![Performance Testing](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_apis/build/status%2Fmatmair.InvenTree?branchName=testing)](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_build/latest?definitionId=3&branchName=testing)
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7179/badge)](https://bestpractices.coreinfrastructure.org/projects/7179) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7179/badge)](https://bestpractices.coreinfrastructure.org/projects/7179)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/inventree/InvenTree/badge)](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/inventree/InvenTree/badge)](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://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://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://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> </p>

View File

@ -24,7 +24,7 @@ flag_management:
carryforward: true carryforward: true
statuses: statuses:
- type: project - type: project
target: 38% target: 40%
- name: web - name: web
carryforward: true carryforward: true
statuses: statuses:

View File

@ -10,7 +10,7 @@
# - Monitors source files for any changes, and live-reloads server # - Monitors source files for any changes, and live-reloads server
# Base image last bumped 2026-06-16 # 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 # Build arguments for this image
ARG commit_tag="" 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 rm -rf /root/.cache/pip
# Install frontend build dependencies # Install frontend build dependencies
# pinned to v0.40.5 RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash && \
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/1889911f0841e669de0be5bd02c737a3f1fd20fa/install.sh | bash && \
bash -c "export NVM_DIR="$HOME/.nvm" && source $HOME/.nvm/nvm.sh && \ 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" 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" RUN bash -c "source $HOME/.nvm/nvm.sh && cd '${INVENTREE_HOME}' && invoke int.frontend-compile --extract"

View File

@ -1,18 +1,18 @@
# Base python requirements for docker containers # Base python requirements for docker containers
# Basic package requirements # Basic package requirements
invoke # Invoke build tool invoke>=2.2.0 # Invoke build tool
pyyaml pyyaml>=6.0.1
setuptools setuptools>=69.0.0
wheel wheel>=0.41.0
# Database links # Database links
psycopg[binary, pool] psycopg[binary, pool]
mysqlclient mysqlclient>=2.2.0
mariadb mariadb>=1.1.8
# gunicorn web server # gunicorn web server
gunicorn gunicorn>=22.0.0
# LDAP required packages # LDAP required packages
django-auth-ldap # Django integration for ldap auth django-auth-ldap # Django integration for ldap auth

View File

@ -236,26 +236,26 @@ typing-extensions==4.15.0 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# psycopg-pool # psycopg-pool
uv==0.11.24 \ uv==0.11.23 \
--hash=sha256:047d763d20d71968c00f4afec40b0e75d9da7e3693f725b9f502d84a25256893 \ --hash=sha256:03fbb0a1c7b6d15e96778bdd79e8d1826c6259fea17fc13337fb0744136953f2 \
--hash=sha256:0e100b9cbc59beff2730840ac989b1f100cc03c90514e7cab6992a1f3f13784e \ --hash=sha256:3a900c8fd757f8c3da9dc5532d9a22d30540e91fdefd63c93909fedbfd756655 \
--hash=sha256:3176668ea8a2318d775c0d9188661c61ccc36790220724b1d360fcc8945d520e \ --hash=sha256:576d776a1ca62e3d8aba99f0d8ec607db91a5ebaf52feaff820f28ed820e1665 \
--hash=sha256:356435577fae11fafe7a067ee3269cccefd35b031b83a3a36c87fe9d192bffba \ --hash=sha256:61e6bd7e7f0fe24f103540ba19516443bea6e689022c787217310a1e64558e3f \
--hash=sha256:38f38c9449aafd71dc0fa35e66a8a547ee48947b2f2f94084893c2afe6058cfa \ --hash=sha256:6d7cea7d9ade3c1c3e3db1dfcc23d335bceaabf38f51e442b6f57f8f7885a9a6 \
--hash=sha256:438f8291fb9daea30a4bd333299b82e6ef755578302745a021162f328cfcfc68 \ --hash=sha256:7a85330de0a7eb0d5c6cf03c80edfb86facad19df367a0b52fc906db1ab15ce9 \
--hash=sha256:48a6123f71b801e0e0b8a38520b011632ad81e0a043445044ce5b1a7b1cec7b6 \ --hash=sha256:91d19c4249d7437b69b91c385134360d7ed9d0ee2e2e83e81d369867151e78c2 \
--hash=sha256:6ecdad43e870f88d3772d9d37e877259ae35ec374d51589805cdcf6196205829 \ --hash=sha256:985aa93c9d6223e32fc747e09662537c4073c9ebef59c0a4fd7c6949d1d24fb3 \
--hash=sha256:79757f90b7765996366b80e77cad13555c7f7e1282ca8d8b686ee21773ab0b77 \ --hash=sha256:9dd412127cbe0e115bd3fc5c6cbe9cf59f593273fafab9f7dc6b2ac95efcc7c1 \
--hash=sha256:8346b0086d213b80430f3bb4477379a8a4fa550b03447d23c84eee85c2e52bc4 \ --hash=sha256:ac337dacd640aab1ef97cf00f8c01e2889f0fd0ef8460a0f4e816bf12bb5988b \
--hash=sha256:8602a1b6300a3a948afacc62e1cb933c8394c27966db85ed7e29483300b69dc4 \ --hash=sha256:b3f515fd6b43068f241467496bced62cb2ed36d52d4c0877cfe61a1240713d32 \
--hash=sha256:9312b6fd44361674e9c847a828ded792493766697816261e05848a024fe34129 \ --hash=sha256:b8abe7d6f5e0d92bd41a9c000bbd9c8387af7886df4790c0451a34e781b8a075 \
--hash=sha256:bf568c62dddd55ad9c85a16ffdfbcc7746be9c3159ed644e4f9e6f5e44905cbb \ --hash=sha256:bbc41182d655f92cd380ecdf378da7fc1598c6b19057208f450f0ee9c259f46a \
--hash=sha256:c4ab221c0a949f8006a7582786dae384706b2f82016a0db60baa7cc696042705 \ --hash=sha256:c2089b992919858dabae89d410cbb5cecf9034d26bbb04f14e6da52dffced290 \
--hash=sha256:c8ec3caf656645f58b53cb9aee9aa95cfc65c82ba2d7f1362bfd2660d1484307 \ --hash=sha256:ca1d37e851fb9323250385403d8512a71c0d1b6162c729ff4909f37cfd067920 \
--hash=sha256:d6a49543c659c0cf1ff4c944955d97e69dbce4b76e3d284f5a92cea19133ebb6 \ --hash=sha256:d256f90513d01ff6cbc2f17d88c0ccde65d138500df547ece214e6a50731c4b7 \
--hash=sha256:e499579f557abf17b8ffd1490d3e827748aea096f6eaa66737e729e046449f08 \ --hash=sha256:d62410e5f60a961cfda00ead8a1cc5fd37d052afda021099e488e90c15419beb \
--hash=sha256:e7e78c18686202c8b8715bebb83bfaf58f82d7fb848b6a5ae4e925a9fac3de4c \ --hash=sha256:e7e215d69ea21fd5824a63edf8fef933bee2c028a0c2930651cfa6b88ca4ff8e \
--hash=sha256:ed0c9a9d7909f0e48a9dafe666ca9ebefe2a1534e51ed05c0a7de7406465f868 --hash=sha256:f2476dda35866ea3ded3a5905759da2d32dfac36dfd5b3428191a99a8ce15b02
# via -r contrib/container/requirements.in # via -r contrib/container/requirements.in
wheel==0.47.0 \ wheel==0.47.0 \
--hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \

View File

@ -1,4 +1,4 @@
# Packages needed for CI/packages # Packages needed for CI/packages
requests==2.34.2 requests==2.34.2
pyyaml==6.0.3 pyyaml==6.0.3
jc jc==1.25.6

View File

@ -145,9 +145,9 @@ idna==3.18 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# requests # requests
jc==1.25.7 \ jc==1.25.6 \
--hash=sha256:5ed6a0da915c931c04693cab806d5c31d9ef14ca998c6c32308460d37cb30baf \ --hash=sha256:27f58befc7ae0a4c63322926c5f1ec892e3eac4a065eff3b07cfe420a6924a07 \
--hash=sha256:63481e34d92715781e1b094eca7d76d122a14489463d45b7f3e8180f44ea3a10 --hash=sha256:7367b59e6e0da8babeede1e5b0da083f3c5aa6b6e585b4aed28dd7c4b2d76162
# via -r contrib/dev_reqs/requirements.in # via -r contrib/dev_reqs/requirements.in
pygments==2.20.0 \ pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \

View File

@ -79,7 +79,7 @@ function detect_python() {
echo "${On_Red}" echo "${On_Red}"
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!" 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| 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}" echo "${Color_Off}"
exit 1 exit 1
fi fi

View File

@ -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_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false} 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} export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
# SETUP_DEBUG can be set to get debug info # SETUP_DEBUG can be set to get debug info
# SETUP_EXTRA_PIP can be set to install extra pip packages # SETUP_EXTRA_PIP can be set to install extra pip packages

View File

@ -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. 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" !!! 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 #### Requesting a Token

View File

@ -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. 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 ## Barcode Settings

View File

@ -4,7 +4,7 @@ title: Importing Data
## 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" !!! danger "Danger"
Uploading bulk data directly is a non-reversible action. Uploading bulk data directly is a non-reversible action.

View File

@ -35,7 +35,7 @@ Parameter templates are used to define the different types of parameters which a
### Create 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: To create a template:

View File

@ -60,8 +60,7 @@ The user menu provides access to the following items:
- **User Settings:** Access to [user settings](../settings/user.md). - **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.* - **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. - **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.*
- **Database Admin Interface:** Low-level administration via the [Database Admin interface](../settings/db_admin.md). *Note: Access may be restricted based on user permissions and staff status.*
- **Change Color Mode:** Toggle between light and dark color modes. - **Change Color Mode:** Toggle between light and dark color modes.
- **About InvenTree:** View version and license information about InvenTree. - **About InvenTree:** View version and license information about InvenTree.
- **Logout:** Log out of the InvenTree system. - **Logout:** Log out of the InvenTree system.
@ -284,14 +283,6 @@ Example: Editing an existing purchase order via the "Edit Purchase Order" form:
{{ image("concepts/ui_form_edit_po.png", "Edit Purchase Order") }} {{ 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 ### 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. 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.

View File

@ -184,7 +184,7 @@ django-upgrade --target-version {{ config.extra.django_version }} `find . -name
## Migration Files ## 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!* *Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*

View File

@ -4,10 +4,10 @@ import json
import os import os
import re import re
from datetime import datetime from datetime import datetime
from distutils.version import StrictVersion # type: ignore[import]
from pathlib import Path from pathlib import Path
import requests import requests
from packaging.version import Version
here = Path(__file__).parent here = Path(__file__).parent
@ -57,7 +57,7 @@ def fetch_rtd_versions():
print('No RTD token found - skipping RTD version fetch') print('No RTD token found - skipping RTD version fetch')
# Sort versions by version number # 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 # Add "latest" version first
if not any(x['title'] == 'latest' for x in versions): if not any(x['title'] == 'latest' for x in versions):

View File

@ -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*. 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 ### 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. 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.

View File

@ -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.

View File

@ -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) - [Via the REST API](../api/index.md)
- [Using the Python library](../api/python/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)

View File

@ -87,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. 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 ## Locked Parts

View File

@ -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. 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" !!! 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 ## Plugin Mixins

View File

@ -4,57 +4,15 @@ title: Report Mixin
## ReportMixin ## 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 ### 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. 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 ### Add Label Context
Similarly, the `add_label_context` method allows custom context data to be added to a label template at time of printing: Additionally the `add_label_context` method, allowing 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)
```
### Sample Plugin ### Sample Plugin

View File

@ -74,7 +74,7 @@ npm install
npm run build 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.
![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_walkthrough_default.png "Attachment Carousel in Inventree panel screenshot") ![Attachment Carousel in Inventree panel screenshot](../assets/images/plugin/plugin_walkthrough_default.png "Attachment Carousel in Inventree panel screenshot")

View File

@ -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 - [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 - [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 - [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 ## Past Supporters

View File

@ -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.

View File

@ -69,14 +69,12 @@ Templates (whether for generating [reports](./report.md) or [labels](./labels.md
| Model Type | Description | | Model Type | Description |
| --- | --- | | --- | --- |
| [company](#company) | A Company instance | | company | A Company instance |
| [build](#build-order) | A [Build Order](../manufacturing/build.md) instance | | [build](#build-order) | A [Build Order](../manufacturing/build.md) instance |
| [buildline](#build-line) | A [Build Order Line Item](../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 | | [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 | | [returnorder](#return-order) | A [Return Order](../sales/return_order.md) instance |
| [purchaseorder](#purchase-order) | A [Purchase Order](../purchasing/purchase_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 | | [stockitem](#stock-item) | A [StockItem](../stock/index.md#stock-item) instance |
| [stocklocation](#stock-location) | A [StockLocation](../stock/index.md#stock-location) instance | | [stocklocation](#stock-location) | A [StockLocation](../stock/index.md#stock-location) instance |
| [part](#part) | A [Part](../part/index.md) 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") }} {{ 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 ### Stock Item
When printing a report or label against a [StockItem](../stock/index.md#stock-item) object, the following context variables are available: 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 ## 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. 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 | | name | Brief name for this part |
| full_name | Full name for this part (including IPN, if not null and including variant, if not null) | | 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 | category | The [PartCategory](#part-category) object to which this part belongs
| description | Longer form description of the part | description | Longer form description of the part
| keywords | Optional keywords for improving part search results | 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 | | Variable | Description |
|----------|-------------| |----------|-------------|
| parent | Link to another [StockItem](#stock-item) from which this StockItem was created | | 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 | | 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) | | supplier_part | Link to a specific [SupplierPart](#supplierpart) (optional) |
| location | The [StockLocation](#stock-location) Where this [StockItem](#stock-item) is located | | 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) | | 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") | | 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) | | 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) | | 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) | | 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) | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | contact | Contact Name |
| phone | Contact phone number | | phone | Contact phone number |
| email | Contact email address | | email | Contact email address |
| link | URL associated with the company | | link | A second URL to the company (Actually only accessible in the admin interface) |
| notes | Extra notes about the company | | notes | Extra notes about the company (Actually only accessible in the admin interface) |
| is_customer | Boolean value, is this company a customer | | is_customer | Boolean value, is this company a customer |
| is_supplier | Boolean value, is this company a supplier | | is_supplier | Boolean value, is this company a supplier |
| is_manufacturer | Boolean value, is this company a manufacturer | | 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 | | Variable | Description |
|----------|-------------| |----------|-------------|
| username | the username of the user | | 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 | | last_name | The last name of the user |
| email | The email address of the user | | email | The email address of the user |
| pk | The primary key of the user | | pk | The primary key of the user |

View File

@ -907,7 +907,7 @@ If you have a custom logo, but explicitly wish to load the InvenTree logo itself
## Report Assets ## 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: You can add asset images to the reports and labels by using the `{% raw %}{% asset ... %}{% endraw %}` template tag:

View File

@ -53,43 +53,29 @@ To read more about the capabilities of the report templating engine, and how to
## Creating Templates ## 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) 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:
- **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
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" !!! 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" !!! info "Editing Reports"
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. 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 ### Name and Description
Each report template requires a name and description, which identify and describe the report template. 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 ### 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. 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 ### Filename Pattern
The filename pattern used to generate the output `.pdf` file. Defaults to "report.pdf". 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 ## 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 ## 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 ## 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 | | `data:` URIs | Always permitted — self-contained, no network access |
| `file://` | Always blocked — assets and images must be inlined as `data:` URIs before reaching WeasyPrint | | `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 | | Any other scheme | Always blocked |
HTTP redirects are also disabled: a URL that passes validation cannot redirect to an internal address. 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
[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. There are various [helper functions](./helpers.md#report-assets) available to assist with embedding assets into templates.

View File

@ -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`. 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 ## 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. 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.

View File

@ -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. 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.
- 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.
## 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" ### Variables
The `page_size`, `landscape` and `merge` values are also made available to the template as [context variables](./context_variables.md#report-context).
## 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 ```html
2. Select the *Print* action, and choose the *Print Report* option {% raw %}
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
!!! info "Enable Reports" <!-- Template variables use {{ double_curly_braces }} -->
Report generation must be [enabled in the global settings](./index.md#enable-reports) before reports can be generated. <h2>Part: {{ part.name }}</h3>
<p><i>
Description:<br>
{{ part.description }}
</p></i>
{% endraw %}
```
## Merging Reports ## 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: 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:

View File

@ -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 | | [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 | | [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 | | [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 | | [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") }} {{ templatefile("report/inventree_test_report.html") }}
### Transfer Order
{{ templatefile("report/inventree_transfer_order_report.html") }}
### Selected Stock Items Report ### Selected Stock Items Report
{{ templatefile("report/inventree_stock_report_merge.html") }} {{ 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 | | Template | Model Type | Description |
| --- | --- | --- | | --- | --- | --- |
| [Build Line](#build-line-label) | [Build line item](../manufacturing/build.md) | Build Line label | | [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](#part-label) | [Part](../part/index.md) | Part label |
| [Part (Code128)](#part-label-code128) | [Part](../part/index.md) | Part label (Code128 barcode) |
| [Stock Item](#stock-item-label) | [StockItem](../stock/index.md#stock-item) | Stock Item 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](#stock-location-label) | [StockLocation](../stock/index.md#stock-location) | Stock Location label |
| [Stock Location (with text)](#stock-location-label-with-text) | [StockLocation](../stock/index.md#stock-location) | Stock Location label (QR code and location details) |
### Build Line 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 ### Part Label
{{ templatefile("label/part_label.html") }}
### Part Label (Code128)
{{ templatefile("label/part_label_code128.html") }} {{ templatefile("label/part_label_code128.html") }}
### Stock Item Label ### 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 ### Stock Location Label
{{ templatefile("label/stocklocation_qr.html") }}
### Stock Location Label (with text)
{{ templatefile("label/stocklocation_qr_and_text.html") }} {{ templatefile("label/stocklocation_qr_and_text.html") }}

View File

@ -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 %}
```

View File

@ -4,11 +4,11 @@ title: Template editor
## 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.
![Template Table](../assets/images/report/template-table.png) ![Template Table](../assets/images/report/template-table.png)
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 ### 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 #### 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 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.

View File

@ -16,4 +16,4 @@ To make MFA mandatory for all users:
### Security Consideration ### 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.

View File

@ -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. 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. 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. Configure the *callback* URL for the external app.
1. Enable SSO for the users in the [global settings](../settings/global.md). 1. Enable SSO for the users in the [global settings](../settings/global.md).
1. Configure [e-mail](../settings/email.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 ## 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.

View File

@ -4,23 +4,24 @@ title: InvenTree Admin Interfaces
## 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): [**Admin Center**](#admin-center):
- Main administration interface for day-to-day operations - Main interface for managing InvenTree
- Uses API-backed flows with validation and safety checks - Robust verification and safety checks
[**System Settings**](#system-settings): [**System Settings**](#system-settings):
- Access to global runtime settings - Access to all settings
- Available to staff users (or users with equivalent API scope) - Robust verification, requires reading the documentation
[**Database Admin Interface**](./db_admin.md): [**Backend Admin Interface**](#backend-admin-interface):
- Low-level database administration - Low level access to the database
- Fewer safeguards than the Admin Center - Few verification or safety checks
- Intended for advanced users and troubleshooting scenarios - Requires knowledge of InvenTree internals
- Recommended for advanced users only
### Admin Center ### 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) - Integration with external services (via machines and plugins)
- Reporting and statistics - Reporting and statistics
#### Access Admin Center 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.
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`
#### Permissions #### 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. 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") }}

View File

@ -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.

View File

@ -226,7 +226,6 @@ Configuration of stock item options
{{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS") }} {{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS") }}
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }} {{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }} {{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
{{ globalsetting("STOCK_MERGE_ON_TRANSFER") }}
{{ globalsetting("TEST_STATION_DATA") }} {{ globalsetting("TEST_STATION_DATA") }}
### Build Orders ### Build Orders

View File

@ -1,19 +1,22 @@
--- ---
title: Error Logs title: Admin Shell
--- ---
## Error Logs ## 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") }} {{ 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" !!! info "Deleting Logs"
Error logs should be deleted periodically Error logs should be deleted periodically

View File

@ -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. 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" !!! 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 ### User
@ -54,7 +54,7 @@ Within each role, there are four levels of available permissions:
## Dangerous User Flags ## Dangerous User Flags
In addition to the above permissions, there are two special flags that can be assigned to a user: 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. - **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. 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. 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 ## Web Interface Permissions

View File

@ -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. 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.

View File

@ -65,7 +65,7 @@ The following basic options are available:
{{ configsetting("INVENTREE_SITE_URL") }} Specify a fixed site URL | {{ configsetting("INVENTREE_SITE_URL") }} Specify a fixed site URL |
{{ configsetting("INVENTREE_TIMEZONE") }} Server timezone | {{ configsetting("INVENTREE_TIMEZONE") }} Server timezone |
{{ configsetting("INVENTREE_ADMIN_ENABLED") }} Enable the [django administrator interface]({% include "django.html" %}/ref/contrib/admin/) | {{ 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_LANGUAGE") }} Default language |
{{ configsetting("INVENTREE_AUTO_UPDATE") }} Database migrations will be run automatically | {{ configsetting("INVENTREE_AUTO_UPDATE") }} Database migrations will be run automatically |

View File

@ -325,7 +325,7 @@ ARG INVENTREE_TAG
# prebuild stage - needs a lot of build dependencies # prebuild stage - needs a lot of build dependencies
# make sure, the alpine and python version matches the version used in the inventree base image # 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) # 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 && \ RUN apk add --no-cache cups-dev gcc musl-dev && \

View File

@ -130,7 +130,6 @@ nav:
- Parts: part/index.md - Parts: part/index.md
- Creating Parts: part/create.md - Creating Parts: part/create.md
- Virtual Parts: part/virtual.md - Virtual Parts: part/virtual.md
- Consumable Parts: part/consumable.md
- Part Views: part/views.md - Part Views: part/views.md
- Tracking: part/trackable.md - Tracking: part/trackable.md
- Revisions: part/revision.md - Revisions: part/revision.md
@ -175,8 +174,6 @@ nav:
- Template Editor: report/template_editor.md - Template Editor: report/template_editor.md
- Reports: report/report.md - Reports: report/report.md
- Labels: report/labels.md - Labels: report/labels.md
- Report Assets: report/assets.md
- Report Snippets: report/snippets.md
- Context Variables: report/context_variables.md - Context Variables: report/context_variables.md
- Helper Functions: report/helpers.md - Helper Functions: report/helpers.md
- Barcodes: report/barcodes.md - Barcodes: report/barcodes.md
@ -185,8 +182,7 @@ nav:
- Global Settings: settings/global.md - Global Settings: settings/global.md
- User Settings: settings/user.md - User Settings: settings/user.md
- Reference Patterns: settings/reference.md - Reference Patterns: settings/reference.md
- Admin Center: settings/admin.md - Admin Interface: settings/admin.md
- Database Admin Interface: settings/db_admin.md
- Setup: - Setup:
- User Permissions: settings/permissions.md - User Permissions: settings/permissions.md
- Single Sign on: settings/SSO.md - Single Sign on: settings/SSO.md
@ -370,7 +366,7 @@ extra:
# provider: google # provider: google
# property: UA-143467500-1 # property: UA-143467500-1
min_python_version: 3.12 min_python_version: 3.11
min_invoke_version: 2.0.0 min_invoke_version: 2.0.0
django_version: 5.2 django_version: 5.2
docker_postgres_version: 17 docker_postgres_version: 17

View File

@ -31,6 +31,9 @@
}, },
{ {
"pattern": "https://opensource.org/license/MIT" "pattern": "https://opensource.org/license/MIT"
},
{
"pattern": "^https://dev.azure.com"
} }
] ]
} }

View File

@ -97,7 +97,7 @@ skip-magic-trailing-comma = true
line-ending = "auto" line-ending = "auto"
[tool.uv.pip] [tool.uv.pip]
python-version = "3.12" python-version = "3.11"
no-strip-extras=true no-strip-extras=true
generate-hashes=true generate-hashes=true
@ -105,16 +105,17 @@ generate-hashes=true
extra-paths = ["src/backend/InvenTree"] extra-paths = ["src/backend/InvenTree"]
[tool.ty.rules] [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 ## call-non-callable="ignore" # 8 ##
invalid-assignment="ignore" # 40 # need to wait for better django field stubs invalid-assignment="ignore" # 17 # need to wait for better django field stubs
invalid-method-override="ignore" # 103 invalid-method-override="ignore" # 104
invalid-return-type="ignore" # 44 ## 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 invalid-argument-type="ignore" # 49
no-matching-overload="ignore" # 10 # need to wait for betterdjango field stubs no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
# possibly-unbound-attribute="ignore" # 21
# warn about unused ignore comments
unused-ignore-comment = "error"
[tool.coverage.run] [tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"] source = ["src/backend/InvenTree", "InvenTree"]

View File

@ -9,9 +9,9 @@ python:
- requirements: src/backend/requirements.txt - requirements: src/backend/requirements.txt
build: build:
os: "ubuntu-24.04" os: "ubuntu-22.04"
tools: tools:
python: "3.12" python: "3.11"
jobs: jobs:
post_install: post_install:
- pip install -U invoke - pip install -U invoke

View File

@ -1,36 +1,11 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 518 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
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 v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204
- Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints - Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints

View File

@ -25,7 +25,6 @@ def log_error(
error_data: Optional[str] = None, error_data: Optional[str] = None,
scope: Optional[str] = None, scope: Optional[str] = None,
plugin: Optional[str] = None, plugin: Optional[str] = None,
user_id: Optional[int] = None,
): ):
"""Log an error to the database. """Log an error to the database.
@ -38,7 +37,6 @@ def log_error(
error_data: The error data (optional, overrides 'data') error_data: The error data (optional, overrides 'data')
scope: The scope of the error (optional) scope: The scope of the error (optional)
plugin: The plugin name associated with this error (optional) plugin: The plugin name associated with this error (optional)
user_id: The user ID associated with this error (optional)
""" """
import InvenTree.ready import InvenTree.ready
@ -73,9 +71,6 @@ def log_error(
except AttributeError: except AttributeError:
data = 'No traceback information available' data = 'No traceback information available'
if user_id:
data = f'User ID: {user_id}\n{data}'
# Log error to stderr # Log error to stderr
logger.error(info) logger.error(info)

View File

@ -1,7 +1,6 @@
"""Custom fields used in InvenTree.""" """Custom fields used in InvenTree."""
import sys import sys
import uuid
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
@ -60,34 +59,6 @@ class InvenTreeURLField(models.URLField):
super().__init__(**kwargs) 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): def money_kwargs(**kwargs):
"""Returns the database settings for MoneyFields.""" """Returns the database settings for MoneyFields."""
from common.currency import currency_code_mappings from common.currency import currency_code_mappings
@ -100,9 +71,7 @@ def money_kwargs(**kwargs):
kwargs['decimal_places'] = 6 kwargs['decimal_places'] = 6
if 'currency_choices' not in kwargs: if 'currency_choices' not in kwargs:
# Pass the function itself (not the evaluated result) so that the kwargs['currency_choices'] = currency_code_mappings()
# available currency options are resolved dynamically.
kwargs['currency_choices'] = currency_code_mappings
if InvenTree.ready.isRunningMigrations(): if InvenTree.ready.isRunningMigrations():
# During migrations, avoid setting a default currency # During migrations, avoid setting a default currency

View File

@ -16,7 +16,6 @@ from drf_spectacular.utils import (
extend_schema, extend_schema,
extend_schema_view, extend_schema_view,
) )
from rest_framework import serializers
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from InvenTree.permissions import OASTokenMixin from InvenTree.permissions import OASTokenMixin
@ -47,20 +46,6 @@ class ExtendedOAuth2Scheme(DjangoOAuthToolkitScheme):
class ExtendedAutoSchema(AutoSchema): class ExtendedAutoSchema(AutoSchema):
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior.""" """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: def is_bulk_action(self, ref: str) -> bool:
"""Check the class of the current view for the bulk mixins.""" """Check the class of the current view for the bulk mixins."""
return ref in [c.__name__ for c in type(self.view).__mro__] return ref in [c.__name__ for c in type(self.view).__mro__]

View File

@ -600,7 +600,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
Default implementation returns an empty list Default implementation returns an empty list
""" """
return getattr(self, 'SKIP_CREATE_FIELDS', []) return []
def save(self, **kwargs): def save(self, **kwargs):
"""Catch any django ValidationError thrown at the moment `save` is called, and re-throw as a DRF ValidationError.""" """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) super().__init__(*args, **kwargs)
if hasattr(self, 'context'): if hasattr(self, 'context'):
request = self.context.get('request', None)
method = getattr(request, 'method', None)
if view := self.context.get('view', None): if view := self.context.get('view', None):
if ( if (
issubclass(view.__class__, ListModelMixin) issubclass(view.__class__, ListModelMixin)
and method in SAFE_METHODS
and not InvenTree.ready.isGeneratingSchema() and not InvenTree.ready.isGeneratingSchema()
): ):
self.fields.pop('notes', None) self.fields.pop('notes', None)
@ -925,104 +921,3 @@ class ContentTypeField(serializers.ChoiceField):
) )
return content_type 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', ''),
)

View File

@ -1,7 +1,5 @@
"""Tests for custom InvenTree management commands.""" """Tests for custom InvenTree management commands."""
import os
import subprocess
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
@ -17,48 +15,6 @@ from InvenTree.config import get_testfolder_dir
class CommandTestCase(TestCase): class CommandTestCase(TestCase):
"""Test case for custom management commands.""" """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): def test_schema(self):
"""Test the schema generation command.""" """Test the schema generation command."""
output = call_command('schema', file='schema.yml', verbosity=0) output = call_command('schema', file='schema.yml', verbosity=0)

View File

@ -22,7 +22,6 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from djmoney.contrib.exchange.models import Rate, convert_money from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.money import Money from djmoney.money import Money
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
from rest_framework import serializers
from sesame.utils import get_user from sesame.utils import get_user
from stdimage.models import StdImageFieldFile from stdimage.models import StdImageFieldFile
@ -1818,35 +1817,6 @@ class SchemaPostprocessingTest(TestCase):
# required key removed when empty # required key removed when empty
self.assertNotIn('required', schemas_out.get('SalesOrderShipment')) 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): class URLCompatibilityTest(InvenTreeTestCase):
"""Unit test for legacy URL compatibility.""" """Unit test for legacy URL compatibility."""

View File

@ -15,10 +15,10 @@ from datetime import timedelta as td
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version # InvenTree software version
INVENTREE_SW_VERSION = '1.5.0 dev' INVENTREE_SW_VERSION = '1.4.1'
# Minimum supported Python version # Minimum supported Python version
MIN_PYTHON_VERSION = (3, 12) MIN_PYTHON_VERSION = (3, 11)
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -133,14 +133,27 @@ def isInvenTreeDevelopmentVersion() -> bool:
return inventreeVersion().endswith('dev') return inventreeVersion().endswith('dev')
def inventreeDocsVersion() -> str:
"""Return the version string matching the latest documentation.
Development -> "latest"
Release -> "major.minor.sub" e.g. "0.5.2"
"""
if isInvenTreeDevelopmentVersion():
return 'latest'
return INVENTREE_SW_VERSION
def inventreeDocUrl() -> str: def inventreeDocUrl() -> str:
"""Return URL for InvenTree documentation site.""" """Return URL for InvenTree documentation site."""
return 'https://docs.inventree.org' tag = inventreeDocsVersion()
return f'https://docs.inventree.org/en/{tag}'
def inventreeAppUrl() -> str: def inventreeAppUrl() -> str:
"""Return URL for InvenTree app site.""" """Return URL for InvenTree app site."""
return 'https://docs.inventree.org/en/latest/app/' return 'https://docs.inventree.org/en/stable/app/'
def inventreeGithubUrl() -> str: def inventreeGithubUrl() -> str:

View File

@ -457,21 +457,8 @@ class BuildLineFilter(FilterSet):
# Fields on related models # Fields on related models
consumable = rest_filters.BooleanFilter( consumable = rest_filters.BooleanFilter(
label=_('Consumable'), method='filter_consumable' label=_('Consumable'), field_name='bom_item__consumable'
) )
def filter_consumable(self, queryset, name, value):
"""Filter the queryset based on the "effective" consumable status of the BOM item.
A BuildLine is considered "consumable" if either the BOM item itself,
or the underlying part, is marked as consumable.
"""
return queryset.filter(
part_models.BomItem.consumable_filter(
consumable=str2bool(value), prefix='bom_item__'
)
)
optional = rest_filters.BooleanFilter( optional = rest_filters.BooleanFilter(
label=_('Optional'), field_name='bom_item__optional' label=_('Optional'), field_name='bom_item__optional'
) )

View File

@ -37,7 +37,10 @@ from build.validators import (
validate_build_order_reference, validate_build_order_reference,
) )
from common.models import ProjectCode from common.models import ProjectCode
from common.settings import get_global_setting from common.settings import (
get_global_setting,
prevent_build_output_complete_on_incompleted_tests,
)
from generic.enums import StringEnum from generic.enums import StringEnum
from generic.states import StateTransitionMixin, StatusCodeMixin from generic.states import StateTransitionMixin, StatusCodeMixin
from plugin.events import trigger_event from plugin.events import trigger_event
@ -974,9 +977,7 @@ class Build(
items_to_delete = [] items_to_delete = []
lines = self.untracked_line_items.all() lines = self.untracked_line_items.all()
lines = lines.exclude( lines = lines.exclude(bom_item__consumable=True)
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
lines = lines.annotate(allocated=annotate_allocated_quantity()) lines = lines.annotate(allocated=annotate_allocated_quantity())
for build_line in lines: for build_line in lines:
@ -1092,61 +1093,6 @@ class Build(
}, },
) )
def can_complete_output(
self,
output: stock.models.StockItem,
quantity: Optional[decimal.Decimal] = None,
required_tests=None,
) -> bool:
"""Determine if the given build output can be completed.
Arguments:
output: The StockItem instance (build output) to check
quantity: The quantity to complete (defaults to entire output quantity)
required_tests: Optional list of required tests to check against (defaults to the part's required tests)
Returns:
True if the build output can be completed, False otherwise
Raises:
ValidationError: If the build output cannot be completed, with an appropriate message
"""
prevent_incomplete = get_global_setting(
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS'
)
if prevent_incomplete and not output.passedAllRequiredTests(
required_tests=required_tests
):
raise ValidationError(_('Build output has not passed all required tests'))
# Ensure that none of the allocated items are themselves still "in production"
allocated_items = output.items_to_install.all().filter(
stock_item__is_building=True
)
if allocated_items.exists():
raise ValidationError(_('Allocated stock items are still in production'))
if quantity is not None and quantity != output.quantity:
# Cannot split a build output with allocated items
if output.items_to_install.exists():
raise ValidationError({
'quantity': _(
'Cannot partially complete a build output with allocated items'
)
})
if quantity <= 0:
raise ValidationError({
'quantity': _('Quantity must be greater than zero')
})
if quantity > output.quantity:
raise ValidationError({
'quantity': _('Quantity cannot be greater than the output quantity')
})
@transaction.atomic @transaction.atomic
def complete_build_output( def complete_build_output(
self, self,
@ -1172,20 +1118,52 @@ class Build(
notes = kwargs.get('notes', '') notes = kwargs.get('notes', '')
required_tests = kwargs.get('required_tests', output.part.getRequiredTests()) required_tests = kwargs.get('required_tests', output.part.getRequiredTests())
prevent_on_incomplete = kwargs.get(
'prevent_on_incomplete',
prevent_build_output_complete_on_incompleted_tests(),
)
self.can_complete_output( if prevent_on_incomplete and not output.passedAllRequiredTests(
output, quantity=quantity, required_tests=required_tests required_tests=required_tests
):
msg = _('Build output has not passed all required tests')
if serial := output.serial:
msg = _(f'Build output {serial} has not passed all required tests')
raise ValidationError(msg)
# List the allocated BuildItem objects for the given output
allocated_items = output.items_to_install.all()
# Ensure that none of the allocated items are themselves still "in production"
for build_item in allocated_items:
if build_item.stock_item.is_building:
raise ValidationError(
_('Allocated stock items are still in production')
) )
# If a partial quantity is provided, split the stock output # If a partial quantity is provided, split the stock output
if quantity is not None and quantity != output.quantity: if quantity is not None and quantity != output.quantity:
# Cannot split a build output with allocated items
if allocated_items.count() > 0:
raise ValidationError(
_('Cannot partially complete a build output with allocated items')
)
if quantity <= 0:
raise ValidationError({
'quantity': _('Quantity must be greater than zero')
})
if quantity > output.quantity:
raise ValidationError({
'quantity': _('Quantity cannot be greater than the output quantity')
})
# Split the stock item # Split the stock item
output = output.splitStock(quantity, user=user, allow_production=True) output = output.splitStock(quantity, user=user, allow_production=True)
allocated_items = output.items_to_install.all().select_related(
'stock_item', 'stock_item__part'
)
for build_item in allocated_items: for build_item in allocated_items:
# Complete the allocation of stock for that item # Complete the allocation of stock for that item
build_item.complete_allocation(user=user) build_item.complete_allocation(user=user)
@ -1256,16 +1234,13 @@ class Build(
return allocations return allocations
tracked_line_items = self.tracked_line_items.filter( tracked_line_items = self.tracked_line_items.filter(
part.models.BomItem.consumable_filter( bom_item__consumable=False, bom_item__sub_part__virtual=False
consumable=False, prefix='bom_item__'
),
bom_item__sub_part__virtual=False,
) )
for line_item in tracked_line_items: for line_item in tracked_line_items:
bom_item = line_item.bom_item bom_item = line_item.bom_item
if bom_item.is_consumable: if bom_item.consumable:
# Do not auto-allocate stock to consumable BOM items # Do not auto-allocate stock to consumable BOM items
continue continue
@ -1389,7 +1364,7 @@ class Build(
# Find the referenced BomItem # Find the referenced BomItem
bom_item = line_item.bom_item bom_item = line_item.bom_item
if bom_item.is_consumable: if bom_item.consumable:
# Do not auto-allocate stock to consumable BOM items # Do not auto-allocate stock to consumable BOM items
continue continue
@ -1509,9 +1484,7 @@ class Build(
lines = self.build_lines.all() lines = self.build_lines.all()
# Remove any 'consumable' line items # Remove any 'consumable' line items
lines = lines.exclude( lines = lines.exclude(bom_item__consumable=True)
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
if tracked is True: if tracked is True:
lines = lines.filter(bom_item__sub_part__trackable=True) lines = lines.filter(bom_item__sub_part__trackable=True)
@ -1548,9 +1521,7 @@ class Build(
we need to test all "trackable" BuildLine objects we need to test all "trackable" BuildLine objects
""" """
lines = self.build_lines.filter(bom_item__sub_part__trackable=True) lines = self.build_lines.filter(bom_item__sub_part__trackable=True)
lines = lines.exclude( lines = lines.exclude(bom_item__consumable=True)
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
# Find any lines which have not been fully allocated # Find any lines which have not been fully allocated
for line in lines: for line in lines:
@ -1574,9 +1545,7 @@ class Build(
Returns: Returns:
True if any BuildLine has been over-allocated. True if any BuildLine has been over-allocated.
""" """
lines = self.build_lines.all().exclude( lines = self.build_lines.all().exclude(bom_item__consumable=True)
part.models.BomItem.consumable_filter(prefix='bom_item__')
)
lines = lines.prefetch_related('allocations') lines = lines.prefetch_related('allocations')
@ -1805,7 +1774,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
def is_fully_allocated(self) -> bool: def is_fully_allocated(self) -> bool:
"""Return True if this BuildLine is fully allocated.""" """Return True if this BuildLine is fully allocated."""
if self.bom_item.is_consumable: if self.bom_item.consumable:
return True return True
required = max(0, self.quantity - self.consumed) required = max(0, self.quantity - self.consumed)

View File

@ -1,8 +1,6 @@
"""JSON serializers for Build API.""" """JSON serializers for Build API."""
from collections.abc import Callable
from decimal import Decimal from decimal import Decimal
from typing import Optional
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models, transaction from django.db import models, transaction
@ -24,6 +22,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
import common.filters import common.filters
import common.settings
import company.serializers import company.serializers
import InvenTree.helpers import InvenTree.helpers
import part.filters import part.filters
@ -34,7 +33,6 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import ( from InvenTree.serializers import (
CustomStatusSerializerMixin, CustomStatusSerializerMixin,
DuplicateOptionsSerializer,
FilterableSerializerMixin, FilterableSerializerMixin,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
@ -54,7 +52,6 @@ from users.serializers import OwnerSerializer, UserSerializer
from .models import Build, BuildItem, BuildLine from .models import Build, BuildItem, BuildLine
from .status_codes import BuildStatus from .status_codes import BuildStatus
from .validators import check_build_output
class BuildSerializer( class BuildSerializer(
@ -68,8 +65,6 @@ class BuildSerializer(
): ):
"""Serializes a Build object.""" """Serializes a Build object."""
SKIP_CREATE_FIELDS = ['duplicate']
class Meta: class Meta:
"""Serializer metaclass.""" """Serializer metaclass."""
@ -83,7 +78,6 @@ class BuildSerializer(
'completed', 'completed',
'completion_date', 'completion_date',
'destination', 'destination',
'duplicate',
'external', 'external',
'parent', 'parent',
'part', 'part',
@ -194,29 +188,12 @@ class BuildSerializer(
return queryset return queryset
duplicate = DuplicateOptionsSerializer(Build.objects.all(), copy_parameters=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Determine if extra serializer fields are required.""" """Determine if extra serializer fields are required."""
kwargs.pop('create', False) kwargs.pop('create', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@transaction.atomic
def create(self, validated_data):
"""Create a new Build instance, optionally copying data from an existing build."""
duplicate = validated_data.pop('duplicate', None)
instance = super().create(validated_data)
if duplicate:
original = duplicate['original']
if duplicate.get('copy_parameters', True):
instance.copy_parameters_from(original)
return instance
def validate_reference(self, reference): def validate_reference(self, reference):
"""Custom validation for the Build reference field.""" """Custom validation for the Build reference field."""
# Ensure the reference matches the required pattern # Ensure the reference matches the required pattern
@ -283,19 +260,11 @@ class BuildOutputSerializer(serializers.Serializer):
class BuildOutputQuantitySerializer(BuildOutputSerializer): class BuildOutputQuantitySerializer(BuildOutputSerializer):
"""Build output with quantity field.""" """Build output with quantity field."""
# Optional callable to validate the output field, if required
output_validator: Optional[Callable] = None
class Meta: class Meta:
"""Serializer metaclass.""" """Serializer metaclass."""
fields = [*BuildOutputSerializer.Meta.fields, 'quantity'] fields = [*BuildOutputSerializer.Meta.fields, 'quantity']
def __init__(self, *args, **kwargs):
"""Initialize the serializer."""
self.output_validator = kwargs.pop('output_validator', None)
super().__init__(*args, **kwargs)
quantity = serializers.DecimalField( quantity = serializers.DecimalField(
max_digits=15, max_digits=15,
decimal_places=5, decimal_places=5,
@ -323,10 +292,6 @@ class BuildOutputQuantitySerializer(BuildOutputSerializer):
'quantity': _('Quantity cannot be greater than the output quantity') 'quantity': _('Quantity cannot be greater than the output quantity')
}) })
if self.output_validator:
# Call the parent serializer's output validator, if provided
self.output_validator(output, quantity=quantity)
return data return data
@ -562,9 +527,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
'notes', 'notes',
] ]
outputs = BuildOutputQuantitySerializer( outputs = BuildOutputQuantitySerializer(many=True, required=True)
many=True, required=True, output_validator=check_build_output
)
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(), queryset=StockLocation.objects.all(),
@ -591,6 +554,30 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
outputs = data.get('outputs', []) outputs = data.get('outputs', [])
if common.settings.prevent_build_output_complete_on_incompleted_tests():
errors = []
for output in outputs:
stock_item = output['output']
if (
stock_item.hasRequiredTests()
and not stock_item.passedAllRequiredTests()
):
serial = stock_item.serial
if serial:
errors.append(
_(
f'Build output {serial} has not passed all required tests'
)
)
else:
errors.append(
_('Build output has not passed all required tests')
)
if errors:
raise ValidationError(errors)
if len(outputs) == 0: if len(outputs) == 0:
raise ValidationError(_('A list of build outputs must be provided')) raise ValidationError(_('A list of build outputs must be provided'))
@ -996,7 +983,7 @@ class BuildAllocationSerializer(serializers.Serializer):
output = item.get('output', None) output = item.get('output', None)
# Ignore allocation for consumable BOM items # Ignore allocation for consumable BOM items
if build_line.bom_item.is_consumable: if build_line.bom_item.consumable:
continue continue
params = { params = {
@ -1389,7 +1376,7 @@ class BuildLineSerializer(
source='bom_item.reference', label=_('Reference'), read_only=True source='bom_item.reference', label=_('Reference'), read_only=True
) )
consumable = serializers.BooleanField( consumable = serializers.BooleanField(
source='bom_item.is_consumable', label=_('Consumable'), read_only=True source='bom_item.consumable', label=_('Consumable'), read_only=True
) )
optional = serializers.BooleanField( optional = serializers.BooleanField(
source='bom_item.optional', label=_('Optional'), read_only=True source='bom_item.optional', label=_('Optional'), read_only=True

View File

@ -11,7 +11,7 @@ from build.models import Build, BuildItem, BuildLine
from build.status_codes import BuildStatus from build.status_codes import BuildStatus
from common.settings import set_global_setting from common.settings import set_global_setting
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import BomItem, BomItemSubstitute, Part, PartTestTemplate from part.models import BomItem, BomItemSubstitute, Part
from stock.models import StockItem, StockLocation, StockSortOrder from stock.models import StockItem, StockLocation, StockSortOrder
from stock.status_codes import StockStatus from stock.status_codes import StockStatus
@ -1531,15 +1531,10 @@ class BuildOutputScrapTest(BuildAPITest):
'notes': 'Partial complete', 'notes': 'Partial complete',
} }
# Ensure that an invalid quantity raises an error, with the expected message # Ensure that an invalid quantity raises an error
for q, expected_message in [ for q in [-4, 0, 999]:
(-4, 'Ensure this value is greater than or equal to 0'),
(0, 'Quantity must be greater than zero'),
(999, 'Quantity cannot be greater than the output quantity'),
]:
data['outputs'][0]['quantity'] = q data['outputs'][0]['quantity'] = q
response = self.post(url, data, expected_code=400) self.post(url, data, expected_code=400)
self.assertIn(expected_message, str(response.data))
# Partially complete the output (with a valid quantity) # Partially complete the output (with a valid quantity)
data['outputs'][0]['quantity'] = 4 data['outputs'][0]['quantity'] = 4
@ -1557,94 +1552,6 @@ class BuildOutputScrapTest(BuildAPITest):
self.assertEqual(completed_output.status, StockStatus.OK) self.assertEqual(completed_output.status, StockStatus.OK)
self.assertFalse(completed_output.is_building) self.assertFalse(completed_output.is_building)
def test_complete_with_required_tests(self):
"""Test that build output completion is blocked if required tests have not passed."""
build = Build.objects.get(pk=1)
output = build.create_build_output(1).first()
template = PartTestTemplate.objects.create(
part=build.part, test_name='Required test', required=True
)
set_global_setting(
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None
)
url = reverse('api-build-output-complete', kwargs={'pk': build.pk})
data = {'outputs': [{'output': output.pk}], 'location': 1}
response = self.post(url, data, expected_code=400)
self.assertIn(
'Build output has not passed all required tests', str(response.data)
)
# Add a passing test result - the output should now be able to be completed
output.add_test_result(template=template, result=True)
self.post(url, data, expected_code=200)
def test_complete_still_in_production(self):
"""Test that build output completion is blocked if an allocated item is still in production."""
build = Build.objects.get(pk=1)
output = build.create_build_output(1).first()
build.create_build_line_items()
line = build.build_lines.first()
sub_build = Build.objects.create(
part=line.bom_item.sub_part,
quantity=1,
title='Sub-build',
reference='BO-9998',
)
in_production = StockItem.objects.create(
part=line.bom_item.sub_part, quantity=1, is_building=True, build=sub_build
)
BuildItem.objects.create(
build_line=line, stock_item=in_production, quantity=1, install_into=output
)
url = reverse('api-build-output-complete', kwargs={'pk': build.pk})
response = self.post(
url, {'outputs': [{'output': output.pk}], 'location': 1}, expected_code=400
)
self.assertIn(
'Allocated stock items are still in production', str(response.data)
)
def test_partial_complete_with_allocated_items(self):
"""Test that a build output with allocated items cannot be partially completed."""
build = Build.objects.get(pk=1)
output = build.create_build_output(10).first()
build.create_build_line_items()
line = build.build_lines.first()
stock_item = StockItem.objects.create(part=line.bom_item.sub_part, quantity=10)
BuildItem.objects.create(
build_line=line, stock_item=stock_item, quantity=1, install_into=output
)
url = reverse('api-build-output-complete', kwargs={'pk': build.pk})
response = self.post(
url,
{'outputs': [{'output': output.pk, 'quantity': 4}], 'location': 1},
expected_code=400,
)
self.assertIn(
'Cannot partially complete a build output with allocated items',
str(response.data),
)
class BuildOutputCancelTest(BuildAPITest): class BuildOutputCancelTest(BuildAPITest):
"""Test cancellation of build outputs.""" """Test cancellation of build outputs."""
@ -1887,77 +1794,6 @@ class BuildLineTests(BuildAPITest):
self.assertSetEqual(false_ids, expected_false) self.assertSetEqual(false_ids, expected_false)
self.assertSetEqual(true_ids | false_ids, {line.pk for line in lines}) self.assertSetEqual(true_ids | false_ids, {line.pk for line in lines})
def test_filter_consumable_via_part(self):
"""Filter BuildLine objects by 'consumable' status, accounting for the underlying part.
A BuildLine should be treated as 'consumable' if either the BOM line
itself is marked as consumable, or the underlying part is marked as consumable.
"""
assembly = Part.objects.create(
name='Consumable Filter Assembly',
description='Assembly for consumable filter tests',
assembly=True,
)
plain = Part.objects.create(
name='Consumable Filter Plain Component',
description='A regular component',
component=True,
)
consumable_part = Part.objects.create(
name='Consumable Filter Consumable Part',
description='A part marked as consumable',
component=True,
consumable=True,
)
consumable_line_part = Part.objects.create(
name='Consumable Filter Consumable BOM Line',
description='A part which is consumable only via its BOM line',
component=True,
)
bom_item_plain = BomItem.objects.create(
part=assembly, sub_part=plain, quantity=1
)
bom_item_part_consumable = BomItem.objects.create(
part=assembly, sub_part=consumable_part, quantity=1
)
bom_item_line_consumable = BomItem.objects.create(
part=assembly, sub_part=consumable_line_part, quantity=1, consumable=True
)
build = Build.objects.create(
part=assembly,
reference='BO-9997',
quantity=1,
title='Consumable Filter Build',
)
url = reverse('api-build-line-list')
response = self.get(url, data={'build': build.pk, 'consumable': True})
returned_bom_items = {item['bom_item'] for item in response.data}
self.assertIn(bom_item_part_consumable.pk, returned_bom_items)
self.assertIn(bom_item_line_consumable.pk, returned_bom_items)
self.assertNotIn(bom_item_plain.pk, returned_bom_items)
response = self.get(url, data={'build': build.pk, 'consumable': False})
returned_bom_items = {item['bom_item'] for item in response.data}
self.assertIn(bom_item_plain.pk, returned_bom_items)
self.assertNotIn(bom_item_part_consumable.pk, returned_bom_items)
self.assertNotIn(bom_item_line_consumable.pk, returned_bom_items)
# Check that the serialized 'consumable' field reflects the combined status
response = self.get(
url, data={'build': build.pk, 'bom_item': bom_item_part_consumable.pk}
)
self.assertEqual(len(response.data), 1)
self.assertTrue(response.data[0]['consumable'])
def test_output_options(self): def test_output_options(self):
"""Test output options for the BuildLine endpoint.""" """Test output options for the BuildLine endpoint."""
self.run_output_test( self.run_output_test(

View File

@ -650,15 +650,11 @@ class BuildTest(BuildTestBase):
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None 'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', True, change_user=None
) )
with self.assertRaises(ValidationError) as exc: with self.assertRaises(ValidationError):
self.build_w_tests_trackable.complete_build_output( self.build_w_tests_trackable.complete_build_output(
self.stockitem_with_required_test, None self.stockitem_with_required_test, None
) )
self.assertIn(
'Build output has not passed all required tests', str(exc.exception)
)
# let's complete the required test and see if it could be saved # let's complete the required test and see if it could be saved
StockItemTestResult.objects.create( StockItemTestResult.objects.create(
stock_item=self.stockitem_with_required_test, stock_item=self.stockitem_with_required_test,
@ -675,59 +671,6 @@ class BuildTest(BuildTestBase):
self.stockitem_wo_required_test, None self.stockitem_wo_required_test, None
) )
def test_complete_output_still_in_production(self):
"""Test that a build output cannot be completed if allocated stock is still in production."""
# Create a stock item of the tracked sub-part, which is itself still "in production"
sub_build = Build.objects.create(
reference=generate_next_build_reference(),
title='Building a sub-part',
part=self.sub_part_3,
quantity=2,
issued_by=get_user_model().objects.get(pk=1),
)
in_production = StockItem.objects.create(
part=self.sub_part_3, quantity=2, is_building=True, build=sub_build
)
self.allocate_stock(self.output_1, {in_production: 2})
with self.assertRaises(ValidationError) as exc:
self.build.complete_build_output(self.output_1, None)
self.assertIn(
'Allocated stock items are still in production', str(exc.exception)
)
def test_partial_complete_with_allocated_items(self):
"""Test that a build output with tracked allocations cannot be partially completed."""
# Allocate tracked stock against output_1 (quantity=3)
self.allocate_stock(self.output_1, {self.stock_3_1: 6})
with self.assertRaises(ValidationError) as exc:
self.build.complete_build_output(self.output_1, None, quantity=1)
self.assertIn(
'Cannot partially complete a build output with allocated items',
str(exc.exception),
)
def test_complete_output_invalid_quantity(self):
"""Test that invalid quantities are rejected when completing a build output directly."""
with self.assertRaises(ValidationError) as exc:
self.build.complete_build_output(self.output_1, None, quantity=0)
self.assertIn('Quantity must be greater than zero', str(exc.exception))
with self.assertRaises(ValidationError) as exc:
self.build.complete_build_output(
self.output_1, None, quantity=self.output_1.quantity + 1
)
self.assertIn(
'Quantity cannot be greater than the output quantity', str(exc.exception)
)
def test_overdue_notification(self): def test_overdue_notification(self):
"""Test sending of notifications when a build order is overdue.""" """Test sending of notifications when a build order is overdue."""
self.ensurePluginsLoaded() self.ensurePluginsLoaded()
@ -998,38 +941,6 @@ class AutoAllocationTests(BuildTestBase):
self.assertEqual(self.build.allocated_stock.count(), N - 8) self.assertEqual(self.build.allocated_stock.count(), N - 8)
def test_consumable_via_part(self):
"""A BOM line should be treated as consumable if the underlying part is consumable.
Even though 'bom_item_1' itself is not marked as consumable, marking
'sub_part_1' as consumable should have the same effect as marking the
BOM line itself as consumable.
"""
self.sub_part_1.consumable = True
self.sub_part_1.save()
self.assertFalse(self.bom_item_1.consumable)
self.assertTrue(self.bom_item_1.is_consumable)
# The BuildLine should be treated as fully allocated, without any stock allocated
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertTrue(self.line_1.is_fully_allocated())
# The build should not consider this line when checking for unallocated lines
unallocated_lines = self.build.unallocated_lines(tracked=False)
self.assertNotIn(self.line_1, unallocated_lines)
# Auto-allocation should skip this line, even though stock exists
self.build.auto_allocate_stock(
interchangeable=True, substitutes=True, optional_items=True
)
self.assertEqual(self.line_1.allocated_quantity(), 0)
self.assertEqual(BuildItem.objects.filter(build_line=self.line_1).count(), 0)
# The other (non-consumable) line should still be allocated as normal
self.assertEqual(self.line_2.allocated_quantity(), 30)
class ExternalBuildTest(InvenTreeAPITestCase): class ExternalBuildTest(InvenTreeAPITestCase):
"""Unit tests for external build order functionality.""" """Unit tests for external build order functionality."""

View File

@ -21,13 +21,3 @@ def validate_build_order_reference(value):
# If we get to here, run the "default" validation routine # If we get to here, run the "default" validation routine
Build.validate_reference_field(value) Build.validate_reference_field(value)
def check_build_output(output, quantity=None):
"""Run a validation check against each output before accepting it for completion.
Arguments:
output (StockItem): The build output to check
quantity (Decimal, optional): The quantity to complete. If None, the full output quantity is assumed.
"""
output.build.can_complete_output(output, quantity=quantity)

View File

@ -115,8 +115,7 @@ class BarcodeScanResultAdmin(admin.ModelAdmin):
class ProjectCodeAdmin(admin.ModelAdmin): class ProjectCodeAdmin(admin.ModelAdmin):
"""Admin settings for ProjectCode.""" """Admin settings for ProjectCode."""
list_display = ('code', 'description', 'active') list_display = ('code', 'description')
list_filter = ('active',)
search_fields = ('code', 'description') search_fields = ('code', 'description')

View File

@ -491,7 +491,7 @@ class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['code'] ordering_fields = ['code']
filterset_fields = ['active']
search_fields = ['code', 'description'] search_fields = ['code', 'description']

View File

@ -162,21 +162,13 @@ def currency_exchange_plugins() -> Optional[list]:
def get_price( def get_price(
instance, instance,
quantity, quantity,
moq: bool = True, moq=True,
multiples: bool = True, multiples=True,
currency: Optional[str] = None, currency=None,
break_name: str = 'price_breaks', break_name: str = 'price_breaks',
): ):
"""Calculate the price based on quantity price breaks. """Calculate the price based on quantity price breaks.
Arguments:
instance: The model instance which contains the price break information
quantity: The quantity to calculate the price for
moq: If True, then minimum order quantity will be observed (CURRENTLY NOT IMPLEMENTED)
multiples: If True, then order multiples will be observed
currency: The currency code to use for the calculation (default is None)
break_name: The name of the price break field on the instance (default is 'price_breaks')
- Don't forget to add in flat-fee cost (base_cost field) - Don't forget to add in flat-fee cost (base_cost field)
- If MOQ (minimum order quantity) is required, bump quantity - If MOQ (minimum order quantity) is required, bump quantity
- If order multiples are to be observed, then we need to calculate based on that, too - If order multiples are to be observed, then we need to calculate based on that, too
@ -193,7 +185,7 @@ def get_price(
return None return None
# Check if quantity is fraction and disable multiples # Check if quantity is fraction and disable multiples
multiples = multiples and (quantity % 1 == 0) multiples = quantity % 1 == 0
# Order multiples # Order multiples
if multiples: if multiples:

View File

@ -1,22 +0,0 @@
# Generated by Django 5.2.15 on 2026-06-25 03:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("common", "0044_notificationmessage_charfield_pk"),
]
operations = [
migrations.AddField(
model_name="projectcode",
name="active",
field=models.BooleanField(
default=True,
help_text="Is this project code active?",
verbose_name="Active",
),
),
]

View File

@ -1,59 +0,0 @@
"""Migration to store UUID primary keys as char(32) on MySQL / MariaDB.
On MariaDB 10.7+, Django 5.x creates UUIDField columns with the native 'uuid'
type and writes 36-character (hyphenated) values. Databases migrated under older
Django / MariaDB versions retain char(32) columns, into which the new values
do not fit.
See: https://github.com/inventree/InvenTree/issues/12270
"""
import uuid
from django.db import migrations
import InvenTree.fields
class Migration(migrations.Migration):
dependencies = [('common', '0045_projectcode_active')]
operations = [
migrations.AlterField(
model_name='emailmessage',
name='global_id',
field=InvenTree.fields.InvenTreeUUIDField(
default=uuid.uuid4,
editable=False,
help_text='Unique identifier for this message',
primary_key=True,
serialize=False,
unique=True,
verbose_name='Global ID',
),
),
migrations.AlterField(
model_name='emailthread',
name='global_id',
field=InvenTree.fields.InvenTreeUUIDField(
default=uuid.uuid4,
editable=False,
help_text='Unique identifier for this thread',
primary_key=True,
serialize=False,
verbose_name='Global ID',
),
),
migrations.AlterField(
model_name='webhookmessage',
name='message_id',
field=InvenTree.fields.InvenTreeUUIDField(
default=uuid.uuid4,
editable=False,
help_text='Unique identifier for this message',
primary_key=True,
serialize=False,
verbose_name='Message ID',
),
),
]

View File

@ -181,12 +181,6 @@ class ProjectCode(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Project description'), help_text=_('Project description'),
) )
active = models.BooleanField(
default=True,
verbose_name=_('Active'),
help_text=_('Is this project code active?'),
)
responsible = models.ForeignKey( responsible = models.ForeignKey(
users.models.Owner, users.models.Owner,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -1388,27 +1382,21 @@ class PriceBreak(MetaMixin):
help_text=_('Unit price at specified quantity'), help_text=_('Unit price at specified quantity'),
) )
def convert_to(self, currency_code: str, raise_error: bool = False): def convert_to(self, currency_code):
"""Convert the unit-price at this price break to the specified currency code. """Convert the unit-price at this price break to the specified currency code.
Arguments: Args:
currency_code: The currency code to convert to (e.g "USD" or "AUD") currency_code: The currency code to convert to (e.g "USD" or "AUD")
raise_error: If True, raise an error if the conversion fails. If False, return None.
""" """
try: try:
converted = convert_money(self.price, currency_code) converted = convert_money(self.price, currency_code)
except MissingRate: # pragma: no cover except MissingRate:
InvenTree.exceptions.log_error('PriceBreak.convert_to')
logger.warning( logger.warning(
'No currency conversion rate available for %s -> %s', 'No currency conversion rate available for %s -> %s',
self.price_currency, self.price_currency,
currency_code, currency_code,
) )
return self.price.amount
if raise_error:
raise
return None
return converted.amount return converted.amount
@ -1590,7 +1578,7 @@ class WebhookMessage(models.Model):
worked_on: Was the work on this message finished? worked_on: Was the work on this message finished?
""" """
message_id = InvenTree.fields.InvenTreeUUIDField( message_id = models.UUIDField(
verbose_name=_('Message ID'), verbose_name=_('Message ID'),
help_text=_('Unique identifier for this message'), help_text=_('Unique identifier for this message'),
primary_key=True, primary_key=True,
@ -3272,7 +3260,7 @@ class EmailMessage(models.Model):
TRACK_READ = 'track_read', _('Track Read') TRACK_READ = 'track_read', _('Track Read')
TRACK_CLICK = 'track_click', _('Track Click') TRACK_CLICK = 'track_click', _('Track Click')
global_id = InvenTree.fields.InvenTreeUUIDField( global_id = models.UUIDField(
verbose_name=_('Global ID'), verbose_name=_('Global ID'),
help_text=_('Unique identifier for this message'), help_text=_('Unique identifier for this message'),
primary_key=True, primary_key=True,
@ -3380,7 +3368,7 @@ class EmailThread(InvenTree.models.InvenTreeMetadataModel):
blank=True, blank=True,
help_text=_('Unique key for this thread (used to identify the thread)'), help_text=_('Unique key for this thread (used to identify the thread)'),
) )
global_id = InvenTree.fields.InvenTreeUUIDField( global_id = models.UUIDField(
verbose_name=_('Global ID'), verbose_name=_('Global ID'),
help_text=_('Unique identifier for this thread'), help_text=_('Unique identifier for this thread'),
primary_key=True, primary_key=True,

View File

@ -417,14 +417,7 @@ class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSeria
"""Meta options for ProjectCodeSerializer.""" """Meta options for ProjectCodeSerializer."""
model = common_models.ProjectCode model = common_models.ProjectCode
fields = [ fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail']
'pk',
'code',
'description',
'active',
'responsible',
'responsible_detail',
]
responsible_detail = OwnerSerializer( responsible_detail = OwnerSerializer(
source='responsible', read_only=True, allow_null=True source='responsible', read_only=True, allow_null=True

View File

@ -793,14 +793,6 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'STOCK_MERGE_ON_TRANSFER': {
'name': _('Merge stock with existing stock on transfer by default'),
'description': _(
'Default state for merge stock on transfer behaviour. (Can be changed per transfer if desired)'
),
'default': False,
'validator': bool,
},
'BUILDORDER_REFERENCE_PATTERN': { 'BUILDORDER_REFERENCE_PATTERN': {
'name': _('Build Order Reference Pattern'), 'name': _('Build Order Reference Pattern'),
'description': _('Required pattern for generating Build Order reference field'), 'description': _('Required pattern for generating Build Order reference field'),

View File

@ -1,7 +1,19 @@
"""Types for settings.""" """Types for settings."""
import sys
from collections.abc import Callable from collections.abc import Callable
from typing import Any, NotRequired, TypedDict from typing import Any, TypedDict
if sys.version_info >= (3, 11):
from typing import NotRequired # pragma: no cover
else:
class NotRequired: # pragma: no cover
"""NotRequired type helper is only supported with Python 3.11+."""
def __class_getitem__(cls, item):
"""Return the item."""
return item
class SettingsKeyType(TypedDict, total=False): class SettingsKeyType(TypedDict, total=False):

View File

@ -142,3 +142,12 @@ def stock_expiry_enabled():
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY', False, create=False) return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY', False, create=False)
def prevent_build_output_complete_on_incompleted_tests():
"""Returns True if the completion of the build outputs is disabled until the required tests are passed."""
from common.models import InvenTreeSetting
return InvenTreeSetting.get_setting(
'PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS', False, create=False
)

View File

@ -1752,22 +1752,6 @@ class ProjectCodesTest(InvenTreeAPITestCase):
str(response.data['code']), str(response.data['code']),
) )
def test_filter_active(self):
"""Test that the 'active' field can be filtered via the API."""
# Mark one code as inactive
code = ProjectCode.objects.first()
code.active = False
code.save()
active_count = ProjectCode.objects.filter(active=True).count()
inactive_count = ProjectCode.objects.filter(active=False).count()
response = self.get(self.url, data={'active': True}, expected_code=200)
self.assertEqual(len(response.data), active_count)
response = self.get(self.url, data={'active': False}, expected_code=200)
self.assertEqual(len(response.data), inactive_count)
def test_write_access(self): def test_write_access(self):
"""Test that non-staff users have read-only access.""" """Test that non-staff users have read-only access."""
# By default user has staff access, can create a new project code # By default user has staff access, can create a new project code

View File

@ -1,6 +1,5 @@
"""JSON serializers for Company app.""" """JSON serializers for Company app."""
from django.db import transaction
from django.db.models import Prefetch from django.db.models import Prefetch
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -15,7 +14,6 @@ from importer.registry import register_importer
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import ( from InvenTree.serializers import (
DuplicateOptionsSerializer,
FilterableSerializerMixin, FilterableSerializerMixin,
InvenTreeCurrencySerializer, InvenTreeCurrencySerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
@ -120,8 +118,6 @@ class CompanySerializer(
import_exclude_fields = ['image'] import_exclude_fields = ['image']
SKIP_CREATE_FIELDS = ['duplicate']
class Meta: class Meta:
"""Metaclass options.""" """Metaclass options."""
@ -136,7 +132,6 @@ class CompanySerializer(
'email', 'email',
'currency', 'currency',
'contact', 'contact',
'duplicate',
'link', 'link',
'image', 'image',
'active', 'active',
@ -196,23 +191,6 @@ class CompanySerializer(
parameters = common.filters.enable_parameters_filter() parameters = common.filters.enable_parameters_filter()
duplicate = DuplicateOptionsSerializer(Company.objects.all(), copy_parameters=True)
@transaction.atomic
def create(self, validated_data):
"""Create a new Company instance, optionally copying data from an existing company."""
duplicate = validated_data.pop('duplicate', None)
instance = super().create(validated_data)
if duplicate:
original = duplicate['original']
if duplicate.get('copy_parameters', True):
instance.copy_parameters_from(original)
return instance
@register_importer() @register_importer()
class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer):
@ -239,8 +217,6 @@ class ManufacturerPartSerializer(
): ):
"""Serializer for ManufacturerPart object.""" """Serializer for ManufacturerPart object."""
SKIP_CREATE_FIELDS = ['duplicate']
class Meta: class Meta:
"""Metaclass options.""" """Metaclass options."""
@ -253,7 +229,6 @@ class ManufacturerPartSerializer(
'manufacturer', 'manufacturer',
'manufacturer_detail', 'manufacturer_detail',
'description', 'description',
'duplicate',
'MPN', 'MPN',
'link', 'link',
'barcode_hash', 'barcode_hash',
@ -266,25 +241,6 @@ class ManufacturerPartSerializer(
parameters = common.filters.enable_parameters_filter() parameters = common.filters.enable_parameters_filter()
duplicate = DuplicateOptionsSerializer(
ManufacturerPart.objects.all(), copy_parameters=True
)
@transaction.atomic
def create(self, validated_data):
"""Create a new ManufacturerPart instance, optionally copying data from an existing instance."""
duplicate = validated_data.pop('duplicate', None)
instance = super().create(validated_data)
if duplicate:
original = duplicate['original']
if duplicate.get('copy_parameters', True):
instance.copy_parameters_from(original)
return instance
part_detail = OptionalField( part_detail = OptionalField(
serializer_class=part_serializers.PartBriefSerializer, serializer_class=part_serializers.PartBriefSerializer,
serializer_kwargs={ serializer_kwargs={
@ -367,8 +323,6 @@ class SupplierPartSerializer(
export_exclude_fields = ['tags'] export_exclude_fields = ['tags']
SKIP_CREATE_FIELDS = ['duplicate']
export_child_fields = [ export_child_fields = [
'part_detail.name', 'part_detail.name',
'part_detail.description', 'part_detail.description',
@ -385,7 +339,6 @@ class SupplierPartSerializer(
'available', 'available',
'availability_updated', 'availability_updated',
'description', 'description',
'duplicate',
'in_stock', 'in_stock',
'on_order', 'on_order',
'link', 'link',
@ -541,10 +494,6 @@ class SupplierPartSerializer(
# Date fields # Date fields
updated = serializers.DateTimeField(allow_null=True, read_only=True) updated = serializers.DateTimeField(allow_null=True, read_only=True)
duplicate = DuplicateOptionsSerializer(
SupplierPart.objects.all(), copy_parameters=True
)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Annotate the SupplierPart queryset with extra fields. """Annotate the SupplierPart queryset with extra fields.
@ -573,11 +522,8 @@ class SupplierPartSerializer(
return response return response
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
"""Extract manufacturer data and process ManufacturerPart.""" """Extract manufacturer data and process ManufacturerPart."""
duplicate = validated_data.pop('duplicate', None)
# Extract 'available' quantity from the serializer # Extract 'available' quantity from the serializer
available = validated_data.pop('available', None) available = validated_data.pop('available', None)
@ -595,12 +541,6 @@ class SupplierPartSerializer(
kwargs = {'manufacturer': manufacturer, 'MPN': MPN} kwargs = {'manufacturer': manufacturer, 'MPN': MPN}
supplier_part.save(**kwargs) supplier_part.save(**kwargs)
if duplicate:
original = duplicate['original']
if duplicate.get('copy_parameters', True):
supplier_part.copy_parameters_from(original)
return supplier_part return supplier_part

View File

@ -16,7 +16,6 @@ from taggit.serializers import TagListSerializerField
import data_exporter.serializers import data_exporter.serializers
import data_exporter.tasks import data_exporter.tasks
import InvenTree.exceptions import InvenTree.exceptions
import InvenTree.serializers
from common.models import DataOutput from common.models import DataOutput
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
@ -65,14 +64,6 @@ class DataExportSerializerMixin:
# Exclude fields which are not required for data export # Exclude fields which are not required for data export
for field in self.get_export_exclude_fields(**kwargs): for field in self.get_export_exclude_fields(**kwargs):
self.fields.pop(field, None) self.fields.pop(field, None)
# Duplication options are never used for data export
for field in [
name
for name, field in self.fields.items()
if isinstance(field, InvenTree.serializers.DuplicateOptionsSerializer)
]:
self.fields.pop(field, None)
else: else:
# Exclude fields which are only used for data export # Exclude fields which are only used for data export
for field in self.get_export_only_fields(**kwargs): for field in self.get_export_only_fields(**kwargs):
@ -108,10 +99,6 @@ class DataExportSerializerMixin:
fields.update(self.get_child_fields(name, field)) fields.update(self.get_child_fields(name, field))
continue continue
# Skip 'many' fields (e.g. nested serializers)
if getattr(field, 'many', False):
continue
fields[name] = field fields[name] = field
return fields return fields

View File

@ -3,8 +3,6 @@
from rest_framework import fields, serializers from rest_framework import fields, serializers
from taggit.serializers import TagListSerializerField from taggit.serializers import TagListSerializerField
import InvenTree.serializers
class DataImportSerializerMixin: class DataImportSerializerMixin:
"""Mixin class for adding data import functionality to a DRF serializer.""" """Mixin class for adding data import functionality to a DRF serializer."""
@ -43,14 +41,6 @@ class DataImportSerializerMixin:
for field in self.get_import_exclude_fields(**kwargs): for field in self.get_import_exclude_fields(**kwargs):
self.fields.pop(field, None) self.fields.pop(field, None)
# Duplication options are never used for data import
for field in [
name
for name, field in self.fields.items()
if isinstance(field, InvenTree.serializers.DuplicateOptionsSerializer)
]:
self.fields.pop(field, None)
else: else:
# Exclude fields which are only used for data import # Exclude fields which are only used for data import
for field in self.get_import_only_fields(**kwargs): for field in self.get_import_only_fields(**kwargs):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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