Compare commits
45 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c56fe7f612 | |
|
|
5df2adae6d | |
|
|
f93c813bb4 | |
|
|
f656ac7263 | |
|
|
4895fbc9fe | |
|
|
4cb29f37c6 | |
|
|
15c64d6695 | |
|
|
5595a0a52b | |
|
|
2722d5450d | |
|
|
836a05a104 | |
|
|
5532946473 | |
|
|
66d1884ad9 | |
|
|
6865b0b6e6 | |
|
|
9594ba9a98 | |
|
|
609e5ee105 | |
|
|
09f85aeae9 | |
|
|
7a81aa216f | |
|
|
304c54530c | |
|
|
fb280d80e7 | |
|
|
6111aace1f | |
|
|
414aac0224 | |
|
|
635ea8179e | |
|
|
d00681e20c | |
|
|
f7e267cca8 | |
|
|
0470dc34b7 | |
|
|
12825aa1f4 | |
|
|
5b1a7db07f | |
|
|
25b83c7388 | |
|
|
2d1d5403d8 | |
|
|
1da71ca3b9 | |
|
|
56679e4b31 | |
|
|
5d581a35e4 | |
|
|
8f73d9291b | |
|
|
e847d96a88 | |
|
|
b99b80957f | |
|
|
39b74c7e87 | |
|
|
496b20e08f | |
|
|
e5fa67ca9f | |
|
|
3f36537391 | |
|
|
d1f91e8b4e | |
|
|
4cfefc18c1 | |
|
|
fa55917659 | |
|
|
de736ee5de | |
|
|
09f11a27d9 | |
|
|
7e59c92422 |
|
|
@ -1,9 +1,7 @@
|
||||||
# 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.
|
||||||
|
|
||||||
# In contrast with the "production" image (which is based on an Alpine image)
|
FROM mcr.microsoft.com/devcontainers/python:3.12-trixie@sha256:5440cb68898d190ad6c6e8a4634ce89d0645bea47f9c8beb75612bb8e3983711
|
||||||
# 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"
|
||||||
|
|
@ -25,10 +23,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.11-dev python3.11-venv \
|
python3-dev python3-venv \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
libldap2-dev libsasl2-dev \
|
libldap2-dev libsasl2-dev \
|
||||||
libpango1.0-0 libcairo2 \
|
libpango-1.0-0 libcairo2 \
|
||||||
poppler-utils weasyprint
|
poppler-utils weasyprint
|
||||||
|
|
||||||
# Install packages required for frontend development
|
# Install packages required for frontend development
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
trigger:
|
|
||||||
batch: true
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- master
|
|
||||||
- stable
|
|
||||||
- refs/tags/*
|
|
||||||
paths:
|
|
||||||
include:
|
|
||||||
- src/backend
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
Python39:
|
|
||||||
PYTHON_VERSION: '3.11'
|
|
||||||
maxParallel: 3
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: UsePythonVersion@0
|
|
||||||
inputs:
|
|
||||||
versionSpec: '$(PYTHON_VERSION)'
|
|
||||||
architecture: 'x64'
|
|
||||||
|
|
||||||
- task: PythonScript@0
|
|
||||||
displayName: 'Export project path'
|
|
||||||
inputs:
|
|
||||||
scriptSource: 'inline'
|
|
||||||
script: |
|
|
||||||
"""Search all subdirectories for `manage.py`."""
|
|
||||||
from glob import iglob
|
|
||||||
from os import path
|
|
||||||
# Python >= 3.5
|
|
||||||
manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None)
|
|
||||||
if not manage_py:
|
|
||||||
raise SystemExit('Could not find a Django project')
|
|
||||||
project_location = path.dirname(path.abspath(manage_py))
|
|
||||||
print('Found Django project in', project_location)
|
|
||||||
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
|
|
||||||
|
|
||||||
- script: |
|
|
||||||
python -m pip install --upgrade pip setuptools wheel uv
|
|
||||||
uv pip install --require-hashes -r src/backend/requirements.txt
|
|
||||||
uv pip install --require-hashes -r src/backend/requirements-dev.txt
|
|
||||||
sudo apt-get install poppler-utils
|
|
||||||
sudo apt-get install libpoppler-dev
|
|
||||||
uv pip install unittest-xml-reporting coverage invoke
|
|
||||||
displayName: 'Install prerequisites'
|
|
||||||
env:
|
|
||||||
UV_SYSTEM_PYTHON: 1
|
|
||||||
|
|
||||||
- script: |
|
|
||||||
pushd '$(projectRoot)'
|
|
||||||
invoke update --uv
|
|
||||||
coverage run manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
|
|
||||||
coverage xml -i
|
|
||||||
displayName: 'Run tests'
|
|
||||||
env:
|
|
||||||
INVENTREE_DB_ENGINE: sqlite3
|
|
||||||
INVENTREE_DB_NAME: inventree
|
|
||||||
INVENTREE_MEDIA_ROOT: ./media
|
|
||||||
INVENTREE_STATIC_ROOT: ./static
|
|
||||||
INVENTREE_BACKUP_DIR: ./backup
|
|
||||||
INVENTREE_SITE_URL: http://localhost:8000
|
|
||||||
INVENTREE_PLUGINS_ENABLED: true
|
|
||||||
UV_SYSTEM_PYTHON: 1
|
|
||||||
INVENTREE_DEBUG: true
|
|
||||||
INVENTREE_LOG_LEVEL: INFO
|
|
||||||
|
|
||||||
- task: PublishTestResults@2
|
|
||||||
inputs:
|
|
||||||
testResultsFiles: "**/TEST-*.xml"
|
|
||||||
testRunTitle: 'Python $(PYTHON_VERSION)'
|
|
||||||
condition: succeededOrFailed()
|
|
||||||
|
|
||||||
- task: PublishCodeCoverageResults@2
|
|
||||||
inputs:
|
|
||||||
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
|
|
||||||
|
|
@ -39,14 +39,14 @@ runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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 # pin@v5.0.0
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # 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 # pin@v5.0.0
|
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # 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
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
)
|
)
|
||||||
steps:
|
steps:
|
||||||
- name: Backport Action
|
- name: Backport Action
|
||||||
uses: sqren/backport-github-action@ad888e978060bc1b2798690dd9d03c4036560947 # pin@v9.2.2
|
uses: sorenlouv/backport-github-action@8a6c0381851f43f9f1fddc7303f0e9015eb57b62 # v12.0.4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
auto_backport_label_prefix: backport-to-
|
auto_backport_label_prefix: backport-to-
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ on:
|
||||||
- l10
|
- l10
|
||||||
|
|
||||||
env:
|
env:
|
||||||
python_version: 3.11
|
python_version: 3.12
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -31,7 +31,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ jobs:
|
||||||
docker: ${{ steps.filter.outputs.docker }}
|
docker: ${{ steps.filter.outputs.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # 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.11"
|
python_version: 3.12
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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.11"
|
python_version: 3.12
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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.11"
|
python_version: 3.12
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 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 # pin@v4.1.0
|
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # 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 # pin@v4.1.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # 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 # pin@v4.1.2
|
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # 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 # pin@v4.2.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # 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 # pin@v4.2.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # 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 # pin@v6.1.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
inventree/inventree
|
inventree/inventree
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1
|
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # 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 # pin@v1
|
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1
|
||||||
with:
|
with:
|
||||||
project: jczzbjkk68
|
project: jczzbjkk68
|
||||||
context: .
|
context: .
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ 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.11
|
python_version: 3.12
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -45,10 +46,10 @@ jobs:
|
||||||
force: ${{ steps.force.outputs.force }}
|
force: ${{ steps.force.outputs.force }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
|
@ -68,7 +69,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -85,7 +86,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 # pin@v7.0.1
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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
|
||||||
|
|
@ -122,7 +123,7 @@ jobs:
|
||||||
VITE_COVERAGE: false
|
VITE_COVERAGE: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -140,7 +141,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
|
|
@ -153,7 +154,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
|
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
|
||||||
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
|
||||||
|
|
@ -164,7 +165,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 # pin@v7.0.1
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 }}
|
||||||
|
|
@ -204,7 +205,7 @@ jobs:
|
||||||
VITE_COVERAGE: true
|
VITE_COVERAGE: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -222,7 +223,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@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
|
|
@ -235,7 +236,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
|
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
|
||||||
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]
|
||||||
|
|
@ -244,7 +245,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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 }}
|
||||||
|
|
@ -252,7 +253,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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
id: coverage-upload
|
id: coverage-upload
|
||||||
if: ${{ !cancelled() && steps.tests.outcome != 'failure' }}
|
if: ${{ !cancelled() && steps.tests.outcome != 'failure' }}
|
||||||
with:
|
with:
|
||||||
|
|
@ -272,7 +273,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
@ -284,7 +285,7 @@ jobs:
|
||||||
update: false
|
update: false
|
||||||
|
|
||||||
- name: Download Coverage Artifacts
|
- name: Download Coverage Artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
path: all-coverage/
|
path: all-coverage/
|
||||||
|
|
@ -303,7 +304,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 # pin@v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: inventree/InvenTree
|
slug: inventree/InvenTree
|
||||||
|
|
|
||||||
|
|
@ -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.11
|
python_version: 3.12
|
||||||
|
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
|
||||||
|
|
@ -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.11
|
python_version: 3.12
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 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@bdca6f102f98e2b4c7029491a53dfd366469e33d # pin@v2
|
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
|
||||||
- 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@a309ff8b426b58ec0e2a45f0f869d46889d02405 # pin@v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 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 # pin@v1
|
uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
|
uses: oasdiff/oasdiff-action/diff@3e9d440d37f468355457604348009f50e0cddbf3 # v0.1.4
|
||||||
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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 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@04702edda442b2e678b25b537cec683a1493fcb9 # pin@v7.1.0
|
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
uses: CodSpeedHQ/action@63f3e98b61959fe67f146a3ff022e4136fe9bb9c # v4.17.6
|
||||||
# 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.11, 3.14]
|
python_version: [3.12, 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -409,13 +409,13 @@ jobs:
|
||||||
- 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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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 # pin@v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
@ -441,7 +441,7 @@ jobs:
|
||||||
INVENTREE_AUTO_UPDATE: true
|
INVENTREE_AUTO_UPDATE: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -454,7 +454,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
node_version: '>=24'
|
node_version: '>=24'
|
||||||
- name: Performance Reporting
|
- name: Performance Reporting
|
||||||
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
|
uses: CodSpeedHQ/action@63f3e98b61959fe67f146a3ff022e4136fe9bb9c # v4.17.6
|
||||||
with:
|
with:
|
||||||
mode: walltime
|
mode: walltime
|
||||||
run: inv dev.test --pytest
|
run: inv dev.test --pytest
|
||||||
|
|
@ -492,7 +492,7 @@ jobs:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -541,7 +541,7 @@ jobs:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -584,7 +584,7 @@ jobs:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Environment Setup
|
- name: Environment Setup
|
||||||
|
|
@ -597,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 # pin@v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
@ -618,7 +618,7 @@ jobs:
|
||||||
INVENTREE_PLUGINS_ENABLED: false
|
INVENTREE_PLUGINS_ENABLED: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
name: Checkout Code
|
name: Checkout Code
|
||||||
|
|
@ -674,8 +674,8 @@ jobs:
|
||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
env:
|
env:
|
||||||
python_version: 3.11
|
python_version: 3.12
|
||||||
|
|
||||||
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v1.3.0
|
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v0
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # 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@59d89421af93a897026c735860bf21b6eb4f7b26 # pin@v4
|
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
|
||||||
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 # pin@v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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
|
||||||
- debian:12
|
- ubuntu:26.04
|
||||||
|
- debian:13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 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 # pin@main
|
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # 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@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
|
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # 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 # pin@main
|
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # 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@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
|
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
|
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # pin@v10.3.0
|
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # 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."
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ on:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
python_version: 3.11
|
python_version: 3.12
|
||||||
node_version: 24
|
node_version: 24
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
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 # pin@v2
|
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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 # pin@v4.15.1
|
- uses: stefanzweifel/git-auto-commit-action@fd157da78fa13d9383e5580d1fd1184d89554b51 # v4.15.1
|
||||||
with:
|
with:
|
||||||
commit_message: "[Bot] Updated dependency"
|
commit_message: "[Bot] Updated dependency"
|
||||||
branch: dep-update
|
branch: dep-update
|
||||||
|
|
|
||||||
10
.pkgr.yml
10
.pkgr.yml
|
|
@ -21,9 +21,9 @@ before:
|
||||||
dependencies:
|
dependencies:
|
||||||
- curl
|
- curl
|
||||||
- poppler-utils
|
- poppler-utils
|
||||||
- "python3.11 | python3.12 | python3.13 | python3.14"
|
- "python3.12 | python3.13 | python3.14"
|
||||||
- "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
|
- "python3.12-venv | python3.13-venv | python3.14-venv"
|
||||||
- "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-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
|
||||||
debian-12: true
|
ubuntu-26.04: true
|
||||||
|
debian-13: true
|
||||||
|
|
|
||||||
|
|
@ -97,4 +97,4 @@ repos:
|
||||||
rev: 0.4.3
|
rev: 0.4.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: teyit
|
- id: teyit
|
||||||
language_version: python3.11
|
language_version: python3.12
|
||||||
|
|
|
||||||
|
|
@ -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.11/site-packages/invoke/__main__.py",
|
"program": "${workspaceFolder}/.venv/lib/python3.12/site-packages/invoke/__main__.py",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"args": [
|
"args": [
|
||||||
"dev.schema","--ignore-warnings"
|
"dev.schema","--ignore-warnings"
|
||||||
|
|
|
||||||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -5,7 +5,23 @@ 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 - YYYY-MM-DD
|
## Unreleased - xxxx.xx.xx
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [#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
|
||||||
|
|
||||||
|
- [#12250](https://github.com/inventree/InvenTree/pull/12250) adds "active" field to the ProjectCode model and API endpoints
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- [#12274](https://github.com/inventree/InvenTree/pull/12274) fixes a bug in rendering the "table field" component in frontend forms. Any plugins which make use of the "table field" component in their forms may be affected by this change, and will need to update their form definitions accordingly.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
## 1.4.0 - 2026-06-24
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
[](https://inventree.readthedocs.io/en/latest/?badge=latest)
|
[](https://inventree.readthedocs.io/en/latest/?badge=latest)
|
||||||

|

|
||||||
[](https://app.netlify.com/sites/inventree/deploys)
|
[](https://app.netlify.com/sites/inventree/deploys)
|
||||||
[](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_build/latest?definitionId=3&branchName=testing)
|
|
||||||
|
|
||||||
[](https://bestpractices.coreinfrastructure.org/projects/7179)
|
[](https://bestpractices.coreinfrastructure.org/projects/7179)
|
||||||
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
|
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
|
||||||
|
|
@ -202,6 +201,7 @@ 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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ flag_management:
|
||||||
carryforward: true
|
carryforward: true
|
||||||
statuses:
|
statuses:
|
||||||
- type: project
|
- type: project
|
||||||
target: 40%
|
target: 38%
|
||||||
- name: web
|
- name: web
|
||||||
carryforward: true
|
carryforward: true
|
||||||
statuses:
|
statuses:
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,8 @@ 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
|
||||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash && \
|
# pinned to v0.40.5
|
||||||
|
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"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
# Base python requirements for docker containers
|
# Base python requirements for docker containers
|
||||||
|
|
||||||
# Basic package requirements
|
# Basic package requirements
|
||||||
invoke>=2.2.0 # Invoke build tool
|
invoke # Invoke build tool
|
||||||
pyyaml>=6.0.1
|
pyyaml
|
||||||
setuptools>=69.0.0
|
setuptools
|
||||||
wheel>=0.41.0
|
wheel
|
||||||
|
|
||||||
# Database links
|
# Database links
|
||||||
psycopg[binary, pool]
|
psycopg[binary, pool]
|
||||||
mysqlclient>=2.2.0
|
mysqlclient
|
||||||
mariadb>=1.1.8
|
mariadb
|
||||||
|
|
||||||
# gunicorn web server
|
# gunicorn web server
|
||||||
gunicorn>=22.0.0
|
gunicorn
|
||||||
|
|
||||||
# LDAP required packages
|
# LDAP required packages
|
||||||
django-auth-ldap # Django integration for ldap auth
|
django-auth-ldap # Django integration for ldap auth
|
||||||
|
|
|
||||||
|
|
@ -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.23 \
|
uv==0.11.24 \
|
||||||
--hash=sha256:03fbb0a1c7b6d15e96778bdd79e8d1826c6259fea17fc13337fb0744136953f2 \
|
--hash=sha256:047d763d20d71968c00f4afec40b0e75d9da7e3693f725b9f502d84a25256893 \
|
||||||
--hash=sha256:3a900c8fd757f8c3da9dc5532d9a22d30540e91fdefd63c93909fedbfd756655 \
|
--hash=sha256:0e100b9cbc59beff2730840ac989b1f100cc03c90514e7cab6992a1f3f13784e \
|
||||||
--hash=sha256:576d776a1ca62e3d8aba99f0d8ec607db91a5ebaf52feaff820f28ed820e1665 \
|
--hash=sha256:3176668ea8a2318d775c0d9188661c61ccc36790220724b1d360fcc8945d520e \
|
||||||
--hash=sha256:61e6bd7e7f0fe24f103540ba19516443bea6e689022c787217310a1e64558e3f \
|
--hash=sha256:356435577fae11fafe7a067ee3269cccefd35b031b83a3a36c87fe9d192bffba \
|
||||||
--hash=sha256:6d7cea7d9ade3c1c3e3db1dfcc23d335bceaabf38f51e442b6f57f8f7885a9a6 \
|
--hash=sha256:38f38c9449aafd71dc0fa35e66a8a547ee48947b2f2f94084893c2afe6058cfa \
|
||||||
--hash=sha256:7a85330de0a7eb0d5c6cf03c80edfb86facad19df367a0b52fc906db1ab15ce9 \
|
--hash=sha256:438f8291fb9daea30a4bd333299b82e6ef755578302745a021162f328cfcfc68 \
|
||||||
--hash=sha256:91d19c4249d7437b69b91c385134360d7ed9d0ee2e2e83e81d369867151e78c2 \
|
--hash=sha256:48a6123f71b801e0e0b8a38520b011632ad81e0a043445044ce5b1a7b1cec7b6 \
|
||||||
--hash=sha256:985aa93c9d6223e32fc747e09662537c4073c9ebef59c0a4fd7c6949d1d24fb3 \
|
--hash=sha256:6ecdad43e870f88d3772d9d37e877259ae35ec374d51589805cdcf6196205829 \
|
||||||
--hash=sha256:9dd412127cbe0e115bd3fc5c6cbe9cf59f593273fafab9f7dc6b2ac95efcc7c1 \
|
--hash=sha256:79757f90b7765996366b80e77cad13555c7f7e1282ca8d8b686ee21773ab0b77 \
|
||||||
--hash=sha256:ac337dacd640aab1ef97cf00f8c01e2889f0fd0ef8460a0f4e816bf12bb5988b \
|
--hash=sha256:8346b0086d213b80430f3bb4477379a8a4fa550b03447d23c84eee85c2e52bc4 \
|
||||||
--hash=sha256:b3f515fd6b43068f241467496bced62cb2ed36d52d4c0877cfe61a1240713d32 \
|
--hash=sha256:8602a1b6300a3a948afacc62e1cb933c8394c27966db85ed7e29483300b69dc4 \
|
||||||
--hash=sha256:b8abe7d6f5e0d92bd41a9c000bbd9c8387af7886df4790c0451a34e781b8a075 \
|
--hash=sha256:9312b6fd44361674e9c847a828ded792493766697816261e05848a024fe34129 \
|
||||||
--hash=sha256:bbc41182d655f92cd380ecdf378da7fc1598c6b19057208f450f0ee9c259f46a \
|
--hash=sha256:bf568c62dddd55ad9c85a16ffdfbcc7746be9c3159ed644e4f9e6f5e44905cbb \
|
||||||
--hash=sha256:c2089b992919858dabae89d410cbb5cecf9034d26bbb04f14e6da52dffced290 \
|
--hash=sha256:c4ab221c0a949f8006a7582786dae384706b2f82016a0db60baa7cc696042705 \
|
||||||
--hash=sha256:ca1d37e851fb9323250385403d8512a71c0d1b6162c729ff4909f37cfd067920 \
|
--hash=sha256:c8ec3caf656645f58b53cb9aee9aa95cfc65c82ba2d7f1362bfd2660d1484307 \
|
||||||
--hash=sha256:d256f90513d01ff6cbc2f17d88c0ccde65d138500df547ece214e6a50731c4b7 \
|
--hash=sha256:d6a49543c659c0cf1ff4c944955d97e69dbce4b76e3d284f5a92cea19133ebb6 \
|
||||||
--hash=sha256:d62410e5f60a961cfda00ead8a1cc5fd37d052afda021099e488e90c15419beb \
|
--hash=sha256:e499579f557abf17b8ffd1490d3e827748aea096f6eaa66737e729e046449f08 \
|
||||||
--hash=sha256:e7e215d69ea21fd5824a63edf8fef933bee2c028a0c2930651cfa6b88ca4ff8e \
|
--hash=sha256:e7e78c18686202c8b8715bebb83bfaf58f82d7fb848b6a5ae4e925a9fac3de4c \
|
||||||
--hash=sha256:f2476dda35866ea3ded3a5905759da2d32dfac36dfd5b3428191a99a8ce15b02
|
--hash=sha256:ed0c9a9d7909f0e48a9dafe666ca9ebefe2a1534e51ed05c0a7de7406465f868
|
||||||
# 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 \
|
||||||
|
|
|
||||||
|
|
@ -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==1.25.6
|
jc
|
||||||
|
|
|
||||||
|
|
@ -145,9 +145,9 @@ idna==3.18 \
|
||||||
# via
|
# via
|
||||||
# -c src/backend/requirements.txt
|
# -c src/backend/requirements.txt
|
||||||
# requests
|
# requests
|
||||||
jc==1.25.6 \
|
jc==1.25.7 \
|
||||||
--hash=sha256:27f58befc7ae0a4c63322926c5f1ec892e3eac4a065eff3b07cfe420a6924a07 \
|
--hash=sha256:5ed6a0da915c931c04693cab806d5c31d9ef14ca998c6c32308460d37cb30baf \
|
||||||
--hash=sha256:7367b59e6e0da8babeede1e5b0da083f3c5aa6b6e585b4aed28dd7c4b2d76162
|
--hash=sha256:63481e34d92715781e1b094eca7d76d122a14489463d45b7f3e8180f44ea3a10
|
||||||
# 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 \
|
||||||
|
|
|
||||||
|
|
@ -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.11'."
|
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 "${Color_Off}"
|
echo "${Color_Off}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export DATA_DIR=${APP_HOME}/data
|
||||||
export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
|
export SETUP_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.11}
|
export SETUP_PYTHON=${SETUP_PYTHON:-python3.12}
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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: StrictVersion(x['version']), reverse=True)
|
versions = sorted(versions, key=lambda x: Version(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):
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,57 @@ 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.
|
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.
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
Additionally the `add_label_context` method, allowing custom context data to be added to a label template at time of printing.
|
Similarly, the `add_label_context` method allows custom context data to be added to a label template at time of printing:
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `label_instance` | The label template instance which is being rendered |
|
||||||
|
| `model_instance` | The model instance against which the label is being generated |
|
||||||
|
| `user` | The user who initiated the label generation |
|
||||||
|
| `context` | The context dictionary, which can be modified in-place |
|
||||||
|
|
||||||
|
### Report Callback
|
||||||
|
|
||||||
|
The `report_callback` method is called after a report has been generated, and allows the plugin to perform custom actions with the generated report - for example, forwarding the report to an external system, or performing custom post-processing.
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `template` | The report template instance which was used to generate the report |
|
||||||
|
| `instance` | The model instance against which the report was generated |
|
||||||
|
| `report` | The generated report (PDF file data) |
|
||||||
|
| `user` | The user who initiated the report generation |
|
||||||
|
|
||||||
|
```python
|
||||||
|
def report_callback(self, template, instance, report, user, **kwargs):
|
||||||
|
"""Custom callback function - called after a report is generated."""
|
||||||
|
# For example, forward the generated report to an external service
|
||||||
|
self.upload_to_external_service(report)
|
||||||
|
```
|
||||||
|
|
||||||
### Sample Plugin
|
### Sample Plugin
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
title: Report Assets
|
||||||
|
---
|
||||||
|
|
||||||
|
## Report Assets
|
||||||
|
|
||||||
|
Users can upload asset files (e.g. images) which can be used when generating reports. For example, you may wish to generate a report with your company logo in the header.
|
||||||
|
|
||||||
|
Asset files are managed from the [Admin Center](../settings/admin.md#admin-center), via the *Report Assets* panel. Staff users can upload new asset files, and remove assets which are no longer required.
|
||||||
|
|
||||||
|
Asset files can be rendered directly into the template as follows
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% raw %}
|
||||||
|
<!-- Need to include the report template tags at the start of the template file -->
|
||||||
|
{% load report %}
|
||||||
|
|
||||||
|
<!-- Simple stylesheet -->
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.company-logo {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Report template code here -->
|
||||||
|
|
||||||
|
<!-- Render an uploaded asset image -->
|
||||||
|
<img src="{% asset 'company_image.png' %}" class="company-logo">
|
||||||
|
|
||||||
|
<!-- ... -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Asset Naming"
|
||||||
|
If the requested asset name does not match the name of an uploaded asset, the template will continue without loading the image.
|
||||||
|
|
||||||
|
!!! info "Assets location"
|
||||||
|
Upload new assets via the *Report Assets* panel in the [Admin Center](../settings/admin.md#admin-center) to ensure they are uploaded to the correct location on the server.
|
||||||
|
|
||||||
|
There are various [helper functions](./helpers.md#report-assets) available to assist with embedding assets into templates.
|
||||||
|
|
@ -69,12 +69,14 @@ Templates (whether for generating [reports](./report.md) or [labels](./labels.md
|
||||||
|
|
||||||
| Model Type | Description |
|
| Model Type | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| company | A Company instance |
|
| [company](#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 |
|
||||||
|
|
@ -141,6 +143,16 @@ 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:
|
||||||
|
|
@ -173,7 +185,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 accessedd 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 accessed 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.
|
||||||
|
|
||||||
|
|
@ -187,7 +199,6 @@ 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
|
||||||
|
|
@ -244,7 +255,6 @@ 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 |
|
||||||
|
|
@ -263,7 +273,6 @@ 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) |
|
||||||
|
|
@ -353,7 +362,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 |
|
||||||
| fist_name | The first name of the user |
|
| first_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 |
|
||||||
|
|
|
||||||
|
|
@ -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](./index.md#report-assets) are files specifically uploaded by the user for inclusion in generated reports and labels.
|
[Report Assets](./assets.md) 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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,29 +53,43 @@ To read more about the capabilities of the report templating engine, and how to
|
||||||
|
|
||||||
## Creating Templates
|
## Creating Templates
|
||||||
|
|
||||||
Report and label templates can be created (and edited) via the [admin interface](../settings/admin.md), under the *Report* section.
|
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:
|
||||||
|
|
||||||
Select the type of template you are wanting to create (a *Report Template* or *Label Template*) and press the *Add* button in the top right corner:
|
- **Label Templates** - Create and edit [label templates](./labels.md)
|
||||||
|
- **Report Templates** - Create and edit [report templates](./report.md)
|
||||||
|
- **Report Snippets** - Manage reusable [snippet](#report-snippets) files
|
||||||
|
- **Report Assets** - Manage uploaded [asset](#report-assets) files
|
||||||
|
|
||||||
{{ image("report/report_template_admin.png", "Report template admin") }}
|
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.
|
||||||
|
|
||||||
!!! tip "Staff Access Only"
|
!!! tip "Staff Access Only"
|
||||||
Only users with staff access can upload or edit report template files.
|
Only users with staff access can create, upload or edit templates, snippets and assets.
|
||||||
|
|
||||||
!!! info "Editing Reports"
|
!!! info "Backend Admin Interface"
|
||||||
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.
|
Templates can also be managed at a lower level via the [backend admin interface](../settings/admin.md#backend-admin-interface), under the *Report* section. This is recommended for advanced users only.
|
||||||
|
|
||||||
!!! 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".
|
||||||
|
|
@ -147,89 +161,15 @@ 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. Asset files are uploaded via the admin interface.
|
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 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
|
||||||
|
|
||||||
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.
|
InvenTree provides report "snippets" - reusable template files which cannot be rendered by themselves, but can be included in other 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.
|
Refer to the [report snippets](./snippets.md) documentation for further information.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -243,7 +183,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 blocked - see *Remote URL Fetching* below |
|
| `http` / `https` | Disabled by default, but can be enabled - 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.
|
||||||
|
|
@ -259,6 +199,6 @@ When enabled, URLs are still validated against private, loopback, link-local, an
|
||||||
|
|
||||||
### Asset Files
|
### Asset Files
|
||||||
|
|
||||||
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.
|
[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.
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,25 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,51 @@
|
||||||
---
|
---
|
||||||
title: Report and Label Generation
|
title: Report Templates
|
||||||
---
|
---
|
||||||
|
|
||||||
## Custom Reports
|
## Report Templates
|
||||||
|
|
||||||
InvenTree supports a customizable reporting ecosystem, allowing the user to develop document templates that meet their particular needs.
|
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).
|
||||||
|
|
||||||
PDF files are generated from custom HTML template files which are written by the user.
|
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:
|
||||||
|
|
||||||
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 overview](./index.md) for information on creating and managing templates.
|
||||||
|
- Refer to the [template rendering documentation](./weasyprint.md) for information on the WeasyPrint engine and the django template language.
|
||||||
|
- Refer to the [context variables documentation](./context_variables.md) for the variables available when rendering a template.
|
||||||
|
|
||||||
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.
|
## Report Options
|
||||||
|
|
||||||
### WeasyPrint Templates
|
In addition to the [options common to all templates](./index.md#creating-templates), each report template provides the following options:
|
||||||
|
|
||||||
InvenTree report templates utilize the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine.
|
### Page Size
|
||||||
|
|
||||||
!!! info "WeasyPrint"
|
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.
|
||||||
WeasyPrint is an extremely powerful and flexible reporting library. Refer to the [WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/) for further information.
|
|
||||||
|
|
||||||
### Stylesheets
|
### Landscape
|
||||||
|
|
||||||
Templates are rendered using standard HTML / CSS - if you are familiar with web page layout, you're ready to go!
|
If enabled, the report is rendered in landscape orientation, rather than the default portrait orientation.
|
||||||
|
|
||||||
### Template Language
|
### Merge
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Variables
|
!!! info "Context Variables"
|
||||||
|
The `page_size`, `landscape` and `merge` values are also made available to the template as [context variables](./context_variables.md#report-context).
|
||||||
|
|
||||||
Each report template is provided a set of *context variables* which can be used when rendering the template.
|
## Generating Reports
|
||||||
|
|
||||||
For example, rendering the name of a part (which is available in the particular template context as `part`) is as follows:
|
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:
|
||||||
|
|
||||||
```html
|
1. Select the items to print - either from a table (using the row checkboxes), or by viewing the detail page of a single item
|
||||||
{% raw %}
|
2. Select the *Print* action, and choose the *Print Report* option
|
||||||
|
3. Select the desired report template - only *enabled* templates which match the selected model type (and pass any [template filters](./index.md#template-filters)) are available for selection
|
||||||
|
4. The report is generated by the server, and the resulting PDF file is made available for download
|
||||||
|
|
||||||
<!-- Template variables use {{ double_curly_braces }} -->
|
!!! info "Enable Reports"
|
||||||
<h2>Part: {{ part.name }}</h3>
|
Report generation must be [enabled in the global settings](./index.md#enable-reports) before reports can be generated.
|
||||||
<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 templeate 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 template 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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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 |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,6 +58,10 @@ 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") }}
|
||||||
|
|
@ -68,9 +73,11 @@ 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 |
|
| [Part](#part-label) | [Part](../part/index.md) | Part label (QR code and part name) |
|
||||||
|
| [Part (Code128)](#part-label-code128) | [Part](../part/index.md) | Part label (Code128 barcode) |
|
||||||
| [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 |
|
| [Stock Location](#stock-location-label) | [StockLocation](../stock/index.md#stock-location) | Stock Location label (QR code) |
|
||||||
|
| [Stock Location (with text)](#stock-location-label-with-text) | [StockLocation](../stock/index.md#stock-location) | Stock Location label (QR code and location details) |
|
||||||
|
|
||||||
### Build Line Label
|
### Build Line Label
|
||||||
|
|
||||||
|
|
@ -78,6 +85,10 @@ 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
|
||||||
|
|
@ -86,4 +97,8 @@ 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") }}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
title: Report Snippets
|
||||||
|
---
|
||||||
|
|
||||||
|
## Report Snippets
|
||||||
|
|
||||||
|
A powerful feature provided by the django / WeasyPrint templating framework is the ability to include external template files. This allows commonly used template features to be broken out into separate files and reused across multiple templates.
|
||||||
|
|
||||||
|
To support this, InvenTree provides report "snippets" - short (or not so short) template files which cannot be rendered by themselves, but can be called from other templates.
|
||||||
|
|
||||||
|
Snippet files are managed from the [Admin Center](../settings/admin.md#admin-center), via the *Report Snippets* panel. Staff users can upload new snippet files, and edit or remove existing snippets.
|
||||||
|
|
||||||
|
Additionally, the content of an existing snippet can be modified directly within the browser - simply select a snippet from the table to open it in the built-in [template editor](./template_editor.md#editing-snippets).
|
||||||
|
|
||||||
|
Snippets are included in a template as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{% raw %}{% include 'snippets/<snippet_name.html>' %}{% endraw %}
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, consider a custom stocktake report for a particular stock location, where we wish to render a table with a row for each item in that location.
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% raw %}
|
||||||
|
|
||||||
|
<table class='stock-table'>
|
||||||
|
<thead>
|
||||||
|
<!-- table header data -->
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in location.stock_items %}
|
||||||
|
{% include 'snippets/stock_row.html' with item=item %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info "Snippet Arguments"
|
||||||
|
Note above that named argument variables can be passed through to the snippet!
|
||||||
|
|
||||||
|
And the snippet file `stock_row.html` may be written as follows:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% raw %}
|
||||||
|
<!-- stock_row snippet -->
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.part.full_name }}</td>
|
||||||
|
<td>{{ item.quantity }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endraw %}
|
||||||
|
```
|
||||||
|
|
@ -4,11 +4,11 @@ title: Template editor
|
||||||
|
|
||||||
## Template editor
|
## Template editor
|
||||||
|
|
||||||
The Template Editor is integrated into the [Admin Center](../settings//admin.md#admin-center) of the Web UI. It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
|
The Template Editor is integrated into the [Admin Center](../settings/admin.md#admin-center) of the Web UI. It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
On the left side (1) are all possible 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 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,3 +31,10 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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.11-alpine3.18 as prebuild
|
FROM python:3.12-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 && \
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,8 @@ 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
|
||||||
|
|
@ -366,7 +368,7 @@ extra:
|
||||||
# provider: google
|
# provider: google
|
||||||
# property: UA-143467500-1
|
# property: UA-143467500-1
|
||||||
|
|
||||||
min_python_version: 3.11
|
min_python_version: 3.12
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "https://opensource.org/license/MIT"
|
"pattern": "https://opensource.org/license/MIT"
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "^https://dev.azure.com"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ skip-magic-trailing-comma = true
|
||||||
line-ending = "auto"
|
line-ending = "auto"
|
||||||
|
|
||||||
[tool.uv.pip]
|
[tool.uv.pip]
|
||||||
python-version = "3.11"
|
python-version = "3.12"
|
||||||
no-strip-extras=true
|
no-strip-extras=true
|
||||||
generate-hashes=true
|
generate-hashes=true
|
||||||
|
|
||||||
|
|
@ -105,17 +105,16 @@ generate-hashes=true
|
||||||
extra-paths = ["src/backend/InvenTree"]
|
extra-paths = ["src/backend/InvenTree"]
|
||||||
|
|
||||||
[tool.ty.rules]
|
[tool.ty.rules]
|
||||||
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
|
unresolved-attribute="ignore" # 837 # need Plugin Mixin typing
|
||||||
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
|
|
||||||
call-non-callable="ignore" # 8 ##
|
call-non-callable="ignore" # 8 ##
|
||||||
invalid-assignment="ignore" # 17 # need to wait for better django field stubs
|
invalid-assignment="ignore" # 40 # need to wait for better django field stubs
|
||||||
invalid-method-override="ignore" # 104
|
invalid-method-override="ignore" # 103
|
||||||
invalid-return-type="ignore" # 22 ##
|
invalid-return-type="ignore" # 44 ##
|
||||||
# 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" # 3 # need to wait for betterdjango field stubs
|
no-matching-overload="ignore" # 10 # 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"]
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ python:
|
||||||
- requirements: src/backend/requirements.txt
|
- requirements: src/backend/requirements.txt
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: "ubuntu-22.04"
|
os: "ubuntu-24.04"
|
||||||
tools:
|
tools:
|
||||||
python: "3.11"
|
python: "3.12"
|
||||||
jobs:
|
jobs:
|
||||||
post_install:
|
post_install:
|
||||||
- pip install -U invoke
|
- pip install -U invoke
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,27 @@
|
||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 511
|
INVENTREE_API_VERSION = 515
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ 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.
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -71,6 +73,9 @@ 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""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
|
||||||
|
|
@ -59,6 +60,34 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ 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
|
||||||
|
|
@ -46,6 +47,20 @@ 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__]
|
||||||
|
|
|
||||||
|
|
@ -600,7 +600,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
Default implementation returns an empty list
|
Default implementation returns an empty list
|
||||||
"""
|
"""
|
||||||
return []
|
return getattr(self, 'SKIP_CREATE_FIELDS', [])
|
||||||
|
|
||||||
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."""
|
||||||
|
|
@ -921,3 +921,104 @@ 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', ''),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -1817,6 +1818,35 @@ 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."""
|
||||||
|
|
|
||||||
|
|
@ -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.4.0 dev'
|
INVENTREE_SW_VERSION = '1.5.0 dev'
|
||||||
|
|
||||||
# Minimum supported Python version
|
# Minimum supported Python version
|
||||||
MIN_PYTHON_VERSION = (3, 11)
|
MIN_PYTHON_VERSION = (3, 12)
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,7 @@ 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 (
|
from common.settings import get_global_setting
|
||||||
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
|
||||||
|
|
@ -1093,6 +1090,61 @@ 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,
|
||||||
|
|
@ -1118,52 +1170,20 @@ 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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if prevent_on_incomplete and not output.passedAllRequiredTests(
|
self.can_complete_output(
|
||||||
required_tests=required_tests
|
output, quantity=quantity, 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)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""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
|
||||||
|
|
@ -22,7 +24,6 @@ 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
|
||||||
|
|
@ -33,6 +34,7 @@ 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,
|
||||||
|
|
@ -52,6 +54,7 @@ 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(
|
||||||
|
|
@ -65,6 +68,8 @@ class BuildSerializer(
|
||||||
):
|
):
|
||||||
"""Serializes a Build object."""
|
"""Serializes a Build object."""
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Serializer metaclass."""
|
"""Serializer metaclass."""
|
||||||
|
|
||||||
|
|
@ -78,6 +83,7 @@ class BuildSerializer(
|
||||||
'completed',
|
'completed',
|
||||||
'completion_date',
|
'completion_date',
|
||||||
'destination',
|
'destination',
|
||||||
|
'duplicate',
|
||||||
'external',
|
'external',
|
||||||
'parent',
|
'parent',
|
||||||
'part',
|
'part',
|
||||||
|
|
@ -188,12 +194,29 @@ 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
|
||||||
|
|
@ -260,11 +283,19 @@ 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,
|
||||||
|
|
@ -292,6 +323,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -527,7 +562,9 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = BuildOutputQuantitySerializer(many=True, required=True)
|
outputs = BuildOutputQuantitySerializer(
|
||||||
|
many=True, required=True, output_validator=check_build_output
|
||||||
|
)
|
||||||
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=StockLocation.objects.all(),
|
queryset=StockLocation.objects.all(),
|
||||||
|
|
@ -554,30 +591,6 @@ 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'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
from part.models import BomItem, BomItemSubstitute, Part, PartTestTemplate
|
||||||
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,10 +1531,15 @@ class BuildOutputScrapTest(BuildAPITest):
|
||||||
'notes': 'Partial complete',
|
'notes': 'Partial complete',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure that an invalid quantity raises an error
|
# Ensure that an invalid quantity raises an error, with the expected message
|
||||||
for q in [-4, 0, 999]:
|
for q, expected_message in [
|
||||||
|
(-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
|
||||||
self.post(url, data, expected_code=400)
|
response = 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
|
||||||
|
|
@ -1552,6 +1557,94 @@ 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."""
|
||||||
|
|
|
||||||
|
|
@ -650,11 +650,15 @@ 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):
|
with self.assertRaises(ValidationError) as exc:
|
||||||
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,
|
||||||
|
|
@ -671,6 +675,59 @@ 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()
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,13 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,8 @@ class BarcodeScanResultAdmin(admin.ModelAdmin):
|
||||||
class ProjectCodeAdmin(admin.ModelAdmin):
|
class ProjectCodeAdmin(admin.ModelAdmin):
|
||||||
"""Admin settings for ProjectCode."""
|
"""Admin settings for ProjectCode."""
|
||||||
|
|
||||||
list_display = ('code', 'description')
|
list_display = ('code', 'description', 'active')
|
||||||
|
list_filter = ('active',)
|
||||||
|
|
||||||
search_fields = ('code', 'description')
|
search_fields = ('code', 'description')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -181,6 +181,12 @@ 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,
|
||||||
|
|
@ -1578,7 +1584,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 = models.UUIDField(
|
message_id = InvenTree.fields.InvenTreeUUIDField(
|
||||||
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,
|
||||||
|
|
@ -3260,7 +3266,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 = models.UUIDField(
|
global_id = InvenTree.fields.InvenTreeUUIDField(
|
||||||
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,
|
||||||
|
|
@ -3368,7 +3374,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 = models.UUIDField(
|
global_id = InvenTree.fields.InvenTreeUUIDField(
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,14 @@ class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSeria
|
||||||
"""Meta options for ProjectCodeSerializer."""
|
"""Meta options for ProjectCodeSerializer."""
|
||||||
|
|
||||||
model = common_models.ProjectCode
|
model = common_models.ProjectCode
|
||||||
fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail']
|
fields = [
|
||||||
|
'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
|
||||||
|
|
|
||||||
|
|
@ -793,6 +793,14 @@ 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'),
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,7 @@
|
||||||
"""Types for settings."""
|
"""Types for settings."""
|
||||||
|
|
||||||
import sys
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any, TypedDict
|
from typing import Any, NotRequired, 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):
|
||||||
|
|
|
||||||
|
|
@ -142,12 +142,3 @@ 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
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1752,6 +1752,22 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""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 _
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ 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,
|
||||||
|
|
@ -118,6 +120,8 @@ class CompanySerializer(
|
||||||
|
|
||||||
import_exclude_fields = ['image']
|
import_exclude_fields = ['image']
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
|
@ -132,6 +136,7 @@ class CompanySerializer(
|
||||||
'email',
|
'email',
|
||||||
'currency',
|
'currency',
|
||||||
'contact',
|
'contact',
|
||||||
|
'duplicate',
|
||||||
'link',
|
'link',
|
||||||
'image',
|
'image',
|
||||||
'active',
|
'active',
|
||||||
|
|
@ -191,6 +196,23 @@ 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):
|
||||||
|
|
@ -217,6 +239,8 @@ class ManufacturerPartSerializer(
|
||||||
):
|
):
|
||||||
"""Serializer for ManufacturerPart object."""
|
"""Serializer for ManufacturerPart object."""
|
||||||
|
|
||||||
|
SKIP_CREATE_FIELDS = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
|
@ -229,6 +253,7 @@ class ManufacturerPartSerializer(
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'manufacturer_detail',
|
'manufacturer_detail',
|
||||||
'description',
|
'description',
|
||||||
|
'duplicate',
|
||||||
'MPN',
|
'MPN',
|
||||||
'link',
|
'link',
|
||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
|
|
@ -241,6 +266,25 @@ 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={
|
||||||
|
|
@ -323,6 +367,8 @@ 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',
|
||||||
|
|
@ -339,6 +385,7 @@ class SupplierPartSerializer(
|
||||||
'available',
|
'available',
|
||||||
'availability_updated',
|
'availability_updated',
|
||||||
'description',
|
'description',
|
||||||
|
'duplicate',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
'on_order',
|
'on_order',
|
||||||
'link',
|
'link',
|
||||||
|
|
@ -494,6 +541,10 @@ 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.
|
||||||
|
|
@ -522,8 +573,11 @@ 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)
|
||||||
|
|
||||||
|
|
@ -541,6 +595,12 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ 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
|
||||||
|
|
@ -64,6 +65,14 @@ 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):
|
||||||
|
|
@ -99,6 +108,10 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
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."""
|
||||||
|
|
@ -41,6 +43,14 @@ 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):
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.core.validators import FileExtensionValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
@ -200,7 +201,7 @@ class DataImportSession(models.Model):
|
||||||
related_field = model_class._meta.get_field(field_name)
|
related_field = model_class._meta.get_field(field_name)
|
||||||
model = related_field.remote_field.model
|
model = related_field.remote_field.model
|
||||||
return model
|
return model
|
||||||
except (AttributeError, models.FieldDoesNotExist):
|
except (AttributeError, FieldDoesNotExist):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_columns(self) -> None:
|
def extract_columns(self) -> None:
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue