2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-02 03:14:56 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into feat--matrix-testing

This commit is contained in:
Matthias Mair
2025-12-22 18:33:12 +01:00
330 changed files with 118246 additions and 106944 deletions

View File

@@ -1,6 +1,6 @@
services:
db:
image: postgres:17
image: postgres:15
restart: unless-stopped
ports:
- 5432/tcp

View File

@@ -4,6 +4,8 @@ updates:
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
dependencies:
patterns:
@@ -13,11 +15,15 @@ updates:
directory: /contrib/container
schedule:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: docker
directory: /.devcontainer
schedule:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: pip
directories:
@@ -28,6 +34,8 @@ updates:
schedule:
interval: weekly
day: friday
cooldown:
default-days: 7
groups:
dependencies:
patterns:
@@ -41,6 +49,8 @@ updates:
- /src/frontend
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
dependencies:
patterns:

View File

@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false

View File

@@ -39,7 +39,7 @@ jobs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
@@ -67,7 +67,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Test Docker Image
@@ -129,7 +129,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Run Migration Tests
@@ -153,11 +153,11 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Set Up Python ${{ env.python_version }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # pin@v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # pin@v6.1.0
with:
python-version: ${{ env.python_version }}
- name: Version Check
@@ -201,7 +201,7 @@ jobs:
- name: Extract Docker metadata
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # pin@v5.9.0
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # pin@v5.10.0
with:
images: |
inventree/inventree

View File

@@ -41,7 +41,7 @@ jobs:
requirements: ${{ steps.filter.outputs.requirements }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
@@ -82,11 +82,11 @@ jobs:
if: needs.paths-filter.outputs.cicd == 'true' || needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # pin@v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # pin@v6.1.0
with:
python-version: ${{ env.python_version }}
cache: "pip"
@@ -104,7 +104,7 @@ jobs:
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -126,11 +126,11 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # pin@v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # pin@v6.1.0
with:
python-version: ${{ env.python_version }}
- name: Check Config
@@ -164,7 +164,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -249,7 +249,7 @@ jobs:
version: ${{ needs.schema.outputs.version }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
name: Checkout Code
with:
repository: inventree/schema
@@ -302,9 +302,9 @@ jobs:
INVENTREE_LOG_LEVEL: WARNING
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: true
persist-credentials: false
- name: Environment Setup
uses: ./.github/actions/setup
with:
@@ -345,7 +345,7 @@ jobs:
python_version: ${{ matrix.python_version }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -376,6 +376,39 @@ jobs:
slug: inventree/InvenTree
flags: backend
performance:
name: Tests - Performance
runs-on: ubuntu-24.04
needs: ["pre-commit", "paths-filter"]
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
permissions:
contents: read
id-token: write
env:
INVENTREE_DB_NAME: inventree_unit_test_db.sqlite
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_PLUGINS_ENABLED: true
INVENTREE_CONSOLE_LOG: false
INVENTREE_AUTO_UPDATE: true
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
dev-install: true
update: true
- name: Performance Reporting
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # pin@v4
with:
mode: simulation
run: inv dev.test --pytest
postgres:
name: Tests - DB [PostgreSQL]
runs-on: ubuntu-24.04
@@ -409,7 +442,7 @@ jobs:
- 6379:6379
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -457,7 +490,7 @@ jobs:
- 3306:3306
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -499,7 +532,7 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -533,7 +566,7 @@ jobs:
INVENTREE_PLUGINS_ENABLED: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
name: Checkout Code
@@ -612,7 +645,7 @@ jobs:
VITE_COVERAGE_BUILD: true
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -662,7 +695,7 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -695,7 +728,7 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- uses: hynek/setup-cached-uv@757bedc3f972eb7227a1aa657651f15a8527c817 # pin@v2
@@ -704,7 +737,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # pin@v3
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # pin@v3
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -20,7 +20,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Version Check
@@ -43,7 +43,7 @@ jobs:
contents: write
attestations: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -55,7 +55,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@8e94d75ddd33f69f691467e42275782e4bfefe84 # pin@v0
uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
@@ -76,7 +76,7 @@ jobs:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
- name: Upload frontend
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # pin@2.11.2
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/backend/InvenTree/web/static/frontend-build.zip
@@ -84,7 +84,7 @@ jobs:
tag: ${{ github.ref }}
overwrite: true
- name: Upload Attestation
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # pin@2.11.2
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: frontend-build.intoto.jsonl
@@ -107,7 +107,7 @@ jobs:
INVENTREE_DEBUG: true
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: false
- name: Environment Setup
@@ -127,7 +127,7 @@ jobs:
cd docs/site
zip -r docs-html.zip *
- name: Publish documentation
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # pin@2.11.2
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: docs/site/docs-html.zip

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
sarif_file: results.sarif

View File

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

View File

@@ -32,9 +32,9 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # pin@v6.0.1
with:
persist-credentials: true
persist-credentials: false
- name: Environment Setup
uses: ./.github/actions/setup
with:
@@ -56,7 +56,7 @@ jobs:
echo "Resetting to HEAD~"
git reset HEAD~ || true
- name: crowdin action
uses: crowdin/github-action@08713f00a50548bfe39b37e8f44afb53e7a802d4 # pin@v2
uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # pin@v2
with:
upload_sources: true
upload_translations: false

8
.gitignore vendored
View File

@@ -37,6 +37,11 @@ local_settings.py
*.backup
*.old
# Files generated by profiling tools
*.prof
*.log
*.sql
# Files used for testing
inventree-demo-dataset/
inventree-data/
@@ -111,3 +116,6 @@ api.yaml
# web frontend (static files)
src/backend/InvenTree/web/static
InvenTree/web/static
# performance test results
.codspeed/

View File

@@ -10,7 +10,7 @@ exclude: |
)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -18,7 +18,7 @@ repos:
exclude: mkdocs.yml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.3
rev: v0.14.8
hooks:
- id: ruff-format
args: [--preview]
@@ -29,7 +29,7 @@ repos:
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.7.12
rev: 0.9.16
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
@@ -71,16 +71,16 @@ repos:
src/frontend/vite.config.ts |
)$
- repo: https://github.com/biomejs/pre-commit
rev: v2.0.0-beta.5
rev: v2.3.8
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.4"]
files: ^src/frontend/.*\.(js|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks
rev: v8.27.2
rev: v8.30.0
hooks:
- id: gitleaks
language_version: 1.23.6
language_version: 1.25.4
#- repo: https://github.com/jumanjihouse/pre-commit-hooks
# rev: 3.0.0
# hooks:

View File

@@ -7,14 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased - YYYY-MM-DD
### Breaking Changes
- [#10699](https://github.com/inventree/InvenTree/pull/10699) removes the `PartParameter` and `PartParameterTempalate` models (and associated API endpoints). These have been replaced with generic `Parameter` and `ParameterTemplate` models (and API endpoints). Any external client applications which made use of the old endpoints will need to be updated.
### Added
- Adds "Category" columns to BOM and Build Item tables and APIs in [#10722](https://github.com/inventree/InvenTree/pull/10772)
- Adds generic "Parameter" and "ParameterTemplate" models (and associated API endpoints) in [#10699](https://github.com/inventree/InvenTree/pull/10699)
- Adds parameter support for multiple new model types in [#10699](https://github.com/inventree/InvenTree/pull/10699)
- Allows report generator to produce PDF input controls in [#10969](https://github.com/inventree/InvenTree/pull/10969)
- UI overhaul of parameter management in [#10699](https://github.com/inventree/InvenTree/pull/10699)
### Changed
-
### Removed
- Removed python 3.9 / 3.10 support as part of Django 5.2 upgrade in [#10730](https://github.com/inventree/InvenTree/pull/10730)
- Removed the "PartParameter" and "PartParameterTemplate" models (and associated API endpoints) in [#10699](https://github.com/inventree/InvenTree/pull/10699)
- Removed the "ManufacturerPartParameter" model (and associated API endpoints) [#10699](https://github.com/inventree/InvenTree/pull/10699)
## 1.1.0 - 2025-11-02

View File

@@ -1,4 +1,4 @@
#!/bin/ash
#!/bin/bash
# exit when any command fails
set -e

View File

@@ -4,9 +4,9 @@ asgiref==3.10.0 \
--hash=sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734 \
--hash=sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e
# via django
django==5.2.8 \
--hash=sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f \
--hash=sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f
django==5.2.9 \
--hash=sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495 \
--hash=sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a
# via
# -r contrib/container/requirements.in
# django-auth-ldap

View File

@@ -214,66 +214,72 @@ ruamel-yaml==0.18.15 \
--hash=sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701 \
--hash=sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700
# via jc
ruamel-yaml-clib==0.2.14 \
--hash=sha256:090782b5fb9d98df96509eecdbcaffd037d47389a89492320280d52f91330d78 \
--hash=sha256:0a54e5e40a7a691a426c2703b09b0d61a14294d25cfacc00631aa6f9c964df0d \
--hash=sha256:10d9595b6a19778f3269399eff6bab642608e5966183abc2adbe558a42d4efc9 \
--hash=sha256:16a60d69f4057ad9a92f3444e2367c08490daed6428291aa16cefb445c29b0e9 \
--hash=sha256:18c041b28f3456ddef1f1951d4492dbebe0f8114157c1b3c981a4611c2020792 \
--hash=sha256:1c1acc3a0209ea9042cc3cfc0790edd2eddd431a2ec3f8283d081e4d5018571e \
--hash=sha256:1f118b707eece8cf84ecbc3e3ec94d9db879d85ed608f95870d39b2d2efa5dca \
--hash=sha256:2070bf0ad1540d5c77a664de07ebcc45eebd1ddcab71a7a06f26936920692beb \
--hash=sha256:26a8de280ab0d22b6e3ec745b4a5a07151a0f74aad92dd76ab9c8d8d7087720d \
--hash=sha256:275f938692013a3883edbd848edde6d9f26825d65c9a2eb1db8baa1adc96a05d \
--hash=sha256:27c070cf3888e90d992be75dd47292ff9aa17dafd36492812a6a304a1aedc182 \
--hash=sha256:29757bdb7c142f9595cc1b62ec49a3d1c83fab9cef92db52b0ccebaad4eafb98 \
--hash=sha256:4ccba93c1e5a40af45b2f08e4591969fa4697eae951c708f3f83dcbf9f6c6bb1 \
--hash=sha256:4f4a150a737fccae13fb51234d41304ff2222e3b7d4c8e9428ed1a6ab48389b8 \
--hash=sha256:557df28dbccf79b152fe2d1b935f6063d9cc431199ea2b0e84892f35c03bb0ee \
--hash=sha256:5ac5ff9425d8acb8f59ac5b96bcb7fd3d272dc92d96a7c730025928ffcc88a7a \
--hash=sha256:5bae1a073ca4244620425cd3d3aa9746bde590992b98ee8c7c8be8c597ca0d4e \
--hash=sha256:5e56ac47260c0eed992789fa0b8efe43404a9adb608608631a948cee4fc2b052 \
--hash=sha256:6aeadc170090ff1889f0d2c3057557f9cd71f975f17535c26a5d37af98f19c27 \
--hash=sha256:6d5472f63a31b042aadf5ed28dd3ef0523da49ac17f0463e10fda9c4a2773352 \
--hash=sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83 \
--hash=sha256:7df6f6e9d0e33c7b1d435defb185095386c469109de723d514142632a7b9d07f \
--hash=sha256:7e4f9da7e7549946e02a6122dcad00b7c1168513acb1f8a726b1aaf504a99d32 \
--hash=sha256:803f5044b13602d58ea378576dd75aa759f52116a0232608e8fdada4da33752e \
--hash=sha256:808c7190a0fe7ae7014c42f73897cf8e9ef14ff3aa533450e51b1e72ec5239ad \
--hash=sha256:81f6d3b19bc703679a5705c6a16dabdc79823c71d791d73c65949be7f3012c02 \
--hash=sha256:83bbd8354f6abb3fdfb922d1ed47ad8d1db3ea72b0523dac8d07cdacfe1c0fcf \
--hash=sha256:8dd3c2cc49caa7a8d64b67146462aed6723a0495e44bf0aa0a2e94beaa8432f6 \
--hash=sha256:915748cfc25b8cfd81b14d00f4bfdb2ab227a30d6d43459034533f4d1c207a2a \
--hash=sha256:94f3efb718f8f49b031f2071ec7a27dd20cbfe511b4dfd54ecee54c956da2b31 \
--hash=sha256:9bd8fe07f49c170e09d76773fb86ad9135e0beee44f36e1576a201b0676d3d1d \
--hash=sha256:9bf6b699223afe6c7fe9f2ef76e0bfa6dd892c21e94ce8c957478987ade76cd8 \
--hash=sha256:a05ba88adf3d7189a974b2de7a9d56731548d35dc0a822ec3dc669caa7019b29 \
--hash=sha256:a0ac90efbc7a77b0d796c03c8cc4e62fd710b3f1e4c32947713ef2ef52e09543 \
--hash=sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27 \
--hash=sha256:a37f40a859b503304dd740686359fcf541d6fb3ff7fc10f539af7f7150917c68 \
--hash=sha256:a911aa73588d9a8b08d662b9484bc0567949529824a55d3885b77e8dd62a127a \
--hash=sha256:aef953f3b8bd0b50bd52a2e52fb54a6a2171a1889d8dea4a5959d46c6624c451 \
--hash=sha256:b28caeaf3e670c08cb7e8de221266df8494c169bd6ed8875493fab45be9607a4 \
--hash=sha256:b30110b29484adc597df6bd92a37b90e63a8c152ca8136aad100a02f8ba6d1b6 \
--hash=sha256:b5b0f7e294700b615a3bcf6d28b26e6da94e8eba63b079f4ec92e9ba6c0d6b54 \
--hash=sha256:c099cafc1834d3c5dac305865d04235f7c21c167c8dd31ebc3d6bbc357e2f023 \
--hash=sha256:d73a0187718f6eec5b2f729b0f98e4603f7bd9c48aa65d01227d1a5dcdfbe9e8 \
--hash=sha256:d8354515ab62f95a07deaf7f845886cc50e2f345ceab240a3d2d09a9f7d77853 \
--hash=sha256:dba72975485f2b87b786075e18a6e5d07dc2b4d8973beb2732b9b2816f1bad70 \
--hash=sha256:dd7546c851e59c06197a7c651335755e74aa383a835878ca86d2c650c07a2f85 \
--hash=sha256:df3ec9959241d07bc261f4983d25a1205ff37703faf42b474f15d54d88b4f8c9 \
--hash=sha256:e1d1735d97fd8a48473af048739379975651fab186f8a25a9f683534e6904179 \
--hash=sha256:e501c096aa3889133d674605ebd018471bc404a59cbc17da3c5924421c54d97c \
--hash=sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640 \
--hash=sha256:f4e97a1cf0b7a30af9e1d9dad10a5671157b9acee790d9e26996391f49b965a2 \
--hash=sha256:f8b2acb0ffdd2ce8208accbec2dca4a06937d556fdcaefd6473ba1b5daa7e3c4 \
--hash=sha256:fb04c5650de6668b853623eceadcdb1a9f2fee381f5d7b6bc842ee7c239eeec4 \
--hash=sha256:fbc08c02e9b147a11dfcaa1ac8a83168b699863493e183f7c0c8b12850b7d259 \
--hash=sha256:ff86876889ea478b1381089e55cf9e345707b312beda4986f823e1d95e8c0f59
ruamel-yaml-clib==0.2.15 \
--hash=sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490 \
--hash=sha256:04d21dc9c57d9608225da28285900762befbb0165ae48482c15d8d4989d4af14 \
--hash=sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6 \
--hash=sha256:0ba6604bbc3dfcef844631932d06a1a4dcac3fee904efccf582261948431628a \
--hash=sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9 \
--hash=sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d \
--hash=sha256:1bb7b728fd9f405aa00b4a0b17ba3f3b810d0ccc5f77f7373162e9b5f0ff75d5 \
--hash=sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3 \
--hash=sha256:27dc656e84396e6d687f97c6e65fb284d100483628f02d95464fd731743a4afe \
--hash=sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c \
--hash=sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc \
--hash=sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf \
--hash=sha256:3cb75a3c14f1d6c3c2a94631e362802f70e83e20d1f2b2ef3026c05b415c4900 \
--hash=sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a \
--hash=sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa \
--hash=sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6 \
--hash=sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd \
--hash=sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25 \
--hash=sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600 \
--hash=sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf \
--hash=sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642 \
--hash=sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614 \
--hash=sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf \
--hash=sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000 \
--hash=sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb \
--hash=sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690 \
--hash=sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e \
--hash=sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137 \
--hash=sha256:5d3c9210219cbc0f22706f19b154c9a798ff65a6beeafbf77fc9c057ec806f7d \
--hash=sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401 \
--hash=sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f \
--hash=sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2 \
--hash=sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471 \
--hash=sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed \
--hash=sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524 \
--hash=sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60 \
--hash=sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef \
--hash=sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043 \
--hash=sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03 \
--hash=sha256:923816815974425fbb1f1bf57e85eca6e14d8adc313c66db21c094927ad01815 \
--hash=sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77 \
--hash=sha256:a8220fd4c6f98485e97aea65e1df76d4fed1678ede1fe1d0eed2957230d287c4 \
--hash=sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d \
--hash=sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467 \
--hash=sha256:badd1d7283f3e5894779a6ea8944cc765138b96804496c91812b2829f70e18a7 \
--hash=sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e \
--hash=sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec \
--hash=sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4 \
--hash=sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd \
--hash=sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff \
--hash=sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c \
--hash=sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862 \
--hash=sha256:dcc7f3162d3711fd5d52e2267e44636e3e566d1e5675a5f0b30e98f2c4af7974 \
--hash=sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922 \
--hash=sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a \
--hash=sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d \
--hash=sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262 \
--hash=sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144 \
--hash=sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1 \
--hash=sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51 \
--hash=sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f
# via ruamel-yaml
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
urllib3==2.6.0 \
--hash=sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f \
--hash=sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1
# via requests
xmltodict==1.0.2 \
--hash=sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649 \

View File

@@ -4,32 +4,92 @@
This repository hosts the [official documentation](https://inventree.readthedocs.io/) for [InvenTree](https://github.com/inventree/inventree), an open source inventory management system.
To serve this documentation locally (e.g. for development), you will need to have Python 3 installed on your system.
## Prerequisites
## Setup
InvenTree uses [MkDocs](https://www.mkdocs.org/) to convert [Markdown](https://www.mkdocs.org/user-guide/writing-your-docs/#writing-with-markdown) format `.md` files into HTML suitable for viewing in a web browser.
Run the following commands from the top-level project directory:
!!! info "Prerequisites"
To build and serve this documentation locally (e.g. for development), you will need:
* Python 3 installed on your system.
* An existing InvenTree installation containing the virtual environment that was created during installation.
These instructions assume you followed the [InvenTree bare metal installation instructions](./docs/start/install.md), so you'll have an `inventree` user, a home directory at `/home/inventree`, the InvenTree source code cloned from [GitHub](https://github.com/inventree/inventree) into `/home/inventree/src`, and a virtual environment at `/home/inventree/env`. If you installed InvenTree some other way, this might vary, and you'll have to adjust these instructions accordingly.
!!! warning "Your InvenTree install will be updated!"
Some of the commands that follow will make changes to your install, for example, by running any pending database migrations. There's a small risk this may cause issues with your existing installation. If you can't risk this, consider setting up a separate InvenTree installation specifically for documentation development.
## Building the documentation locally
To build the documentation locally, run these commands as the `inventree` user:
```
$ git clone https://github.com/inventree/inventree
$ cd /home/inventree
$ source env/bin/activate
```
!!! info "(env) prefix"
The shell prompt should now display the `(env)` prefix, showing that you are operating within the context of the python virtual environment
You can now install the additional packages needed by mkdocs:
```
$ cd src
$ pip install --require-hashes -r docs/requirements.txt
```
## Serve Locally
## Schema generation
To serve the pages locally, run the following command (from the top-level project directory):
Building the documentation requires extracting the API schema from the source code.
!!! tip
This command is only required when building the documentation for the first time, or when changes have been made to the API schema.
```
$ mkdocs serve -f docs/mkdocs.yml -a localhost:8080
$ invoke build-docs
```
## Edit Documentation Files
You will see output similar to this (truncated for brevity):
```
Running InvenTree database migrations...
Exporting definitions...
Exporting settings definition to '/home/inventree/src/docs/generated/inventree_settings.json'...
Exported InvenTree settings definitions to '/home/inventree/src/docs/generated/inventree_settings.json'
Exported InvenTree tag definitions to '/home/inventree/src/docs/generated/inventree_tags.yml'
Exported InvenTree filter definitions to '/home/inventree/src/docs/generated/inventree_filters.yml'
Exported InvenTree report context definitions to '/home/inventree/src/docs/generated/inventree_report_context.json'
Exporting definitions complete
Exporting schema file to '/home/inventree/src/docs/generated/schema.yml'
Once the server is running, it will monitor the documentation files for any changes, and update the served pages.
Schema export completed: /home/inventree/src/docs/generated/schema.yml
Documentation build complete, but mkdocs not requested
```
## Viewing the documentation
Generate the HTML files from the markdown source files, and start the MkDocs webpage server:
```
$ mkdocs serve -f docs/mkdocs.yml
```
You can then point your web browser at http://localhost:8080/
Alternatively, you can use the `invoke` command:
```
$ invoke dev.docs-server
```
If you need to, use the `-a` option after `mkdocs` or `invoke` to set the address and port. Run `invoke dev.docs-server --help` for details.
## Editing the Documentation Files
Once the server is running, it will monitor the documentation files for changes, and regenerate the HTML pages as required. Refresh your web browser to see the changes.
### Admonitions
"Admonition" blocks can be added as follow:
"Admonition" blocks can be added to the documentation source as follows:
```
!!! info "This is the admonition block title"
This is the admonition block content

View File

@@ -106,7 +106,7 @@ response = request.get('http://localhost:8080/api/part/', data=data, headers=hea
InvenTree has built-in support for using [oAuth2](https://oauth.net/2/) and OpenID Connect (OIDC) for authentication to the API. This enables using the instance as a very limited identity provider.
A default application using a public client with PKCE enabled ships with each instance. Intended to be used with the python api and configured with very wide scopes this can also be used for quick tests - the cliend_id is `zDFnsiRheJIOKNx6aCQ0quBxECg1QBHtVFDPloJ6`.
A default application using a public client with PKCE enabled ships with each instance. Intended to be used with the python api and configured with very wide scopes this can also be used for quick tests - the client_id is `zDFnsiRheJIOKNx6aCQ0quBxECg1QBHtVFDPloJ6`.
#### Managing applications

View File

@@ -67,7 +67,7 @@ print("Minimum stock:", part.minimum_stock)
### Adding Parameters
Each [part](../../part/index.md) can have multiple [parameters](../../part/parameter.md). For the example of the sofa (above) *length* and *weight* make sense. Each parameter has a parameter template that combines the parameter name with a unit. So we first have to create the parameter templates and afterwards add the parameter values to the sofa.
Each [part](../../part/index.md) can have multiple [parameters](../../concepts/parameters.md). For the example of the sofa (above) *length* and *weight* make sense. Each parameter has a parameter template that combines the parameter name with a unit. So we first have to create the parameter templates and afterwards add the parameter values to the sofa.
```python
from inventree.part import Parameter

View File

@@ -73,7 +73,7 @@ the *Scan Items Into Location* action allows you to scan items into the selected
### Stock Item Actions
From the [Stock Item detail page](./stock.md#stock-item-detail-view), the following barcode actions may be available:
From the [Stock Item detail page](./stock.md#details-tab), the following barcode actions may be available:
{{ image("app/barcode_stock_item_actions.png", "Stock item barcode actions") }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -69,6 +69,27 @@ To access this page, select *Scan Barcode* from the main navigation menu:
{{ image("barcode/barcode_nav_menu.png", "Barcode menu item") }}
{{ image("barcode/barcode_scan_page.png", "Barcode scan page") }}
## Barcodes in Forms
The InvenTree user interface supports direct scanning of barcodes within certain forms in the web UI. This means that any form field which points to a model which supports barcodes can accept barcode input. If barcode scanning is supported for a particular field, a barcode icon will be displayed next to the input field:
{{ image("barcode/barcode_field.png", "Barcode form field") }}
To scan a barcode into a form field, click this barcode icon. A barcode scanning dialog will be displayed, allowing the user to scan a barcode using their preferred input method:
{{ image("barcode/barcode_field_dialog.png", "Barcode field scan dialog") }}
Once scanned, the form field will be automatically populated with the correct item.
{{ image("barcode/barcode_field_filled.png", "Barcode field populated") }}
Any field which supports barcode input will have this functionality, such as allocating stock items to an order:
{{ image("barcode/barcode_allocate_stock.png", "Allocate stock via barcode") }}
### User Configuration
By default, barcode scanning in form fields is disabled. Each user can enable this feature via their [user preferences](../settings/user.md#display-settings).
## App Integration
@@ -76,8 +97,15 @@ Barcode scanning is a key feature of the [companion mobile app](../app/barcode.m
## Barcode History
If enabled, InvenTree can retain logs of the most recent barcode scans. This can be very useful for debugging or auditing purpopes.
If enabled, InvenTree can retain logs of the most recent barcode scans. This can be very useful for debugging or auditing purposes.
Refer to the [barcode settings](../settings/global.md#barcodes) to enable barcode history logging.
The barcode history can be viewed via the admin panel in the web interface.
## Barcode Settings
There are a number of settings which control the behavior of barcodes within InvenTree. For more information, refer to the links below:
- [Global Barcode Settings](../settings/global.md#barcodes)
- [User Preferences for Barcode Scanning](../settings/user.md#display-settings)

View File

@@ -0,0 +1,18 @@
---
title: Attachments
---
## Attachments
An *attachment* is a file which has been uploaded and linked to a specific object within InvenTree. Attachments can be used to store additional documentation, images, or other relevant files associated with various InvenTree models.
!!! note "Business Logic"
Attachments are not used for any core business logic within InvenTree. They are intended to provide additional metadata for objects, which can be useful for documentation, reference, or reporting purposes.
Parameters can be associated with various InvenTree models.
### Attachments Tab
Any model which supports attachments will have an "Attachments" tab on its detail page. This tab displays all attachments associated with that object:
{{ image("concepts/attachments-tab.png", "Order Attachments Example") }}

View File

@@ -1,17 +1,21 @@
---
title: Part Parameters
title: Parameters
---
## Part Parameters
## Parameters
A part *parameter* describes a particular "attribute" or "property" of a specific part.
A *parameter* describes a particular "attribute" or "property" of a specific object in InvenTree. Parameters allow for flexible and customizable data to be stored against various InvenTree models.
Part parameters are located in the "Parameters" tab, on each part detail page.
There is no limit for the number of part parameters and they are fully customizable through the use of [parameter templates](#parameter-templates).
!!! note "Business Logic"
Parameters are not used for any core business logic within InvenTree. They are intended to provide additional metadata for objects, which can be useful for documentation, filtering, or reporting purposes.
Here is an example of parameters for a capacitor:
Parameters can be associated with various InvenTree models.
{{ image("part/part_parameters_example.png", "Part Parameters Example") }}
### Parameter Tab
Any model which supports parameters will have a "Parameters" tab on its detail page. This tab displays all parameters associated with that object:
{{ image("concepts/parameter-tab.png", "Part Parameters Example") }}
## Parameter Templates
@@ -22,13 +26,16 @@ Parameter templates are used to define the different types of parameters which a
| Name | The name of the parameter template (*must be unique*) |
| Description | Optional description for the template |
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
| Model Type | The InvenTree model to which this parameter template applies (e.g. Part, Company, etc). If this is left blank, the template can be used for any model type. |
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
| Selection List | If set, parameters linked to this template can only be assigned values from the linked [selection list](#selection-lists) |
{{ image("concepts/parameter-template.png", "Parameters Template") }}
### Create Template
Parameter templates are created and edited via the [settings interface](../settings/global.md).
Parameter templates are created and edited via the [admin interface](../settings/admin.md).
To create a template:
@@ -54,11 +61,11 @@ Select the parameter `Template` you would like to use for this parameter, fill-o
## Parametric Tables
Parametric tables gather all parameters from all parts inside a particular [part category](./index.md#part-category) to be sorted and filtered.
Parametric tables gather all parameters from all objects of a particular type, to be sorted and filtered.
To access a category's parametric table, click on the "Parameters" tab within the category view:
Tables views which support parametric filtering and sorting will have a "Parametric View" button above the table:
{{ image("part/parametric_table_tab.png", "Parametric Table Tab") }}
{{ image("concepts/parametric-parts.png", "Parametric Parts Table") }}
### Sorting by Parameter Value
@@ -139,7 +146,7 @@ Parameter sorting takes unit conversion into account, meaning that values provid
### Selection Lists
Selection Lists can be used to add a large number of predefined values to a parameter template. This can be useful for parameters which must be selected from a large predefined list of values (e.g. a list of standardised colo codes). Choices on templates are limited to 5000 characters, selection lists can be used to overcome this limitation.
Selection Lists can be used to add a large number of predefined values to a parameter template. This can be useful for parameters which must be selected from a large predefined list of values (e.g. a list of standardized color codes). Choices on templates are limited to 5000 characters, selection lists can be used to overcome this limitation.
It is possible that plugins lock selection lists to ensure a known state.

View File

@@ -59,9 +59,9 @@ The [unit of measure](../part/index.md#units-of-measure) field for the [Part](..
The [supplier part](../part/index.md/#supplier-parts) model uses real-world units to convert between supplier part quantities and internal stock quantities. Unit conversion rules ensure that only compatible unit types can be supplied
### Part Parameter
### Parameter
The [part parameter template](../part/parameter.md#parameter-templates) model can specify units of measure, and part parameters can be specified against these templates with compatible units
The [parameter template](../concepts/parameters.md#parameter-templates) model can specify units of measure, and part parameters can be specified against these templates with compatible units
## Custom Units

View File

@@ -0,0 +1,56 @@
---
title: InvenTree Development
---
## Introduction
If you are interested in contributing to InvenTree, then this section is for you! Here you will find information about the architecture of InvenTree, how to set up a development environment, and guidelines for contributing code or documentation.
### Architecture Overview
Read the [architecture overview](./architecture.md) to understand the high-level architecture of InvenTree, including how requests are processed, the backend and frontend architecture, and various components of the system.
### Contribution Guide
Start with the [contribution guide](./contributing.md) to understand how to get involved with the InvenTree project.
### Devcontainer Setup
We provide a [devcontainer](./devcontainer.md) configuration to help you quickly set up a development environment using vscode.
### Frontend Development
For information on developing the InvenTree frontend, refer to the [frontend development guide](./react-frontend.md).
## Profiling Tools
The InvenTree project supports integrated profiling tools to help developers analyze and optimize performance. Note that the following tools are intended for development use only and should not be enabled in production environments. In fact, they are explicitly disabled unless the server is running in [debug mode](../start/index.md#debug-mode).
### Django Silk
[django-silk](https://silk.readthedocs.io/en/latest/) is a profiling tool that can be used to monitor and analyze the performance of Django applications. It provides insights into SQL queries, request/response times, and more.
To enable django-silk profiling, ensure that the `debug_silk` option is set to `True` in your [config file](../start/config.md#configuration-file). Alternative, you can set the `INVENTREE_DEBUG_SILK` environment variable to enable this feature.
Once enabled, you can access the silk interface at the `/silk/` endpoint of your InvenTree instance.
!!! tip "Run Migrations"
If you are enabling django-silk for the first time, you may need to run database migrations to create the necessary tables. You can do this by running `invoke migrate`.
#### Detailed Profiling
To enable detailed profiling in django-silk, set the `INVENTREE_DEBUG_SILK_PROFILING` environment variable to `True`, or set the `debug_silk_profiling` option to `True` in your config file. This will enable more granular profiling features within django-silk. Refer to the [django-silk documentation](https://github.com/jazzband/django-silk#profiling) for more information.
### Django QueryCount
Enabling the `INVENTREE_DEBUG_QUERYCOUNT` setting will log (to the terminal) the number of database queries executed for each page load. This can be useful for identifying performance bottlenecks in the InvenTree server. Note that this setting is only available if `INVENTREE_DEBUG` is also enabled.
### Database Logging
Enabling the `INVENTREE_DB_LOGGING` setting will log all database queries to the terminal. This can be useful for debugging database-related issues.
### Internal Profiling Tools
In addition to the above third-party tools, InvenTree includes some internal profiling tools that can be enabled in debug mode. These tools can be used to provide additional insights into the performance of various components of the InvenTree server.
These profiling tools can be found in `./src/backend/InvenTree/profiling.py`.

View File

@@ -98,7 +98,7 @@ Sometimes, users may encounter unexpected error messages when updating their Inv
The most common problem here is that the correct sequence of steps has not been followed:
1. Ensure that the InvenTree web server and background worker processes are *halted*
1. Ensure that the InvenTree [web server](./start/processes.md#web-server) and [background worker](./start/processes.md#background-worker) processes are *halted*
1. Update the InvenTree software (e.g. using git or docker, depending on installation method)
1. Run the `invoke update` command
1. Restart the web server and background worker processes
@@ -150,7 +150,7 @@ or
### Background Worker "Not Running"
The background worker process must be started separately to the web-server application.
The [background worker process](./start/processes.md#background-worker) must be started separately to the web-server application.
From the top-level source directory, run the following command from a separate terminal, while the server is already running:

View File

@@ -31,7 +31,7 @@ Before continuing, it is important that the difference between *untracked* and *
#### BOM Considerations
A [Bill of Materials](./bom.md) to generate an assembly may consist of a mixture of *untracked* and *tracked* components. The build order process can facilitate this, as documentated in the sections below.
A [Bill of Materials](./bom.md) to generate an assembly may consist of a mixture of *untracked* and *tracked* components. The build order process can facilitate this, as documented in the sections below.
### Tracked Build Outputs

View File

@@ -4,7 +4,7 @@ title: Trackable Parts
## Stock Tracking
Denoting a part as *Trackble* changes the way that [stock items](../stock/index.md) associated with the particular part are handled in the database. A trackable part also has more restrictions imposed by the database scheme.
Denoting a part as *Trackable* changes the way that [stock items](../stock/index.md) associated with the particular part are handled in the database. A trackable part also has more restrictions imposed by the database scheme.
For many parts in an InvenTree database, simply tracking current stock levels (and locations) is sufficient. However, some parts require more extensive tracking than simple stock level knowledge.

View File

@@ -39,12 +39,6 @@ A Part is defined in the system by the following parameters:
The Part view page organizes part data into sections, displayed as tabs. Each tab has its own function, which is described in this section.
### Parameters
Parts can have multiple defined parameters.
[Read about Part parameters](./parameter.md)
### Variants
If a part is a *Template Part* then the *Variants* tab will be visible.
@@ -125,10 +119,18 @@ Related parts can be added and are shown under a table of the same name in the "
This feature can be enabled or disabled in the global part settings.
### Parameters
Parts can have multiple defined parameters.
[Read about parameters](../concepts/parameters.md).
### Attachments
The *Part Attachments* tab displays file attachments associated with the selected *Part*. Multiple file attachments (such as datasheets) can be uploaded for each *Part*.
[Read about attachments](../concepts/attachments.md).
### Notes
A part may have notes attached, which support markdown formatting.

View File

@@ -21,7 +21,7 @@ The following builtin plugins are available in InvenTree:
| Barcodes | [TME](./barcode_tme.md) | TME barcode support | No |
| Data Export | [BOM Exporter](./bom_exporter.md) | Custom [exporter](../mixins/export.md) for BOM data | Yes |
| Data Export | [InvenTree Exporter](./inventree_exporter.md) | Custom [exporter](../mixins/export.md) for InvenTree data | Yes |
| Data Export | [Parameter Exporter](./part_parameter_exporter.md) | Custom [exporter](../mixins/export.md) for part parameter data | Yes |
| Data Export | [Parameter Exporter](./parameter_exporter.md) | Custom [exporter](../mixins/export.md) for parameter data | Yes |
| Data Export | [Stocktake Exporter](./stocktake_exporter.md) | Custom [exporter](../mixins/export.md) for stocktake data | No |
| Events | [Auto Create Child Builds](./auto_create_builds.md) | Automatically create child build orders for sub-assemblies | No |
| Events | [Auto Issue Orders](./auto_issue.md) | Automatically issue pending orders when target date is reached | No |

View File

@@ -0,0 +1,27 @@
---
title: Parameter Exporter
---
## Parameter Exporter
The **Parameter Exporter** plugin provides custom export functionality for models which support custom [Parameter](../../concepts/parameters.md) data.
It utilizes the [ExporterMixin](../mixins/export.md) mixin to provide a custom export format for part parameter data.
In addition to the standard exported fields, this plugin also exports all associated parameter data for each row of the export.
### Activation
This plugin is a *mandatory* plugin, and is always enabled.
### Plugin Settings
This plugin has no configurable settings.
## Usage
This plugin is used in the same way as the [InvenTree Exporter Plugin](./inventree_exporter.md), but provides a custom export format for part parameter data.
When exporting parameter data, the *Parameter Exporter* plugin is available for selection in the export dialog. When selected, the plugin provides some additional export options to control the data export process.
{{ image("parameter_export_options.png", base="plugin/builtin", title="Parameter Export Options") }}

View File

@@ -1,25 +0,0 @@
---
title: Part Parameter Exporter
---
## Part Parameter Exporter
The **Part Parameter Exporter** plugin provides custom export functionality for [Part Parameter](../../part/parameter.md) data.
It utilizes the [ExporterMixin](../mixins/export.md) mixin to provide a custom export format for part parameter data.
### Activation
This plugin is a *mandatory* plugin, and is always enabled.
### Plugin Settings
This plugin has no configurable settings.
## Usage
This plugin is used in the same way as the [InvenTree Exporter Plugin](./inventree_exporter.md), but provides a custom export format for part parameter data.
When exporting part parameter data, the *Part Parameter Exporter* plugin is available for selection in the export dialog. When selected, the plugin provides some additional export options to control the data export process.
{{ image("parameter_export_options.png", base="plugin/builtin", title="Part Parameter Export Options") }}

View File

@@ -65,8 +65,14 @@ Admin users can install plugins directly from the web interface, via the "Plugin
{{ image("plugin/plugin_install_web.png", "Install plugin via web interface") }}
Enter the package name into the form as shown below. You can add a path and a version. Leave the version field empty for the latest version. In case the package is on pypi the path can be omitted. Pip will find it automatically.
{{ image("plugin/plugin_install_git.png", "Install plugin from git") }}
!!! success "Plugin File"
A plugin installed via the web interface is added to the [plugins.txt](#plugin-installation-file-pip) plugin file.
A plugin installed via the web interface is added to the [plugins.txt](#plugin-installation-file-pip) plugin file as shown below.
{{ image("plugin/plugin_install_txt.png", "Plugin.txt file") }}
#### Local Directory

View File

@@ -134,11 +134,11 @@ Validation of the Part IPN (Internal Part Number) field is exposed to custom plu
summary: False
members: []
### Part Parameter Values
### Parameter Values
[Part parameters](../../part/parameter.md) can also have custom validation rules applied, by implementing the `validate_part_parameter` method. A plugin which implements this method should raise a `ValidationError` with an appropriate message if the part parameter value does not match a required convention.
[Parameters](../../concepts/parameters.md) can also have custom validation rules applied, by implementing the `validate_parameter` method. A plugin which implements this method should raise a `ValidationError` with an appropriate message if the parameter value does not match a required convention.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_parameter
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_parameter
options:
show_bases: False
show_root_heading: False

View File

@@ -6,7 +6,7 @@ The InvenTree project is grateful to receive resources from various vendors free
Individuals and companies can also support via [GitHub sponsors](https://github.com/sponsors/inventree).
## Current supporters
## Current Supporters
- [DigitalOcean](https://inventree.org/digitalocean) - Cloud hosting provider, used to host the InvenTree demo instance
- [Crowdin](https://crowdin.com/) - Translation platform, used to manage the [InvenTree translations](../develop/contributing.md#translations) across backend, frontend and app
- [SonarQube Cloud](https://sonarcloud.io/) - Code quality and security analysis, used to track the code quality of the various components
@@ -15,7 +15,8 @@ Individuals and companies can also support via [GitHub sponsors](https://github.
- [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
## Past supporters
Non cromprehensive list of past supporters. The project stops consuming resources for various reasons, this does not mean they are not good resources.
## Past Supporters
Non comprehensive list of past supporters. The project stops consuming resources for various reasons, this does not mean they are not good resources.
- [Coveralls](https://coveralls.io/) - Code coverage as a service
- [Deepsource](https://deepsource.io/) - Code quality and security analysis

View File

@@ -545,11 +545,11 @@ You can add asset images to the reports and labels by using the `{% raw %}{% ass
{% endraw %}
```
## Part Parameters
## Parameters
If you need to load a part parameter for a particular Part, within the context of your template, you can use the `part_parameter` template tag:
If you need to load a parameter value for a particular model instance, within the context of your template, you can use the `parameter` template tag:
::: report.templatetags.report.part_parameter
::: report.templatetags.report.parameter
options:
show_docstring_description: false
show_source: False
@@ -562,7 +562,7 @@ The following example assumes that you have a report or label which contains a v
{% raw %}
{% load report %}
{% part_parameter part "length" as length %}
{% parameter part "length" as length %}
Part: {{ part.name }}<br>
Length: {{ length.data }} [{{ length.units }}]
@@ -570,7 +570,7 @@ Length: {{ length.data }} [{{ length.units }}]
{% endraw %}
```
A [Part Parameter](../part/parameter.md) has the following available attributes:
A [Parameter](../concepts/parameters.md) has the following available attributes:
| Attribute | Description |
| --- | --- |
@@ -578,7 +578,7 @@ A [Part Parameter](../part/parameter.md) has the following available attributes:
| Description | The *description* of the parameter |
| Data | The *value* of the parameter (e.g. "123.4") |
| Units | The *units* of the parameter (e.g. "km") |
| Template | A reference to a [PartParameterTemplate](../part/parameter.md#parameter-templates) |
| Template | A reference to a [ParameterTemplate](../concepts/parameters.md#parameter-templates) |
## Rendering Markdown

View File

@@ -44,5 +44,45 @@ For example, rendering the name of a part (which is available in the particular
</p></i>
{% endraw %}
```
#### Rendering a single report vs. multiple report from selection
Users can select multiple items such as `part`, `stockItem`,...etc to render from a report template. By default, the `merge` attribute of report template is disabled, which means an independent report will be generated for each item in the list of selected items. If `merge` is enabled, all selected items will be available in the `instances` context variable of the report template. Users are free to access them by indexing or in a loop. For more details, visit [context variable](./context_variables.md)
## 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.
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:
{{ image("report/report_merge.png", alt="Report Merge Option") }}
When the `merge` is enabled, all selected items are passed to the report template in the `instances` context variable, which is a list of all selected items. The user can then iterate over this list to render multiple items in a single report document.
### Instance Context
When rendering a single template against multiple *instances* of a particular model (e.g. multiple parts, multiple stock items, etc), each instance being rendered has its own unique context data.
Each "instance" is provided to the report template as a dictionary of context variables, which can be accessed using standard django template syntax.
Refer to the [context variable documentation](./context_variables.md) for more information about the available context variables for each model type.
The `instances` variable is provided as a list of all selected items, where each item in the list is a dictionary of context variables for that particular instance. Within the report template, the user can iterate over the `instances` list to render each item in turn.
### Example
As an example, let's consider a report template where we are printing multiple parts in a single report document.
When the `merge` option is enabled, the report template is provided with an `instances` variable, which is a list of all selected parts.
Each *instance* in the `instances` list is a dictionary of context variables for that particular part, which conforms to the standard [part context structure](./context_variables.md#part).
```django
{% raw %}
{% for instance in instances %}
Part Name: {{ instance.part.name }} <br>
IPN: {{ instance.part.IPN }} <br>
Description: {{ instance.part.description }} <br>
{% endraw %}
```
!!! tip "Instance Prefix"
Note that the context variable is prefixed with `instance.` when accessing variables for each item in the `instances` list.

View File

@@ -13,6 +13,9 @@ We use the powerful [WeasyPrint](https://weasyprint.org/) PDF generation engine
Templates are rendered using standard HTML / CSS - if you are familiar with web page layout, you're ready to go!
HTML form elements (`input`, `select`, `textarea`, `button`) are converted into PDF form controls. Use CSS to limit which elements are converted; refer to the
[WeasyPrint docs](https://doc.courtbouillon.org/weasyprint/stable/common_use_cases.html#include-pdf-forms) for further information.
### Template Language
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.

View File

@@ -4,7 +4,7 @@ title: InvenTree Multi Factor Authentication
## Multi Factor Authentication
InvenTree gives the option to use TOTP or statically generated backup tokens as an additional factor to password or SSO authentication. This is a widely adopted security feature on enterprise web services. We highly encourage to enable it if you expose your instance to the public internet.
InvenTree gives the option to use [TOTP](https://en.wikipedia.org/wiki/Time-based_One-Time_Password) or statically generated backup tokens as an additional factor to password or SSO authentication. This is a widely adopted security feature on enterprise web services. We highly encourage to enable it if you expose your instance to the public internet.
As TOTP is an [open standard](https://datatracker.ietf.org/doc/html/rfc6238) there are a lot of different ways to hold your key and generate the time based tokens needed for authentication. That ranges from physical devices to password managers and mobile apps. We do not advertise any method but recommend to keep password and token generator separate from each other.
@@ -16,4 +16,4 @@ To make MFA mandatory for all users:
### Security Consideration
A user can lock themself out if they lose access to both the device with their TOTP app and their backup tokens. An admin can delete their tokens from the admin pages (they exist under the 'TOTP devices' / 'static devices' models) . This should be a last resort and only done by people knowledgeable about the [admin pages](../settings/admin.md) as changes there might circumvent InvneTrees business and security logic.
A user can lock themselves out if they lose access to both the device with their TOTP app and their backup tokens. An admin can delete their tokens from the admin pages (they exist under the 'TOTP devices' / 'static devices' models) . This should be a last resort and only done by people knowledgeable about the [admin pages](../settings/admin.md) as changes there might circumvent InvenTree's business and security logic.

View File

@@ -9,7 +9,7 @@ InvenTree can be configured to send emails to users, for various purposes.
To enable this, email configuration settings must be supplied to the InvenTree [configuration options](../start/config.md#email-settings).
!!! info "Functionality might be degraded"
Multiple functions of InvenTree require functioning email delivery, including *Password Reset*, *Notififications*, *Update Infos*
Multiple functions of InvenTree require functioning email delivery, including *Password Reset*, *Notifications*, *Update Infos*
### Outgoing

View File

@@ -95,6 +95,12 @@ An error occurred while reading the InvenTree configuration file. This might be
Django is not installed in the current Python environment. This means that the InvenTree backend is not running within the correct [python virtual environment](../start/index.md#virtual-environment) or that the required Python packages were not installed correctly.
#### INVE-E15
**Python version not supported**
This error occurs attempting to run InvenTree on a version of Python which is older than the minimum required version. We [require Python {{ config.extra.min_python_version }} or newer](../start/index.md#python-requirements)
### INVE-W (InvenTree Warning)
Warnings - These are non-critical errors which should be addressed when possible.

View File

@@ -78,7 +78,7 @@ If this setting is enabled, users can reset their password via email. This requi
If this setting is enabled, users must have multi-factor authentication enabled to log in.
#### Auto Fil SSO Users
#### Auto Fill SSO Users
Automatically fill out user-details from SSO account-data. If this feature is enabled the user is only asked for their username, first- and surname if those values can not be gathered from their SSO profile. This might lead to unwanted usernames bleeding over.
@@ -174,11 +174,14 @@ Configuration of label printing:
{{ globalsetting("PART_COPY_TESTS") }}
{{ globalsetting("PART_CATEGORY_PARAMETERS") }}
{{ globalsetting("PART_CATEGORY_DEFAULT_ICON") }}
{{ globalsetting("PART_PARAMETER_ENFORCE_UNITS") }}
#### Part Parameter Templates
#### Parameter Templates
Refer to the section describing [how to create part parameter templates](../part/parameter.md#create-template).
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PARAMETER_ENFORCE_UNITS") }}
For more information on parameters, refer to the [parameter documentation](../concepts/parameters.md).
### Categories

View File

@@ -22,6 +22,7 @@ The *Display Settings* screen shows general display configuration options:
{{ usersetting("STICKY_HEADER") }}
{{ usersetting("STICKY_TABLE_HEADER") }}
{{ usersetting("SHOW_SPOTLIGHT") }}
{{ usersetting("BARCODE_IN_FORM_FIELDS") }}
{{ usersetting("DATE_DISPLAY_FORMAT") }}
{{ usersetting("FORMS_CLOSE_USING_ESCAPE") }}
{{ usersetting("DISPLAY_STOCKTAKE_TAB") }}

View File

@@ -143,7 +143,7 @@ pip install django-storages[google]
You will need to change the storage backend, which is set via the `INVENTREE_BACKUP_STORAGE` environment variable, or via `backup_storage` in the configuration file:
```yaml
backup_stoage: storages.backends.gcloud.GoogleCloudStorage
backup_storage: storages.backends.gcloud.GoogleCloudStorage
```
### Configure Backend Options

View File

@@ -38,7 +38,7 @@ First, let's confirm that the gunicorn server is operational.
cd /home/InvenTree
source ./env/bin/activate
cd src/InvenTree
cd src/src/backend/InvenTree
/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi -b 127.0.0.1:8000
```

View File

@@ -92,7 +92,9 @@ The following debugging / logging options are available:
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_DEBUG | debug | Enable [debug mode](./index.md#debug-mode) | False |
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable [query count logging](https://github.com/bradmontgomery/django-querycount) in the terminal | False |
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable support for [django-querycount](../develop/index.md#django-querycount) middleware. | False |
| INVENTREE_DEBUG_SILK | debug_silk | Enable support for [django-silk](../develop/index.md#django-silk) profiling tool. | False |
| INVENTREE_DEBUG_SILK_PROFILING | debug_silk_profiling | Enable detailed profiling in django-silk | False |
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
| INVENTREE_JSON_LOG | json_log | log as json | False |
@@ -103,13 +105,7 @@ The following debugging / logging options are available:
Enabling the `INVENTREE_DEBUG` setting will turn on [Django debug mode]({% include "django.html" %}/ref/settings/#debug). This mode is intended for development purposes, and should not be enabled in a production environment. Read more about [InvenTree debug mode](./index.md#debug-mode).
### Query Count Logging
Enabling the `INVENTREE_DEBUG_QUERYCOUNT` setting will log the number of database queries executed for each page load. This can be useful for identifying performance bottlenecks in the InvenTree server. Note that this setting is only available if `INVENTREE_DEBUG` is also enabled.
### Database Logging
Enabling the `INVENTREE_DB_LOGGING` setting will log all database queries to the terminal. This can be useful for debugging database-related issues.
In debug mode, there are additional [profiling tools](../develop/index.md#profiling-tools) available to help developers analyze and optimize performance.
## Server Access
@@ -266,6 +262,9 @@ The following database options can be configured:
| INVENTREE_DB_HOST | database.HOST | Database host address (if required) | *Not specified* |
| INVENTREE_DB_PORT | database.PORT | Database host port (if required) | *Not specified* |
!!! tip "Database Password"
The value specified for `INVENTREE_DB_PASSWORD` should not contain comma `,` or colon `:` characters, otherwise the connection to the database may fail.
### PostgreSQL Settings
If running with a PostgreSQL database backend, the following additional options are available:
@@ -318,6 +317,9 @@ The following cache settings are available:
| INVENTREE_CACHE_KEEPALIVE_INTERVAL | cache.keepalive_interval | Cache keepalive interval | 1 |
| INVENTREE_CACHE_USER_TIMEOUT | cache.user_timeout | Cache user timeout | 1000 |
!!! tip "Cache Password"
The value specified for `INVENTREE_CACHE_PASSWORD` should not contain comma `,` or colon `:` characters, otherwise the connection to the cache server may fail.
## Email Settings
To enable [email functionality](../settings/email.md), email settings must be configured here, either via environment variables or within the configuration file.
@@ -336,6 +338,10 @@ The following email settings are available:
| INVENTREE_EMAIL_SENDER | email.sender | Sending email address | *Not specified* |
| INVENTREE_EMAIL_PREFIX | email.prefix | Prefix for subject text | [InvenTree] |
### Email Backend
The default email implementation uses the Django STMP backend. This should be sufficient for most implementations, although other backends can be used if required. Note that selection of a different backend requires must use fully qualified module path, and requires advanced knowledge.
### Sender Email
The "sender" email address is the address from which InvenTree emails are sent (by default) and must be specified for outgoing emails to function:
@@ -482,7 +488,7 @@ The INVENTREE_CUSTOMIZE environment variable must contain a json object with the
the wanted values. Example:
```
INVENTREE_CUSTOMIZE={"login_message":"Hallo Michi"}
INVENTREE_CUSTOMIZE={"login_message":"Hello World"}
```
This example sets a login message. Take care of the double quotes.

View File

@@ -235,6 +235,21 @@ The [Caddy](./docker.md#ssl-certificates) container will automatically generate
Any persistent files generated by the Caddy container (such as certificates, etc) will be stored in the `caddy` directory within the external volume.
### Web Server Bind Address
By default, the Dockerized InvenTree web server binds to all available network interfaces and listens for IPv4 traffic on port 8000.
This can be adjusted using the following environment variables:
| Environment Variable | Default |
| --- | --- | --- | --- |
| INVENTREE_WEB_ADDR | 0.0.0.0 |
| INVENTREE_WEB_PORT | 8000 |
These variables are combined in the [Dockerfile]({{ sourcefile("contrib/container/Dockerfile") }}) to build the bind string passed to the InvenTree server on startup.
!!! tip "IPv6 Support"
To enable IPv6/Dual Stack support, set `INVENTREE_WEB_ADDR` to `[::]` when you create/start the container.
### Demo Dataset
To quickly get started with a [demo dataset](../demo.md), you can run the following command:

View File

@@ -123,11 +123,14 @@ By default, a production InvenTree installation is configured to run with [DEBUG
Running in DEBUG mode provides many handy development features, however it is strongly recommended *NOT* to run in DEBUG mode in a production environment. This recommendation is made because DEBUG mode leaks a lot of information about your installation and may pose a security risk.
So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
So, for a production setup, you should ensure that `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
!!! warning "Security Risk"
Running InvenTree in DEBUG mode in a production environment is a significant security risk, and should be avoided at all costs.
### Turning Debug Mode off
When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the proxy setup has to be configured to handle this.
When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled* (which is the default for a production setup), the proxy setup has to be configured to handle this.
!!! info "Read More"
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details

View File

@@ -108,6 +108,27 @@ The Python packages required by the InvenTree server must be installed into the
```
pip install --upgrade --ignore-installed invoke
```
#### Install Python Bindings
Depending on your database the python bindings must also be installed (into your virtual environment).
For PostgreSQL install:
```
pip3 install psycopg pgcli
```
For MySQL install:
```
pip3 install mysqlclient mariadb
```
If all packages have been installed run:
```
invoke install
```
@@ -155,14 +176,6 @@ grant all privileges on database inventree to myuser;
!!! info "Username / Password"
You should change the username and password from the values specified above. This username and password will also be for the InvenTree database connection configuration.
#### Install Python Bindings
The PostgreSQL python binding must also be installed (into your virtual environment):
```
pip3 install psycopg pgcli
```
#### Install Postgresql client
If PostgreSQL and InvenTree are installed on separate servers / containers the PostgreSQL client has to be installed also where InvenTree is running.
@@ -184,14 +197,6 @@ To run InvenTree with the MySQL or MariaDB backends, a number of extra packages
sudo apt-get install mysql-server libmysqlclient-dev
```
#### Install Python Bindings
Install the python bindings for MySQL (into the python virtual environment).
```
pip3 install mysqlclient mariadb
```
#### Create Database
Assuming the MySQL server is installed and running, login to the MySQL server as follows:

View File

@@ -114,3 +114,86 @@ Once the migration process completes, the database records are now updated! Rest
!!! tip "Example: Docker"
If running under docker, run `docker compose up -d`
## Migrating Between Incompatible Database Versions
There may be occasions when InvenTree data needs to be migrated between two database installations running *incompatible* versions of the database software. For example, InvenTree may be running on a Postgres database running on version 12, and the administrator wishes to migrate the data to a Postgres version 17 database.
!!! warning "Advanced Procedure"
The following procedure is *advanced*, and should only be attempted by experienced database administrators. Always ensure that database backups are made before attempting any migration procedure.
Due to inherit incompatibilities between different major versions of database software, it is not always possible to directly migrate data between two different database versions. In such cases, the following procedure can be used as a workaround.
!!! warning "InvenTree Version"
It is *crucial* that both InvenTree database installations are running the same version of InvenTree software! If this is not the case, data migration may fail, and there is a possibility that data corruption can occur. Ensure that the original database is up to date, by running `invoke update`.
The following instructions assume that the source (old) database is Postgres version 15, and the target (new) database is Postgres version 17. Additionally, it assumes that the InvenTree installation is running under [docker / docker compose](./docker.md), for simplicity. Adjust commands as required for other InvenTree configurations or database software.
The overall procedure is as follows:
### Backup Old Database
Run the following command to create a backup dump of the old database:
```
docker compose run --rm inventree-server invoke backup
```
This will create a database backup file in the InvenTree backup directory.
!!! tip "Secondary Backup"
It may be prudent to create a secondary backup of the database, separate to the one created by InvenTree.
### Shutdown Old Database
Stop the old InvenTree installation, to ensure that the database is not being accessed during the migration process:
```
docker compose down
```
### Remove Old Database Files
The raw database files are incompatible between different major versions of Postgres. Thus, the old database files must be removed before starting the new database.
!!! warning "Data Loss"
Ensure that a complete backup of the old database has been made before proceeding! Removing the database files will result in data loss if a backup does not exist.
```
rm -rf ./path/to/database/*
```
!!! info "Database Location"
The location of the database files depends on how InvenTree was configured.
### Start New Database
Update the InvenTree docker configuration to use the new version of Postgres (e.g. `postgres:17`), and then start the InvenTree installation:
```
docker compose up -d
```
This will initialize a new, empty database using the new version of Postgres.
### Run Database Migrations
Run the database migration process to ensure that the new database schema is correctly initialized:
```
docker compose run --rm inventree-server invoke update
```
### Restore Database Backup
Finally, restore the database backup created earlier into the new database:
```
docker compose run --rm inventree-server invoke restore
```
This will load the database records from the backup file into the new database.
### Caveats
The process described here is a *suggested* procedure for migrating between incompatible database versions. However, due to the complexity of database software, there may be unforeseen complications that arise during the process.

View File

@@ -0,0 +1,88 @@
---
title: Stock Availability
---
## Stock Availability
Each stock item represents a physical quantity of a particular part in a specific location. Given the complexities of tracking stock across multiple locations, reservations, and allocations, it is important to understand whether a stock item is actually available for use.
There are multiple terms used in InvenTree to describe stock availability:
### Required
The *Required* quantity indicates the total number of a certain [part](../part/index.md) that is needed to fulfill all current orders, builds, or other commitments. This is independent of the actual stock levels.
### In Stock
The *In Stock* quantity represents the total number of items physically present in the stock location, regardless of their allocation or reservation status.
For example, if a stock item has a quantity of 100, it means there are 100 units of that part physically present in the specified location.
### Allocated
The *Allocated* quantity indicates the number of stock items that have been reserved for specific orders, builds, or other commitments. This number is specified against each stock item when it is allocated.
Note that the *Allocated* quantity is always less than or equal to the *In Stock* quantity.
For example, if a stock item has an *In Stock* quantity of 100, and 30 units have been allocated to fulfill orders, then the *Allocated* quantity is 30. In this case the *In Stock* quantity remains 100, until the alloacted quantity is actually consumed.
### Available
The *Available* quantity for a given stock item is the difference between the *In Stock* quantity and the *Allocated* quantity.
For example, if a stock item has an *In Stock* quantity of 100, and 30 units have been allocated, then the *Available* quantity is 70.
## Consumed or Depleted Stock
Once allocated stock quantities are consumed (e.g. when fulfilling an order or completing a build), the *In Stock* quantity of the stock item is reduced accordingly. This means that the *Available* quantity is also reduced.
### Delete on Deplete
Stock items can be designated with the `Delete on Deplete` flag. When this flag is set, if the stock quantity of the item reaches zero, the stock item will be automatically deleted from the system.
## Part Stock Levels
There are multiple ways to view the overall stock levels for a given part across all stock items and locations.
### Stock Overview
Each [part](../part/index.md) page displays an overview of the stocktotal stock levels across all locations. This page shows the total *In Stock*, *Allocated*, and *Available* quantities for the part, aggregated across all stock items:
{{ image("stock/stock_overview.png", title="Stock overview") }}
In the example above, the "Red Widget" part has the following stock levels:
| Metric | Quantity | Description |
| ------ | -------- | ----------- |
| In Stock | 138 | There are 138 physical units of the Red Widget part across all stock locations. |
| Available | 123 | Only 123 units are available for allocation, as 15 units have already been allocated to fulfill orders. |
| Required | 657 | A total of 657 units of the Red Widget part are needed to fulfill all current orders and builds. |
| Deficit | 519 | There is a deficit of 519 units, meaning that the current available stock is insufficient to meet the required quantity. |
### Stock Details
The *Part Detail* view displays additional information regarding the stock levels for the part:
{{ image("stock/stock_detail.png", title="Stock detail") }}
Here we can see that:
- There are 138 units of the Red Widget part physically in stock across all locations.
- Of this quantity, 15 units have been allocated to fulfill orders, leaving 123 units available for allocation.
- A total of 657 units are required to fulfill all current orders and builds.
- 645 units are required to fulfil outstanding build orders, of which 15 units have already been allocated.
- 12 units are required to fulfil outstanding sales orders, none of which have been allocated yet.
### Allocations Tab
To view further details regarding which stock items have been allocated, the *Allocations* tab on the part view can be used.
This tab displays a complete overview of where the stock items for this part are allocated, against any open orders:
#### Build Order Allocations
{{ image("stock/build_order_allocations.png", title="Build order allocations") }}
#### Sales Order Allocations
{{ image("stock/sales_order_allocations.png", title="Sales order allocations") }}

View File

@@ -26,11 +26,15 @@ The *Stock Item* detail view shows information regarding the particular stock it
**Status** - Status of this stock item
### Stock Availability
InvenTree has a number of different mechanisms to determine whether stock is available for use. See the [Stock Availability](./availability.md) page for more information.
### Stock Tracking
Every time a *Stock Item* is adjusted, a *Stock Tracking* entry is automatically created. This ensures a complete history of the *Stock Item* is maintained as long as the item is in the system.
Each stock tracking historical item records the user who performed the action.
Each stock tracking historical item records the user who performed the action. [Read more about stock tracking here](./tracking.md).
## Stock Location
@@ -49,12 +53,10 @@ If there are some icons missing in the tabler icons package, users can even inst
A stock location type represents a specific type of location (e.g. one specific size of drawer, shelf, ... or box) which can be assigned to multiple stock locations. In the first place, it is used to specify an icon and having the icon in sync for all locations that use this location type, but it also serves as a data field to quickly see what type of location this is. It is planned to add e.g. drawer dimension information to the location type to add a "find a matching, empty stock location" tool.
## External Stock Location
An external stock location can be used to indicate that items in there might not be available
for immediate usage. Stock items in an external location are marked with an additional icon
It may be useful to mark certain stock locations as *external*. An external stock location can be used to indicate that items in there might not be available for immediate usage. Stock items in an external location are marked with an additional icon
in the build order line items view where the material is allocated.
{{ image("stock/stock_external_icon.png", title="External stock indication") }}
Anyhow there is no limitation on the stock item. It can be allocated as usual.
The external flag does not get inherited to sublocations.

View File

@@ -132,6 +132,17 @@ If a provided serial number (or group of numbers) is not considered valid, an er
!!! tip "Serial Number Validation"
Custom serial number validation can be implemented using an external plugin
#### Adjusting Serial Numbers
Once a stock item has been created with a serial number, it is possible to adjust that serial number value if required. This can be achieved by navigating to the stock item's detail page, and selecting the "Edit" option from the actions menu. Here, the serial number value can be modified as required:
{{ image("stock/serial_edit.png", title="Editing a serial number") }}
Note that any serial number adjustments are subject to the same validation rules as when the stock item was created. If the new serial number value is not valid, an error message will be displayed to the user:
{{ image("stock/serial_edit_error.png", title="Error while editing a serial number") }}
#### Plugin Support
Custom serial number functionality, with any arbitrary requirements or level of complexity, can be implemented using the [Validation Plugin Mixin class](../plugins/mixins/validation.md#serial-numbers). Refer to the documentation for this plugin for technical details.

View File

@@ -9,6 +9,12 @@ If you are struggling with an issue which is not covered in the FAQ above, pleas
Even if you cannot immediately resolve the issue, the information below will be very useful when reporting the issue on GitHub.
## Error Codes
InvenTree uses a system of error codes to help identify specific issues. Each error code is prefixed with `INVE-`, followed by a letter indicating the error type, and a number.
Refer to the [error code documentation](./settings/error_codes.md) for more information on specific error codes.
## Recent Update
If you have recently updated your InvenTree instance, please ensure that you have followed all update instructions carefully. In particular, make sure that you have run any required database migrations using the `invoke update` command.

View File

@@ -78,6 +78,7 @@ nav:
- Demo: demo.md
- Release Notes: releases/release_notes.md
- Development:
- InvenTree Development: develop/index.md
- Contributing: develop/contributing.md
- Architecture: develop/architecture.md
- Roadmap: develop/roadmap.md
@@ -96,6 +97,8 @@ nav:
- Custom States: concepts/custom_states.md
- Pricing: concepts/pricing.md
- Project Codes: concepts/project_codes.md
- Attachments: concepts/attachments.md
- Parameters: concepts/parameters.md
- Barcodes:
- Barcode Support: barcodes/index.md
- Internal Barcodes: barcodes/internal.md
@@ -125,7 +128,6 @@ nav:
- Virtual Parts: part/virtual.md
- Part Views: part/views.md
- Tracking: part/trackable.md
- Parameters: part/parameter.md
- Revisions: part/revision.md
- Templates: part/template.md
- Tests: part/test.md
@@ -134,8 +136,9 @@ nav:
- Notifications: part/notification.md
- Stock:
- Stock Items: stock/index.md
- Stock Status: stock/status.md
- Availability: stock/availability.md
- Stock Tracking: stock/tracking.md
- Stock Status: stock/status.md
- Adjusting Stock: stock/adjust.md
- Stock Expiry: stock/expiry.md
- Stock Ownership: stock/owner.md
@@ -237,7 +240,7 @@ nav:
- Export Plugins:
- BOM Exporter: plugins/builtin/bom_exporter.md
- InvenTree Exporter: plugins/builtin/inventree_exporter.md
- Parameter Exporter: plugins/builtin/part_parameter_exporter.md
- Parameter Exporter: plugins/builtin/parameter_exporter.md
- Stocktake Exporter: plugins/builtin/stocktake_exporter.md
- Label Printing:
- Label Printer: plugins/builtin/inventree_label.md
@@ -357,7 +360,7 @@ extra:
# provider: google
# property: UA-143467500-1
min_python_version: 3.10
min_python_version: 3.11
min_invoke_version: 2.0.0
django_version: 5.2
docker_postgres_version: 17

View File

@@ -6,5 +6,5 @@ mkdocs-redirects
mkdocs-simple-hooks>=0.1,<1.0
mkdocs-include-markdown-plugin
neoteroi-mkdocs
mkdocstrings[python]>=0.25.0,<=0.30.1
mkdocstrings[python]>=0.25.0,<=1.0.0
mkdocs-mermaid2-plugin

View File

@@ -148,9 +148,9 @@ essentials==1.1.6 \
--hash=sha256:3fd26923f5f2ece51a219dbb17b1fb22c9190d70fa2104919be92a6419521877 \
--hash=sha256:a470e693d83c13369ebf1f488d60236b4ea99400f38db6b7224e2808c1369256
# via essentials-openapi
essentials-openapi==1.2.1 \
--hash=sha256:70b06d80a8d9cb7634b702903cfe8fcfc48e6fc054e744eab514bb7bc37f00c9 \
--hash=sha256:d0085cde3ceaa25e6fc4983d8372ac0185909e3fa2791f498280379bb3c86c62
essentials-openapi==1.3.0 \
--hash=sha256:453327a0a847a431133f4472ced7e4a9180bf667437049b57381ddf88079e886 \
--hash=sha256:9c2a88531e2c70c565d5b526d74043941e46f60c114f7a0e3ae91e9e6bef4dae
# via neoteroi-mkdocs
ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
@@ -354,9 +354,9 @@ mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in
mkdocstrings[python]==0.30.1 \
--hash=sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82 \
--hash=sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f
mkdocstrings[python]==1.0.0 \
--hash=sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a \
--hash=sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa
# via
# -r docs/requirements.in
# mkdocstrings-python
@@ -364,9 +364,9 @@ mkdocstrings-python==1.16.12 \
--hash=sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374 \
--hash=sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d
# via mkdocstrings
neoteroi-mkdocs==1.1.3 \
--hash=sha256:3ecfb825e898d10a6d703a3ef3f0484d823b7b5660425e76af421e316ac18036 \
--hash=sha256:772aee317c9bb10a89d67e71e322730f92cc349d5eecc8a08e8fb079398e514b
neoteroi-mkdocs==1.2.0 \
--hash=sha256:58e25cb1b9db093ffa8d12bdb33264bf567cac30fb964b56e0a493efa749ad6e \
--hash=sha256:91b6aa95a4e46c9bb93e00e021d2044cb0c7d80c0b4600434ff8f440d613a0a8
# via -r docs/requirements.in
packaging==25.0 \
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
@@ -539,9 +539,9 @@ typing-extensions==4.14.1 \
# via
# anyio
# beautifulsoup4
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
urllib3==2.6.0 \
--hash=sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f \
--hash=sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1
# via requests
watchdog==6.0.0 \
--hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \

View File

@@ -101,8 +101,8 @@ python-version = "3.11.0"
no-strip-extras=true
generate-hashes=true
[tool.ty.src]
root = "src/backend/InvenTree"
[tool.ty.environment]
root = ["src/backend/InvenTree"]
[tool.ty.rules]
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
@@ -133,3 +133,9 @@ sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
[tool.codespell]
ignore-words-list = ["assertIn","SME","intoto","fitH"]
[tool.pytest]
django_find_project = false
pythonpath = ["src/backend/InvenTree"]
DJANGO_SETTINGS_MODULE = "InvenTree.settings"
python_files = ["test*.py",]

View File

@@ -600,6 +600,34 @@ class BulkUpdateMixin(BulkOperationMixin):
return Response({'success': f'Updated {n} items'}, status=200)
class ParameterListMixin:
"""Mixin class which supports filtering against parametric fields."""
def filter_queryset(self, queryset):
"""Perform filtering against parametric fields."""
import common.filters
queryset = super().filter_queryset(queryset)
# Filter by parametric data
queryset = common.filters.filter_parametric_data(
queryset, self.request.query_params
)
serializer_class = (
getattr(self, 'serializer_class', None) or self.get_serializer_class()
)
model_class = serializer_class.Meta.model
# Apply ordering based on query parameter
queryset = common.filters.order_by_parameter(
queryset, model_class, self.request.query_params.get('ordering', None)
)
return queryset
class BulkDeleteMixin(BulkOperationMixin):
"""Mixin class for enabling 'bulk delete' operations for various models.

View File

@@ -1,11 +1,45 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 427
INVENTREE_API_VERSION = 435
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v435 -> 2025-12-16 : https://github.com/inventree/InvenTree/pull/11030
- Adds token refresh endpoint to auth API
v434 -> 2025-12-16 : https://github.com/inventree/InvenTree/pull/11021
- The "tags" fields (on various API endpoints) is now optional, and disabled by default
- To request tags information, add "tags=true" to the API request query parameters
v433 -> 2025-12-16 : https://github.com/inventree/InvenTree/pull/11023
- "substitutes" field on the BomItem API endpoint is now excluded by default
- Add "?substitutes=true" query parameter to include substitute parts in BomItem API endpoint(s)
v432 -> 2025-12-15 : https://github.com/inventree/InvenTree/pull/11012
- The "part_detail" field on the SupplierPart API endpoint is now optional
- The "supplier_detail" field on the SupplierPart API endpoint is now optional
- The "manufacturer_detail" field on the ManufacturerPart API endpoint is now optional
- The "part_detail" field on the StockItem API is now disabled by default
v431 -> 2025-12-14 : https://github.com/inventree/InvenTree/pull/11006
- Remove duplicate "address" field on the Company API endpoint
- Make "primary_address" field optional on the Company API endpoint
- Remove "address_count" field from the Company API endpoint
v430 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10699
- Removed the "PartParameter" and "PartParameterTemplate" API endpoints
- Removed the "ManufacturerPartParameter" API endpoint
- Added generic "Parameter" and "ParameterTemplate" API endpoints
v429 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10938
- Adjust default values for currency codes in the API schema
- Note that this does not change any functional behavior, only the schema documentation
v428 -> 2025-11-28 : https://github.com/inventree/InvenTree/pull/10926
- Various typo fixes in API - no functional changes
v427 -> 2025-11-24 : https://github.com/inventree/InvenTree/pull/10896
- Fixes a spelling mistake in the API field labels
@@ -42,7 +76,7 @@ v418 -> 2025-10-24 : https://github.com/inventree/InvenTree/pull/10657
v417 -> 2025-10-22 : https://github.com/inventree/InvenTree/pull/10654
- Adds "checked" filter to SalesOrderShipment API endpoint
- Adds "order_status" filter to SalesOrdereShipment API endpoint
- Adds "order_status" filter to SalesOrderShipment API endpoint
- Adds "order_outstanding" filter to SalesOrderShipment API endpoint
v416 -> 2025-10-22 : https://github.com/inventree/InvenTree/pull/10651
@@ -100,7 +134,7 @@ v401 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10381
- Adds machine properties to machine API endpoints
v400 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10486
- Adds return datatypes for admin/config and flags entpoints
- Adds return datatypes for admin/config and flags endpoints
v399 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10445
- Refactors 'customer_detail' param in SalesOrder API endpoint

View File

@@ -18,6 +18,7 @@ import InvenTree.conversion
import InvenTree.ready
import InvenTree.tasks
from InvenTree.config import get_setting
from InvenTree.ready import ignore_ready_warning
logger = structlog.get_logger('inventree')
MIGRATIONS_CHECK_DONE = False
@@ -70,9 +71,7 @@ class InvenTreeConfig(AppConfig):
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
self.update_site_url()
# Ensure the unit registry is loaded
InvenTree.conversion.get_unit_registry()
self.load_unit_registry()
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
self.add_user_on_startup()
@@ -84,6 +83,7 @@ class InvenTreeConfig(AppConfig):
social_account_updated.connect(sso.ensure_sso_groups)
@ignore_ready_warning
def remove_obsolete_tasks(self):
"""Delete any obsolete scheduled tasks in the database."""
obsolete = [
@@ -112,6 +112,7 @@ class InvenTreeConfig(AppConfig):
except Exception:
logger.exception('Failed to remove obsolete tasks - database not ready')
@ignore_ready_warning
def start_background_tasks(self):
"""Start all background tests for InvenTree."""
logger.info('Starting background tasks...')
@@ -171,6 +172,7 @@ class InvenTreeConfig(AppConfig):
logger.info('Started %s scheduled background tasks...', len(tasks))
@ignore_ready_warning
def add_heartbeat(self):
"""Ensure there is at least one background task in the queue."""
import django_q.models
@@ -185,6 +187,7 @@ class InvenTreeConfig(AppConfig):
except Exception:
pass
@ignore_ready_warning
def collect_tasks(self):
"""Collect all background tasks."""
for app_name, app in apps.app_configs.items():
@@ -197,6 +200,7 @@ class InvenTreeConfig(AppConfig):
except Exception as e: # pragma: no cover
logger.exception('Error loading tasks for %s: %s', app_name, e)
@ignore_ready_warning
def update_site_url(self):
"""Update the site URL setting.
@@ -223,6 +227,12 @@ class InvenTreeConfig(AppConfig):
except Exception:
pass
@ignore_ready_warning
def load_unit_registry(self):
"""Ensure the unit registry is loaded."""
InvenTree.conversion.get_unit_registry()
@ignore_ready_warning
def add_user_on_startup(self):
"""Add a user on startup."""
# stop if checks were already created
@@ -281,6 +291,7 @@ class InvenTreeConfig(AppConfig):
except IntegrityError:
logger.warning('The user "%s" could not be created', add_user)
@ignore_ready_warning
def add_user_from_file(self):
"""Add the superuser from a file."""
# stop if checks were already created

View File

@@ -4,6 +4,8 @@ import socket
import threading
from typing import Any
from django.db.utils import OperationalError, ProgrammingError
import structlog
import InvenTree.config
@@ -169,3 +171,22 @@ def set_session_cache(key: str, value: Any) -> None:
if request_cache is not None:
request_cache[key] = value
def get_cached_content_types(cache_key: str = 'all_content_types') -> list:
"""Return a list of all ContentType objects, using session cache if possible."""
from django.contrib.contenttypes.models import ContentType
# Attempt to retrieve a list of ContentType objects from session cache
if content_types := get_session_cache(cache_key):
return content_types
try:
content_types = list(ContentType.objects.all())
if len(content_types) > 0:
set_session_cache(cache_key, content_types)
except (OperationalError, ProgrammingError):
# Database is likely not yet ready
content_types = []
return content_types

View File

@@ -24,7 +24,13 @@ from common.notifications import (
trigger_notification,
)
from common.settings import get_global_setting
from InvenTree.cache import (
get_cached_content_types,
get_session_cache,
set_session_cache,
)
from InvenTree.format import format_money
from InvenTree.ready import ignore_ready_warning
logger = structlog.get_logger('inventree')
@@ -260,6 +266,7 @@ def render_currency(
)
@ignore_ready_warning
def getModelsWithMixin(mixin_class) -> list:
"""Return a list of database models that inherit from the given mixin class.
@@ -268,17 +275,23 @@ def getModelsWithMixin(mixin_class) -> list:
Returns:
List of models that inherit from the given mixin class
"""
from django.contrib.contenttypes.models import ContentType
# First, look in the session cache - to prevent repeated expensive comparisons
cache_key = f'models_with_mixin_{mixin_class.__name__}'
try:
db_models = [
x.model_class() for x in ContentType.objects.all() if x is not None
]
except (OperationalError, ProgrammingError):
# Database is likely not yet ready
db_models = []
if cached_models := get_session_cache(cache_key):
return cached_models
return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
content_types = get_cached_content_types()
db_models = [x.model_class() for x in content_types if x is not None]
models_with_mixin = [
x for x in db_models if x is not None and issubclass(x, mixin_class)
]
# Store the result in the session cache
set_session_cache(cache_key, models_with_mixin)
return models_with_mixin
def notify_responsible(

View File

@@ -70,8 +70,10 @@ class Command(spectacular.Command):
# Reformat paths
for p_name, p_spec in spec['paths'].items():
# strip path name
p_name = p_name.removeprefix(dja_path_prefix).removeprefix(
'/_allauth/browser/v1/'
p_name = (
p_name.removeprefix(dja_path_prefix)
.removeprefix('/_allauth/browser/v1/')
.removeprefix('/_allauth/app/v1/')
)
# fix refs

View File

@@ -23,7 +23,7 @@ logger = structlog.get_logger('inventree')
class InvenTreeMetadata(SimpleMetadata):
"""Custom metadata class for the DRF API.
This custom metadata class imits the available "actions",
This custom metadata class limits the available "actions",
based on the user's role permissions.
Thus when a client send an OPTIONS request to an API endpoint,
@@ -50,6 +50,10 @@ class InvenTreeMetadata(SimpleMetadata):
for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
view.request = clone_request(request, method)
# Mark this request, to prevent expensive prefetching
view.request._metadata_requested = True
try:
# Test global permissions
if hasattr(view, 'check_permissions'):
@@ -420,6 +424,13 @@ class InvenTreeMetadata(SimpleMetadata):
if field_info['type'] == 'dependent field':
field_info['depends_on'] = field.depends_on
# Extends with extra attributes from the serializer
extra_field_attributes = ['allow_blank', 'allow_null']
for attr in extra_field_attributes:
if hasattr(field, attr):
field_info[attr] = getattr(field, attr)
# Extend field info if the field has a get_field_info method
if (
not field_info.get('read_only')

View File

@@ -258,6 +258,19 @@ class OutputOptionsMixin:
return serializer
def get_queryset(self):
"""Return the queryset with output options applied.
This automatically applies any prefetching defined against the optional fields.
"""
queryset = super().get_queryset()
serializer = self.get_serializer()
if isinstance(serializer, FilterableSerializerMixin):
queryset = serializer.prefetch_queryset(queryset)
return queryset
class SerializerContextMixin:
"""Mixin to add context to serializer."""

View File

@@ -6,10 +6,13 @@ from string import Formatter
from typing import Any, Optional
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db import models, transaction
from django.db.models import QuerySet
from django.db.models.signals import post_save
from django.db.transaction import TransactionManagementError
from django.dispatch import receiver
from django.urls import resolve, reverse
from django.urls.exceptions import NoReverseMatch
@@ -449,7 +452,18 @@ class ReferenceIndexingMixin(models.Model):
reference_int = models.BigIntegerField(default=0)
class InvenTreeModel(PluginValidationMixin, models.Model):
class ContentTypeMixin:
"""Mixin class which supports retrieval of the ContentType for a model instance."""
@classmethod
def get_content_type(cls):
"""Return the ContentType object associated with this model."""
from django.contrib.contenttypes.models import ContentType
return ContentType.objects.get_for_model(cls)
class InvenTreeModel(ContentTypeMixin, PluginValidationMixin, models.Model):
"""Base class for InvenTree models, which provides some common functionality.
Includes the following mixins by default:
@@ -472,7 +486,167 @@ class InvenTreeMetadataModel(MetadataMixin, InvenTreeModel):
abstract = True
class InvenTreeAttachmentMixin:
class InvenTreePermissionCheckMixin:
"""Provides an abstracted class for managing permissions against related fields."""
@classmethod
def check_related_permission(cls, permission, user) -> bool:
"""Check if the user has permission to perform the specified action on the attachment.
The default implementation runs a permission check against *this* model class,
but this can be overridden in the implementing class if required.
Arguments:
permission: The permission to check (add / change / view / delete)
user: The user to check against
Returns:
bool: True if the user has permission, False otherwise
"""
perm = f'{cls._meta.app_label}.{permission}_{cls._meta.model_name}'
return user.has_perm(perm)
class InvenTreeParameterMixin(InvenTreePermissionCheckMixin, models.Model):
"""Provides an abstracted class for managing parameters.
Links the implementing model to the common.models.Parameter table,
and provides the following methods:
"""
class Meta:
"""Metaclass options for InvenTreeParameterMixin."""
abstract = True
# Define a reverse relation to the Parameter model
parameters_list = GenericRelation(
'common.Parameter', content_type_field='model_type', object_id_field='model_id'
)
@staticmethod
def annotate_parameters(queryset: QuerySet) -> QuerySet:
"""Annotate a queryset with pre-fetched parameters.
Args:
queryset: Queryset to annotate
Returns:
Annotated queryset
"""
return queryset.prefetch_related(
'parameters_list',
'parameters_list__model_type',
'parameters_list__updated_by',
'parameters_list__template',
'parameters_list__template__model_type',
)
@property
def parameters(self) -> QuerySet:
"""Return a QuerySet containing all the Parameter instances for this model.
This will return pre-fetched data if available (i.e. in a serializer context).
"""
# Check the query cache for pre-fetched parameters
if cache := getattr(self, '_prefetched_objects_cache', None):
if 'parameters_list' in cache:
return cache['parameters_list']
return self.parameters_list.all()
def delete(self, *args, **kwargs):
"""Handle the deletion of a model instance.
Before deleting the model instance, delete any associated parameters.
"""
self.parameters_list.all().delete()
super().delete(*args, **kwargs)
@transaction.atomic
def copy_parameters_from(self, other, clear=True, **kwargs):
"""Copy all parameters from another model instance.
Arguments:
other: The other model instance to copy parameters from
clear: If True, clear existing parameters before copying
**kwargs: Additional keyword arguments to pass to the Parameter constructor
"""
import common.models
if clear:
self.parameters_list.all().delete()
parameters = []
content_type = ContentType.objects.get_for_model(self.__class__)
template_ids = [parameter.template.pk for parameter in other.parameters.all()]
# Remove all conflicting parameters first
self.parameters_list.filter(template__pk__in=template_ids).delete()
for parameter in other.parameters.all():
parameter.pk = None
parameter.model_id = self.pk
parameter.model_type = content_type
parameters.append(parameter)
if len(parameters) > 0:
common.models.Parameter.objects.bulk_create(parameters)
def get_parameter(self, name: str):
"""Return a Parameter instance for the given parameter name.
Args:
name: Name of the parameter template
Returns:
Parameter instance if found, else None
"""
return self.parameters_list.filter(template__name=name).first()
def get_parameters(self) -> QuerySet:
"""Return all Parameter instances for this model."""
return (
self.parameters_list.all()
.prefetch_related('template', 'model_type')
.order_by('template__name')
)
def parameters_map(self) -> dict:
"""Return a map (dict) of parameter values associated with this Part instance, of the form.
Example:
{
"name_1": "value_1",
"name_2": "value_2",
}
"""
params = {}
for parameter in self.parameters.all().prefetch_related('template'):
params[parameter.template.name] = parameter.data
return params
def check_parameter_delete(self, parameter):
"""Run a check to determine if the provided parameter can be deleted.
The default implementation always returns True, but this can be overridden in the implementing class.
"""
return True
def check_parameter_save(self, parameter):
"""Run a check to determine if the provided parameter can be saved.
The default implementation always returns True, but this can be overridden in the implementing class.
"""
return True
class InvenTreeAttachmentMixin(InvenTreePermissionCheckMixin):
"""Provides an abstracted class for managing file attachments.
Links the implementing model to the common.models.Attachment table,
@@ -490,33 +664,15 @@ class InvenTreeAttachmentMixin:
super().delete(*args, **kwargs)
@property
def attachments(self):
def attachments(self) -> QuerySet:
"""Return a queryset containing all attachments for this model."""
return self.attachments_for_model().filter(model_id=self.pk)
@classmethod
def check_attachment_permission(cls, permission, user) -> bool:
"""Check if the user has permission to perform the specified action on the attachment.
The default implementation runs a permission check against *this* model class,
but this can be overridden in the implementing class if required.
Arguments:
permission: The permission to check (add / change / view / delete)
user: The user to check against
Returns:
bool: True if the user has permission, False otherwise
"""
perm = f'{cls._meta.app_label}.{permission}_{cls._meta.model_name}'
return user.has_perm(perm)
def attachments_for_model(self):
def attachments_for_model(self) -> QuerySet:
"""Return all attachments for this model class."""
from common.models import Attachment
model_type = self.__class__.__name__.lower()
return Attachment.objects.filter(model_type=model_type)
def create_attachment(self, attachment=None, link=None, comment='', **kwargs):
@@ -532,7 +688,7 @@ class InvenTreeAttachmentMixin:
Attachment.objects.create(**kwargs)
class InvenTreeTree(MPTTModel):
class InvenTreeTree(ContentTypeMixin, MPTTModel):
"""Provides an abstracted self-referencing tree model, based on the MPTTModel class.
Our implementation provides the following key improvements:
@@ -763,7 +919,15 @@ class InvenTreeTree(MPTTModel):
if len(trees) > 0:
# A tree update was performed, so we need to refresh the instance
self.refresh_from_db()
try:
self.refresh_from_db()
except TransactionManagementError:
# If we are inside a transaction block, we cannot refresh from db
pass
except Exception as e:
# Any other error is unexpected
InvenTree.sentry.report_exception(e)
InvenTree.exceptions.log_error(f'{self.__class__.__name__}.save')
def partial_rebuild(self, tree_id: int) -> bool:
"""Perform a partial rebuild of the tree structure.

View File

@@ -149,7 +149,7 @@ class InvenTreeRoleScopeMixin(OASTokenMixin):
class InvenTreeTokenMatchesOASRequirements(InvenTreeRoleScopeMixin):
"""Combines InvenTree role-based scope handling with OpenAPI schema token requirements.
Usesd as default permission class.
Used as default permission class.
"""
def has_permission(self, request, view):
@@ -166,6 +166,29 @@ class InvenTreeTokenMatchesOASRequirements(InvenTreeRoleScopeMixin):
return True
class ModelPermission(permissions.DjangoModelPermissions):
"""Custom ModelPermission implementation which provides cached lookup of queryset.
This is entirely for optimization purposes.
"""
def _queryset(self, view):
"""Return the queryset associated with this view, with caching.
This is because in a metadata OPTIONS request, the view is copied multiple times.
We can cache the queryset to avoid repeated calculation.
"""
if getattr(view, '_cached_queryset', None) is not None:
return view._cached_queryset
queryset = super()._queryset(view)
if queryset is not None:
view._cached_queryset = queryset
return queryset
class RolePermission(InvenTreeRoleScopeMixin, permissions.BasePermission):
"""Role mixin for API endpoints, allowing us to specify the user "role" which is required for certain operations.

View File

@@ -0,0 +1,130 @@
"""Helper functions for profiling InvenTree code.
A set of decorators to assist with profiling functions and logging,
which implement oft-repeated patterns used during development and debugging.
Note: These functions are not to be used in production code.
"""
from functools import wraps
def ensure_debug():
"""Ensure that InvenTree is running in DEBUG mode."""
from django.conf import settings
if not settings.DEBUG:
raise RuntimeError('Profiling functions can only be used in DEBUG mode!')
def time_function(func): # pragma: no cover
"""Decorator to time a function's execution duration.
Args:
func: Function to be timed
"""
@wraps(func)
def wrapper(*args, **kwargs):
import time
ensure_debug()
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
duration = end_time - start_time
print(f"Function '{func.__name__}' executed in {duration:.6f} seconds.")
return result
return wrapper
def profile_function(filename='profile.prof'): # pragma: no cover
"""Decorator to profile a function using cProfile.
Args:
func: Function to be profiled
filename: Output filename for the profiling data
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import cProfile
import io
import pstats
ensure_debug()
pr = cProfile.Profile()
pr.enable()
result = func(*args, **kwargs)
pr.disable()
s = io.StringIO()
sortby = pstats.SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.dump_stats(filename)
print(s.getvalue())
return result
return wrapper
return decorator
def log_slow_queries(
threshold: float = 0.01, n: int = 5, log_to_file: bool = True
): # pragma: no cover
"""Decorator to log slow database queries in a Django view function.
Args:
func: Function to be decorated
threshold: Time threshold (in seconds) for logging slow queries
n: Number of slowest queries to log
log_to_file: Whether to log to a file or print to console
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
from django.db import connection
ensure_debug()
result = func(*args, **kwargs)
slow_queries = [
q for q in connection.queries if float(q.get('time', 0)) >= threshold
]
slow_queries.sort(key=lambda x: float(x.get('time', 0)), reverse=True)
log_entries = []
for query in slow_queries[:n]:
log_entry = f'Time: {query["time"]}s | SQL: {query["sql"]}'
log_entries.append(log_entry)
if log_entries:
log_message = '\n'.join(log_entries)
if log_to_file:
with open('slow_queries.log', 'w', encoding='utf-8') as f:
f.write(f'Slow queries detected:\n{log_message}\n')
else:
print(f'Slow queries detected:\n{log_message}')
return result
return wrapper
return decorator
# Raise an exception if this file is imported outside of DEBUG mode
ensure_debug()

View File

@@ -1,13 +1,36 @@
"""Functions to check if certain parts of InvenTree are ready."""
import functools
import inspect
import os
import sys
import warnings
# Keep track of loaded apps, to prevent multiple executions of ready functions
_loaded_apps = set()
def clearLoadedApps():
"""Clear the set of loaded apps."""
global _loaded_apps
_loaded_apps = set()
def setAppLoaded(app_name: str):
"""Mark an app as loaded."""
global _loaded_apps
_loaded_apps.add(app_name)
def isAppLoaded(app_name: str) -> bool:
"""Return True if the app has been marked as loaded."""
global _loaded_apps
return app_name in _loaded_apps
def isInTestMode():
"""Returns True if the database is in testing mode."""
return 'test' in sys.argv
return 'test' in sys.argv or sys.argv[0].endswith('pytest')
def isImportingData():
@@ -157,3 +180,22 @@ def isPluginRegistryLoaded():
from plugin import registry
return registry.plugins_loaded
def ignore_ready_warning(func):
"""Decorator to ignore 'AppRegistryNotReady' warnings in functions called during app ready phase.
Ref: https://github.com/inventree/InvenTree/issues/10806
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
message='Accessing the database during app initialization is discouraged',
category=RuntimeWarning,
)
return func(*args, **kwargs)
return wrapper

View File

@@ -1,7 +1,7 @@
"""Schema processing functions for cleaning up generated schema."""
from itertools import chain
from typing import Optional
from typing import Any, Optional
from django.conf import settings
@@ -138,6 +138,43 @@ class ExtendedAutoSchema(AutoSchema):
return operation
def postprocess_schema_enums(result, generator, **kwargs):
"""Override call to drf-spectacular's enum postprocessor to filter out specific warnings."""
from drf_spectacular import drainage
# Monkey-patch the warn function temporarily
original_warn = drainage.warn
def custom_warn(msg: str, delayed: Any = None) -> None:
"""Custom patch to ignore some drf-spectacular warnings.
- Some warnings are unavoidable due to the way that InvenTree implements generic relationships (via ContentType).
- The cleanest way to handle this appears to be to override the 'warn' function from drf-spectacular.
Ref: https://github.com/inventree/InvenTree/pull/10699
"""
ignore_patterns = [
'enum naming encountered a non-optimally resolvable collision for fields named "model_type"'
]
if any(pattern in msg for pattern in ignore_patterns):
return
original_warn(msg, delayed)
# Replace the warn function with our custom version
drainage.warn = custom_warn
import drf_spectacular.hooks
result = drf_spectacular.hooks.postprocess_schema_enums(result, generator, **kwargs)
# Restore the original warn function
drainage.warn = original_warn
return result
def postprocess_required_nullable(result, generator, request, public):
"""Un-require nullable fields.

View File

@@ -7,8 +7,10 @@ from decimal import Decimal
from typing import Any, Optional
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from djmoney.contrib.django_rest_framework.fields import MoneyField
@@ -21,13 +23,14 @@ from rest_framework.fields import empty
from rest_framework.mixins import ListModelMixin
from rest_framework.serializers import DecimalField
from rest_framework.utils import model_meta
from taggit.serializers import TaggitSerializer
from taggit.serializers import TaggitSerializer, TagListSerializerField
import common.models as common_models
import InvenTree.ready
from common.currency import currency_code_default, currency_code_mappings
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
from InvenTree.helpers import str2bool
from InvenTree.helpers_model import getModelsWithMixin
from .setting.storages import StorageBackends
@@ -42,11 +45,15 @@ class FilterableSerializerField:
is_filterable = None
is_filterable_vals = {}
# Options for automatic queryset prefetching
prefetch_fields: Optional[list[str]] = None
def __init__(self, *args, **kwargs):
"""Initialize the serializer."""
if self.is_filterable is None: # Materialize parameters for later usage
self.is_filterable = kwargs.pop('is_filterable', None)
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
self.is_filterable = kwargs.pop('is_filterable', None)
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
self.prefetch_fields = kwargs.pop('prefetch_fields', None)
super().__init__(*args, **kwargs)
@@ -55,6 +62,7 @@ def enable_filter(
default_include: bool = False,
filter_name: Optional[str] = None,
filter_by_query: bool = True,
prefetch_fields: Optional[list[str]] = None,
):
"""Decorator for marking a serializer field as filterable.
@@ -65,11 +73,12 @@ def enable_filter(
default_include (bool): If True, the field will be included by default unless explicitly excluded. If False, the field will be excluded by default unless explicitly included.
filter_name (str, optional): The name of the filter parameter to use in the URL. If None, the function name of the (decorated) function will be used.
filter_by_query (bool): If True, also look for filter parameters in the request query parameters.
prefetch_fields (list of str, optional): List of related fields to prefetch when this field is included. This can be used to optimize database queries.
Returns:
The decorated serializer field, marked as filterable.
"""
# Ensure this function can be actually filteres
# Ensure this function can be actually filtered
if not issubclass(func.__class__, FilterableSerializerField):
raise TypeError(
'INVE-I2: `enable_filter` can only be applied to serializer fields / serializers that contain the `FilterableSerializerField` mixin!'
@@ -82,6 +91,10 @@ def enable_filter(
'filter_name': filter_name if filter_name else func.field_name,
'filter_by_query': filter_by_query,
}
# Attach queryset prefetching information
func._kwargs['prefetch_fields'] = prefetch_fields
return func
@@ -111,6 +124,43 @@ class FilterableSerializerMixin:
super().__init__(*args, **kwargs)
self.do_filtering()
def prefetch_queryset(self, queryset: QuerySet) -> QuerySet:
"""Apply any prefetching to the queryset based on the optionally included fields.
Args:
queryset: The original queryset.
Returns:
The modified queryset with prefetching applied.
"""
# If we are inside an OPTIONS request, DO NOT PREFETCH
if request := getattr(self, 'request', None):
if method := getattr(request, 'method', None):
if str(method).lower() == 'options':
return queryset
if getattr(request, '_metadata_requested', False):
return queryset
# Gather up the set of simple 'prefetch' fields and functions
prefetch_fields = set()
filterable_fields = [
field
for field in self.fields.values()
if getattr(field, 'is_filterable', None)
]
for field in filterable_fields:
if prefetch_names := getattr(field, 'prefetch_fields', None):
for pf in prefetch_names:
prefetch_fields.add(pf)
if prefetch_fields and len(prefetch_fields) > 0:
queryset = queryset.prefetch_related(*list(prefetch_fields))
return queryset
def gather_filters(self, kwargs) -> None:
"""Gather filterable fields through introspection."""
# Fast exit if this has already been done or would not have any effect
@@ -132,11 +182,11 @@ class FilterableSerializerMixin:
query_params = dict(getattr(context.get('request', {}), 'query_params', {}))
# Remove filter args from kwargs to avoid issues with super().__init__
poped_kwargs = {} # store popped kwargs as a arg might be reused for multiple fields
popped_kwargs = {} # store popped kwargs as a arg might be reused for multiple fields
tgs_vals: dict[str, bool] = {}
for k, v in self.filter_targets.items():
pop_ref = v['filter_name'] or k
val = kwargs.pop(pop_ref, poped_kwargs.get(pop_ref))
val = kwargs.pop(pop_ref, popped_kwargs.get(pop_ref))
# Optionally also look in query parameters
if val is None and self.filter_on_query and v.get('filter_by_query', True):
@@ -145,13 +195,13 @@ class FilterableSerializerMixin:
val = val[0]
if val: # Save popped value for reuse
poped_kwargs[pop_ref] = val
popped_kwargs[pop_ref] = val
tgs_vals[k] = (
str2bool(val) if isinstance(val, (str, int, float)) else val
) # Support for various filtering style for backwards compatibility
self.filter_target_values = tgs_vals
# Ensure this mixin is not proadly applied as it is expensive on scale (total CI time increased by 21% when running all coverage tests)
# Ensure this mixin is not broadly applied as it is expensive on scale (total CI time increased by 21% when running all coverage tests)
if len(self.filter_targets) == 0 and not self.no_filters:
raise Exception(
'INVE-I2: No filter targets found in fields, remove `PathScopedMixin`'
@@ -166,6 +216,16 @@ class FilterableSerializerMixin:
):
return
# Skip filtering when exporting data - leave all fields intact
if getattr(self, '_exporting_data', False):
return
# Skip filtering for a write requests - all fields should be present for data creation
if request := self.context.get('request', None):
if method := getattr(request, 'method', None):
if str(method).lower() in ['post', 'put', 'patch']:
return
# Throw out fields which are not requested (either by default or explicitly)
for k, v in self.filter_target_values.items():
# See `enable_filter` where` is_filterable and is_filterable_vals are set
@@ -208,6 +268,13 @@ class FilterableIntegerField(FilterableSerializerField, serializers.IntegerField
"""Custom IntegerField which allows filtering."""
class FilterableTagListField(FilterableSerializerField, TagListSerializerField):
"""Custom TagListSerializerField which allows filtering."""
class Meta:
"""Empty Meta class."""
# endregion
@@ -646,6 +713,11 @@ class InvenTreeDecimalField(serializers.FloatField):
def to_internal_value(self, data):
"""Convert to python type."""
if data in [None, '']:
if self.allow_null:
return None
raise serializers.ValidationError(_('This field may not be null.'))
# Convert the value to a string, and then a decimal
try:
return Decimal(str(data))
@@ -716,3 +788,96 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
raise ValidationError(_('Failed to download image from remote URL'))
return url
class ContentTypeField(serializers.ChoiceField):
"""Serializer field which represents a ContentType as 'app_label.model_name'.
This field converts a ContentType instance to a string representation in the format 'app_label.model_name' during serialization, and vice versa during deserialization.
Additionally, a "mixin_class" can be supplied to the field, which will restrict the valid content types to only those models which inherit from the specified mixin.
"""
mixin_class = None
def __init__(self, *args, mixin_class=None, **kwargs):
"""Initialize the ContentTypeField.
Args:
mixin_class: Optional mixin class to restrict valid content types.
"""
from InvenTree.cache import get_cached_content_types
self.mixin_class = mixin_class
# Override the 'choices' field, to limit to the appropriate models
if self.mixin_class is not None:
models = getModelsWithMixin(self.mixin_class)
kwargs['choices'] = [
(
f'{model._meta.app_label}.{model._meta.model_name}',
model._meta.verbose_name,
)
for model in models
]
else:
content_types = get_cached_content_types()
kwargs['choices'] = [
(f'{ct.app_label}.{ct.model}', str(ct)) for ct in content_types
]
if kwargs.get('allow_null') or kwargs.get('allow_blank'):
kwargs['choices'] = [('', '---------'), *kwargs['choices']]
super().__init__(*args, **kwargs)
def to_representation(self, value):
"""Convert ContentType instance to string representation."""
return f'{value.app_label}.{value.model}'
def to_internal_value(self, data):
"""Convert string representation back to ContentType instance."""
content_type = None
if data in ['', None]:
return None
# First, try to resolve the content type via direct pk value
try:
content_type_id = int(data)
content_type = ContentType.objects.get_for_id(content_type_id)
except (ValueError, ContentType.DoesNotExist):
content_type = None
try:
if len(data.split('.')) == 2:
app_label, model = data.split('.')
content_types = ContentType.objects.filter(
app_label=app_label, model=model
)
if content_types.count() == 1:
# Try exact match first
content_type = content_types.first()
else:
# Try lookup just on model name
content_types = ContentType.objects.filter(model=data)
if content_types.exists() and content_types.count() == 1:
content_type = content_types.first()
except Exception:
raise ValidationError(_('Invalid content type format'))
if content_type is None:
raise ValidationError(_('Content type not found'))
if self.mixin_class is not None:
model_class = content_type.model_class()
if not issubclass(model_class, self.mixin_class):
raise ValidationError(
_('Content type does not match required mixin class')
)
return content_type

View File

@@ -20,7 +20,7 @@ def get_spectacular_settings():
'SERVE_INCLUDE_SCHEMA': False,
'SCHEMA_PATH_PREFIX': '/api/',
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums',
'InvenTree.schema.postprocess_schema_enums',
'InvenTree.schema.postprocess_required_nullable',
'InvenTree.schema.postprocess_print_stats',
],
@@ -28,6 +28,7 @@ def get_spectacular_settings():
'UserTypeEnum': 'users.models.UserProfile.UserType',
'TemplateModelTypeEnum': 'report.models.ReportTemplateBase.ModelChoices',
'AttachmentModelTypeEnum': 'common.models.Attachment.ModelChoices',
'ParameterModelTypeEnum': 'common.models.Parameter.ModelChoices',
'DataImportSessionModelTypeEnum': 'importer.models.DataImportSession.ModelChoices',
# Allauth
'UnauthorizedStatus': [[401, 401]],

View File

@@ -53,7 +53,7 @@ INVENTREE_BASE_URL = 'https://inventree.org'
INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom'
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
TESTING = 'test' in sys.argv or 'TESTING' in os.environ or 'pytest' in sys.argv
if TESTING:
# Use a weaker password hasher for testing (improves testing speed)
@@ -367,6 +367,21 @@ MIDDLEWARE = CONFIG.get(
],
)
# In DEBUG mode, add support for django-silk
# Ref: https://silk.readthedocs.io/en/latest/
DJANGO_SILK_ENABLED = DEBUG and get_boolean_setting( # pragma: no cover
'INVENTREE_DEBUG_SILK', 'debug_silk', False
)
if DJANGO_SILK_ENABLED: # pragma: no cover
MIDDLEWARE.append('silk.middleware.SilkyMiddleware')
INSTALLED_APPS.append('silk')
# Optionally enable the silk python profiler
SILKY_PYTHON_PROFILER = SILKY_PYTHON_PROFILER_BINARY = get_boolean_setting(
'INVENTREE_DEBUG_SILK_PROFILING', 'debug_silk_profiling', False
)
# In DEBUG mode, add support for django-querycount
# Ref: https://github.com/bradmontgomery/django-querycount
if DEBUG and get_boolean_setting( # pragma: no cover
@@ -577,7 +592,7 @@ REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.ModelPermission',
'InvenTree.permissions.RolePermission',
'InvenTree.permissions.InvenTreeTokenMatchesOASRequirements',
],
@@ -1017,11 +1032,13 @@ EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
# region email
# Email configuration options
EMAIL_BACKEND = 'InvenTree.backends.InvenTreeMailLoggingBackend'
INTERNAL_EMAIL_BACKEND = get_setting(
'INVENTREE_EMAIL_BACKEND',
'email.backend',
'django.core.mail.backends.smtp.EmailBackend',
)
# SMTP backend
EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host', '')
EMAIL_PORT = get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25, typecast=int)

View File

@@ -16,7 +16,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission, User
from django.db import connections, models
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.test import TestCase, tag
from django.test.utils import CaptureQueriesContext, override_settings
from django.urls import reverse
@@ -824,3 +824,11 @@ class AdminTestCase(InvenTreeAPITestCase):
def in_env_context(envs):
"""Patch the env to include the given dict."""
return mock.patch.dict(os.environ, envs)
@tag('performance_test')
class InvenTreeAPIPerformanceTestCase(InvenTreeAPITestCase):
"""Base class for InvenTree API performance tests."""
MAX_QUERY_COUNT = 50
MAX_QUERY_TIME = 60

View File

@@ -186,6 +186,9 @@ urlpatterns.append(
if settings.FRONTEND_SETTINGS.get('url_compatibility'):
urlpatterns += cui_compatibility_urls(settings.FRONTEND_URL_BASE)
if settings.DJANGO_SILK_ENABLED:
urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]
# Send any unknown URLs to the index page
urlpatterns += [
re_path(

View File

@@ -12,14 +12,13 @@ import sys
from datetime import datetime as dt
from datetime import timedelta as td
import django
from django.conf import settings
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '1.2.0 dev'
# Minimum supported Python version
MIN_PYTHON_VERSION = (3, 11)
logger = logging.getLogger('inventree')
@@ -59,33 +58,36 @@ except Exception as exc:
def checkMinPythonVersion():
"""Check that the Python version is at least 3.11."""
"""Check that the Python version meets the minimum requirements."""
V_MIN_MAJOR, V_MIN_MINOR = MIN_PYTHON_VERSION
version = sys.version.split(' ')[0]
docs = 'https://docs.inventree.org/en/stable/start/intro/#python-requirements'
docs = 'https://docs.inventree.org/en/stable/start/#python-requirements'
msg = f"""
InvenTree requires Python 3.11 or above - you are running version {version}.
INVE-E15: Python version not supported.
InvenTree requires Python {V_MIN_MAJOR}.{V_MIN_MINOR} or above - you are running version {version}.
- Refer to the InvenTree documentation for more information:
- {docs}
"""
if sys.version_info.major < 3: # noqa: UP036
if sys.version_info.major < V_MIN_MAJOR:
raise RuntimeError(msg)
if sys.version_info.major == 3 and sys.version_info.minor < 11:
if sys.version_info.major == V_MIN_MAJOR and sys.version_info.minor < V_MIN_MINOR:
raise RuntimeError(msg)
print(f'Python version {version} - {sys.executable}')
logger.info(f'Python version {version} - {sys.executable}')
def inventreeInstanceName():
def inventreeInstanceName() -> str:
"""Returns the InstanceName settings for the current database."""
from common.settings import get_global_setting
return get_global_setting('INVENTREE_INSTANCE')
def inventreeInstanceTitle():
def inventreeInstanceTitle() -> str:
"""Returns the InstanceTitle for the current database."""
from common.settings import get_global_setting
@@ -95,7 +97,7 @@ def inventreeInstanceTitle():
return 'InvenTree'
def inventreeVersion():
def inventreeVersion() -> str:
"""Returns the InvenTree version string."""
return INVENTREE_SW_VERSION.lower().strip()
@@ -110,12 +112,12 @@ def inventreeVersionTuple(version=None):
return [int(g) for g in match.groups()] if match else []
def isInvenTreeDevelopmentVersion():
def isInvenTreeDevelopmentVersion() -> bool:
"""Return True if current InvenTree version is a "development" version."""
return inventreeVersion().endswith('dev')
def inventreeDocsVersion():
def inventreeDocsVersion() -> str:
"""Return the version string matching the latest documentation.
Development -> "latest"
@@ -123,26 +125,27 @@ def inventreeDocsVersion():
"""
if isInvenTreeDevelopmentVersion():
return 'latest'
return INVENTREE_SW_VERSION # pragma: no cover
return INVENTREE_SW_VERSION
def inventreeDocUrl():
def inventreeDocUrl() -> str:
"""Return URL for InvenTree documentation site."""
tag = inventreeDocsVersion()
return f'https://docs.inventree.org/en/{tag}'
def inventreeAppUrl():
def inventreeAppUrl() -> str:
"""Return URL for InvenTree app site."""
return 'https://docs.inventree.org/app/'
return 'https://docs.inventree.org/en/stable/app/'
def inventreeGithubUrl():
def inventreeGithubUrl() -> str:
"""Return URL for InvenTree github site."""
return 'https://github.com/InvenTree/InvenTree/'
def isInvenTreeUpToDate():
def isInvenTreeUpToDate() -> bool:
"""Test if the InvenTree instance is "up to date" with the latest version.
A background task periodically queries GitHub for latest version, and stores it to the database as "_INVENTREE_LATEST_VERSION"
@@ -164,7 +167,7 @@ def isInvenTreeUpToDate():
return inventree_version >= latest_version # pragma: no cover
def inventreeApiVersion():
def inventreeApiVersion() -> int:
"""Returns current API version of InvenTree."""
return INVENTREE_API_VERSION
@@ -224,6 +227,8 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
def inventreeDjangoVersion():
"""Returns the version of Django library."""
import django
return django.get_version()
@@ -290,6 +295,8 @@ def inventreePlatform():
def inventreeDatabase():
"""Return the InvenTree database backend e.g. 'postgresql'."""
from django.conf import settings
return settings.DB_ENGINE

View File

@@ -24,7 +24,7 @@ from build.models import Build, BuildItem, BuildLine
from build.status_codes import BuildStatus, BuildStatusGroups
from data_exporter.mixins import DataExportViewMixin
from generic.states.api import StatusView
from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.api import BulkDeleteMixin, MetadataView, ParameterListMixin
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
SEARCH_ORDER_FILTER_ALIAS,
@@ -317,15 +317,7 @@ class BuildMixin:
"""Return the queryset for the Build API endpoints."""
queryset = super().get_queryset()
queryset = queryset.prefetch_related(
'responsible',
'issued_by',
'build_lines',
'build_lines__bom_item',
'build_lines__build',
'part',
'part__pricing_data',
)
queryset = build.serializers.BuildSerializer.annotate_queryset(queryset)
return queryset
@@ -336,7 +328,13 @@ class BuildListOutputOptions(OutputConfiguration):
OPTIONS = [InvenTreeOutputOption('part_detail', default=True)]
class BuildList(DataExportViewMixin, BuildMixin, OutputOptionsMixin, ListCreateAPI):
class BuildList(
DataExportViewMixin,
BuildMixin,
OutputOptionsMixin,
ParameterListMixin,
ListCreateAPI,
):
"""API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters)
@@ -378,14 +376,6 @@ class BuildList(DataExportViewMixin, BuildMixin, OutputOptionsMixin, ListCreateA
'priority',
]
def get_queryset(self):
"""Override the queryset filtering, as some of the fields don't natively play nicely with DRF."""
queryset = super().get_queryset().select_related('part')
queryset = build.serializers.BuildSerializer.annotate_queryset(queryset)
return queryset
def get_serializer(self, *args, **kwargs):
"""Add extra context information to the endpoint serializer."""
kwargs['create'] = True
@@ -569,26 +559,27 @@ class BuildLineOutputOptions(OutputConfiguration):
InvenTreeOutputOption(
'bom_item_detail',
description='Include detailed information about the BOM item linked to this build line.',
default=True,
default=False,
),
InvenTreeOutputOption(
'assembly_detail',
description='Include brief details of the assembly (parent part) related to the BOM item in this build line.',
default=True,
default=False,
),
InvenTreeOutputOption(
'part_detail',
description='Include detailed information about the specific part being built or consumed in this build line.',
default=True,
default=False,
),
InvenTreeOutputOption(
'build_detail',
description='Include detailed information about the associated build order.',
default=False,
),
InvenTreeOutputOption(
'allocations',
description='Include allocation details showing which stock items are allocated to this build line.',
default=True,
default=False,
),
]
@@ -906,6 +897,7 @@ class BuildItemOutputOptions(OutputConfiguration):
InvenTreeOutputOption('location_detail'),
InvenTreeOutputOption('stock_detail'),
InvenTreeOutputOption('build_detail'),
InvenTreeOutputOption('supplier_part_detail'),
]
@@ -928,26 +920,9 @@ class BuildItemList(
"""Override the queryset method, to perform custom prefetch."""
queryset = super().get_queryset()
queryset = queryset.select_related(
'build_line',
'build_line__build',
'build_line__build__part',
'build_line__build__responsible',
'build_line__build__issued_by',
'build_line__build__project_code',
'build_line__build__part__pricing_data',
'build_line__bom_item',
'build_line__bom_item__part',
'build_line__bom_item__sub_part',
'install_into',
'stock_item',
'stock_item__location',
'stock_item__part',
'stock_item__supplier_part__part',
'stock_item__supplier_part__supplier',
'stock_item__supplier_part__manufacturer_part',
'stock_item__supplier_part__manufacturer_part__manufacturer',
).prefetch_related('stock_item__location__tags', 'stock_item__tags')
queryset = queryset.select_related('install_into').prefetch_related(
'build_line', 'build_line__build', 'build_line__bom_item'
)
return queryset

View File

@@ -76,6 +76,7 @@ class BuildReportContext(report.mixins.BaseReportContext):
class Build(
InvenTree.models.PluginValidationMixin,
report.mixins.InvenTreeReportMixin,
InvenTree.models.InvenTreeParameterMixin,
InvenTree.models.InvenTreeAttachmentMixin,
InvenTree.models.InvenTreeBarcodeMixin,
InvenTree.models.InvenTreeNotesMixin,
@@ -85,7 +86,7 @@ class Build(
InvenTree.models.MetadataMixin,
InvenTree.models.InvenTreeTree,
):
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
"""A Build object organizes the creation of new StockItem objects from other existing StockItem objects.
Attributes:
part: The part to be built (from component BOM items)

View File

@@ -22,17 +22,16 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError
import build.tasks
import common.filters
import common.settings
import company.serializers
import InvenTree.helpers
import part.filters
import part.serializers as part_serializers
from common.serializers import ProjectCodeSerializer
from common.settings import get_global_setting
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import (
FilterableCharField,
FilterableSerializerMixin,
InvenTreeDecimalField,
InvenTreeModelSerializer,
@@ -101,6 +100,7 @@ class BuildSerializer(
'issued_by_detail',
'responsible',
'responsible_detail',
'parameters',
'priority',
'level',
]
@@ -122,8 +122,11 @@ class BuildSerializer(
part_detail = enable_filter(
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
True,
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
)
parameters = common.filters.enable_parameters_filter()
part_name = serializers.CharField(
source='part.name', read_only=True, label=_('Part Name')
)
@@ -136,34 +139,21 @@ class BuildSerializer(
UserSerializer(source='issued_by', read_only=True),
True,
filter_name='user_detail',
prefetch_fields=['issued_by'],
)
responsible_detail = enable_filter(
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
True,
filter_name='user_detail',
prefetch_fields=['responsible'],
)
barcode_hash = serializers.CharField(read_only=True)
project_code_label = enable_filter(
FilterableCharField(
source='project_code.code',
read_only=True,
label=_('Project Code Label'),
allow_null=True,
),
True,
filter_name='project_code_detail',
)
project_code_label = common.filters.enable_project_label_filter()
project_code_detail = enable_filter(
ProjectCodeSerializer(
source='project_code', many=False, read_only=True, allow_null=True
),
True,
filter_name='project_code_detail',
)
project_code_detail = common.filters.enable_project_code_filter()
@staticmethod
def annotate_queryset(queryset):
@@ -1230,6 +1220,7 @@ class BuildItemSerializer(
pricing=False,
),
True,
prefetch_fields=['stock_item__part'],
)
stock_item_detail = enable_filter(
@@ -1245,6 +1236,12 @@ class BuildItemSerializer(
),
True,
filter_name='stock_detail',
prefetch_fields=[
'stock_item',
'stock_item__part',
'stock_item__supplier_part',
'stock_item__supplier_part__manufacturer_part',
],
)
location = serializers.PrimaryKeyRelatedField(
@@ -1259,6 +1256,7 @@ class BuildItemSerializer(
allow_null=True,
),
True,
prefetch_fields=['stock_item__location'],
)
build_detail = enable_filter(
@@ -1270,15 +1268,32 @@ class BuildItemSerializer(
allow_null=True,
),
True,
prefetch_fields=[
'build_line__build',
'build_line__build__part',
'build_line__build__responsible',
'build_line__build__issued_by',
'build_line__build__project_code',
'build_line__build__part__pricing_data',
],
)
supplier_part_detail = company.serializers.SupplierPartSerializer(
label=_('Supplier Part'),
source='stock_item.supplier_part',
many=False,
read_only=True,
allow_null=True,
brief=True,
supplier_part_detail = enable_filter(
company.serializers.SupplierPartSerializer(
label=_('Supplier Part'),
source='stock_item.supplier_part',
many=False,
read_only=True,
allow_null=True,
brief=True,
),
False,
prefetch_fields=[
'stock_item__supplier_part',
'stock_item__supplier_part__supplier',
'stock_item__supplier_part__manufacturer_part',
'stock_item__supplier_part__manufacturer_part__manufacturer',
],
)
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
@@ -1363,7 +1378,17 @@ class BuildLineSerializer(
)
allocations = enable_filter(
BuildItemSerializer(many=True, read_only=True, build_detail=False), True
BuildItemSerializer(many=True, read_only=True, build_detail=False),
True,
prefetch_fields=[
'allocations',
'allocations__stock_item',
'allocations__stock_item__part',
'allocations__stock_item__part__pricing_data',
'allocations__stock_item__supplier_part',
'allocations__stock_item__supplier_part__manufacturer_part',
'allocations__stock_item__location',
],
)
# BOM item info fields
@@ -1407,7 +1432,8 @@ class BuildLineSerializer(
part_detail=False,
can_build=False,
),
True,
False,
prefetch_fields=['bom_item'],
)
assembly_detail = enable_filter(
@@ -1419,7 +1445,8 @@ class BuildLineSerializer(
allow_null=True,
pricing=False,
),
True,
False,
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
)
part_detail = enable_filter(
@@ -1430,7 +1457,8 @@ class BuildLineSerializer(
read_only=True,
pricing=False,
),
True,
False,
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
)
category_detail = enable_filter(
@@ -1442,6 +1470,7 @@ class BuildLineSerializer(
allow_null=True,
),
False,
prefetch_fields=['bom_item__sub_part__category'],
)
build_detail = enable_filter(
@@ -1507,77 +1536,16 @@ class BuildLineSerializer(
'bom_item__sub_part__pricing_data',
)
# Pre-fetch related fields
queryset = queryset.prefetch_related(
'allocations',
'allocations__stock_item',
'allocations__stock_item__part',
'allocations__stock_item__supplier_part',
'allocations__stock_item__supplier_part__manufacturer_part',
'allocations__stock_item__location',
'allocations__stock_item__tags',
'bom_item',
'bom_item__part',
'bom_item__sub_part',
'bom_item__sub_part__category',
'bom_item__sub_part__stock_items',
'bom_item__sub_part__stock_items__allocations',
'bom_item__sub_part__stock_items__sales_order_allocations',
'bom_item__substitutes',
'bom_item__substitutes__part__stock_items',
'bom_item__substitutes__part__stock_items__allocations',
'bom_item__substitutes__part__stock_items__sales_order_allocations',
)
# Defer expensive fields which we do not need for this serializer
queryset = (
queryset.defer(
'build__lft',
'build__rght',
'build__level',
'build__tree_id',
'build__destination',
'build__take_from',
'build__completed_by',
'build__sales_order',
'build__parent',
'build__notes',
'build__metadata',
'build__responsible',
'build__barcode_data',
'build__barcode_hash',
'build__project_code',
)
.defer('bom_item__metadata')
.defer(
'bom_item__part__lft',
'bom_item__part__rght',
'bom_item__part__level',
'bom_item__part__tree_id',
'bom_item__part__tags',
'bom_item__part__notes',
'bom_item__part__variant_of',
'bom_item__part__revision_of',
'bom_item__part__creation_user',
'bom_item__part__bom_checked_by',
'bom_item__part__default_supplier',
'bom_item__part__responsible_owner',
)
.defer(
'bom_item__sub_part__lft',
'bom_item__sub_part__rght',
'bom_item__sub_part__level',
'bom_item__sub_part__tree_id',
'bom_item__sub_part__tags',
'bom_item__sub_part__notes',
'bom_item__sub_part__variant_of',
'bom_item__sub_part__revision_of',
'bom_item__sub_part__creation_user',
'bom_item__sub_part__bom_checked_by',
'bom_item__sub_part__default_supplier',
'bom_item__sub_part__responsible_owner',
)
queryset = queryset.defer(
'build__notes',
'build__metadata',
'bom_item__metadata',
'bom_item__part__notes',
'bom_item__part__metadata',
'bom_item__sub_part__notes',
'bom_item__sub_part__metadata',
)
# Annotate the "allocated" quantity

View File

@@ -86,7 +86,7 @@ class TestReferencePatternMigration(MigratorTestCase):
"""
migrate_from = ('build', '0019_auto_20201019_1302')
migrate_to = ('build', unit_test.getNewestMigrationFile('build'))
migrate_to = ('build', '0037_build_priority')
def prepare(self):
"""Create some initial data prior to migration."""

View File

@@ -6,6 +6,32 @@ import common.models
import common.validators
@admin.register(common.models.ParameterTemplate)
class ParameterTemplateAdmin(admin.ModelAdmin):
"""Admin interface for ParameterTemplate objects."""
list_display = ('name', 'description', 'model_type', 'units')
search_fields = ('name', 'description')
@admin.register(common.models.Parameter)
class ParameterAdmin(admin.ModelAdmin):
"""Admin interface for Parameter objects."""
list_display = (
'template',
'model_type',
'model_id',
'data',
'updated',
'updated_by',
)
autocomplete_fields = ('template', 'updated_by')
list_filter = ('template', 'model_type', 'updated_by')
search_fields = ('template__name', 'data', 'note')
@admin.register(common.models.Attachment)
class AttachmentAdmin(admin.ModelAdmin):
"""Admin interface for Attachment objects."""

View File

@@ -27,7 +27,9 @@ from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from sql_util.utils import SubqueryCount
import common.filters
import common.models
import common.serializers
import InvenTree.conversion
@@ -35,15 +37,20 @@ from common.icons import get_icon_packs
from common.settings import get_global_setting
from data_exporter.mixins import DataExportViewMixin
from generic.states.api import urlpattern as generic_states_api_urls
from InvenTree.api import BulkDeleteMixin, MetadataView
from InvenTree.api import BulkCreateMixin, BulkDeleteMixin, MetadataView
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
from InvenTree.helpers import inheritors
from InvenTree.filters import (
ORDER_FILTER,
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
)
from InvenTree.helpers import inheritors, str2bool
from InvenTree.helpers_email import send_email
from InvenTree.mixins import (
CreateAPI,
ListAPI,
ListCreateAPI,
OutputOptionsMixin,
RetrieveAPI,
RetrieveDestroyAPI,
RetrieveUpdateAPI,
@@ -708,13 +715,17 @@ class AttachmentFilter(FilterSet):
return queryset.filter(Q(attachment=None) | Q(attachment='')).distinct()
class AttachmentList(BulkDeleteMixin, ListCreateAPI):
"""List API endpoint for Attachment objects."""
class AttachmentMixin:
"""Mixin class for Attachment views."""
queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer
permission_classes = [IsAuthenticatedOrReadScope]
class AttachmentList(AttachmentMixin, BulkDeleteMixin, ListCreateAPI):
"""List API endpoint for Attachment objects."""
filter_backends = SEARCH_ORDER_FILTER
filterset_class = AttachmentFilter
@@ -746,13 +757,9 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
)
class AttachmentDetail(RetrieveUpdateDestroyAPI):
class AttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
"""Detail API endpoint for Attachment objects."""
queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer
permission_classes = [IsAuthenticatedOrReadScope]
def destroy(self, request, *args, **kwargs):
"""Check user permissions before deleting an attachment."""
attachment = self.get_object()
@@ -765,6 +772,167 @@ class AttachmentDetail(RetrieveUpdateDestroyAPI):
return super().destroy(request, *args, **kwargs)
class ParameterTemplateFilter(FilterSet):
"""FilterSet class for the ParameterTemplateList API endpoint."""
class Meta:
"""Metaclass options."""
model = common.models.ParameterTemplate
fields = ['name', 'units', 'checkbox', 'enabled']
has_choices = rest_filters.BooleanFilter(
method='filter_has_choices', label='Has Choice'
)
def filter_has_choices(self, queryset, name, value):
"""Filter queryset to include only PartParameterTemplates with choices."""
if str2bool(value):
return queryset.exclude(Q(choices=None) | Q(choices=''))
return queryset.filter(Q(choices=None) | Q(choices='')).distinct()
has_units = rest_filters.BooleanFilter(method='filter_has_units', label='Has Units')
def filter_has_units(self, queryset, name, value):
"""Filter queryset to include only PartParameterTemplates with units."""
if str2bool(value):
return queryset.exclude(Q(units=None) | Q(units=''))
return queryset.filter(Q(units=None) | Q(units='')).distinct()
model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')
def filter_model_type(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates of the given model type."""
return common.filters.filter_content_type(
queryset, 'model_type', value, allow_null=False
)
for_model = rest_filters.CharFilter(method='filter_for_model', label='For Model')
def filter_for_model(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates which apply to the given model.
Note that this varies from the 'model_type' filter, in that ParameterTemplates
with a blank 'model_type' are considered to apply to all models.
"""
return common.filters.filter_content_type(
queryset, 'model_type', value, allow_null=True
)
exists_for_model = rest_filters.CharFilter(
method='filter_exists_for_model', label='Exists For Model'
)
def filter_exists_for_model(self, queryset, name, value):
"""Filter queryset to include only ParameterTemplates which have at least one Parameter for the given model type."""
content_type = common.filters.determine_content_type(value)
if not content_type:
return queryset.none()
queryset = queryset.prefetch_related('parameters')
# Annotate the queryset to determine which ParameterTemplates have at least one Parameter for the given model type
queryset = queryset.annotate(
parameter_count=SubqueryCount(
'parameters', filter=Q(model_type=content_type)
)
)
# Return only those ParameterTemplates which have at least one Parameter for the given model type
return queryset.filter(parameter_count__gt=0)
class ParameterTemplateMixin:
"""Mixin class for ParameterTemplate views."""
queryset = common.models.ParameterTemplate.objects.all().prefetch_related(
'model_type'
)
serializer_class = common.serializers.ParameterTemplateSerializer
permission_classes = [IsAuthenticatedOrReadScope]
class ParameterTemplateList(ParameterTemplateMixin, DataExportViewMixin, ListCreateAPI):
"""List view for ParameterTemplate objects."""
filterset_class = ParameterTemplateFilter
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['name', 'description']
ordering_fields = ['name', 'units', 'checkbox']
class ParameterTemplateDetail(ParameterTemplateMixin, RetrieveUpdateDestroyAPI):
"""Detail view for a ParameterTemplate object."""
class ParameterFilter(FilterSet):
"""Custom filters for the ParameterList API endpoint."""
class Meta:
"""Metaclass options for the filterset."""
model = common.models.Parameter
fields = ['model_id', 'template', 'updated_by']
enabled = rest_filters.BooleanFilter(
label='Template Enabled', field_name='template__enabled'
)
model_type = rest_filters.CharFilter(method='filter_model_type', label='Model Type')
def filter_model_type(self, queryset, name, value):
"""Filter queryset to include only Parameters of the given model type."""
return common.filters.filter_content_type(
queryset, 'model_type', value, allow_null=False
)
class ParameterMixin:
"""Mixin class for Parameter views."""
queryset = common.models.Parameter.objects.all().prefetch_related('model_type')
serializer_class = common.serializers.ParameterSerializer
permission_classes = [IsAuthenticatedOrReadScope]
class ParameterList(
OutputOptionsMixin,
ParameterMixin,
BulkCreateMixin,
BulkDeleteMixin,
DataExportViewMixin,
ListCreateAPI,
):
"""List API endpoint for Parameter objects."""
filterset_class = ParameterFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = ['name', 'data', 'units', 'template', 'updated', 'updated_by']
ordering_field_aliases = {
'name': 'template__name',
'units': 'template__units',
'data': ['data_numeric', 'data'],
}
search_fields = [
'data',
'template__name',
'template__description',
'template__units',
]
unique_create_fields = ['model_type', 'model_id', 'template']
class ParameterDetail(ParameterMixin, RetrieveUpdateDestroyAPI):
"""Detail API endpoint for Parameter objects."""
@method_decorator(cache_control(public=True, max_age=86400), name='dispatch')
class IconList(ListAPI):
"""List view for available icon packages."""
@@ -997,6 +1165,51 @@ common_api_urls = [
path('', AttachmentList.as_view(), name='api-attachment-list'),
]),
),
# Parameters and templates
path(
'parameter/',
include([
path(
'template/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(
model=common.models.ParameterTemplate
),
name='api-parameter-template-metadata',
),
path(
'',
ParameterTemplateDetail.as_view(),
name='api-parameter-template-detail',
),
]),
),
path(
'',
ParameterTemplateList.as_view(),
name='api-parameter-template-list',
),
]),
),
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(model=common.models.Parameter),
name='api-parameter-metadata',
),
path('', ParameterDetail.as_view(), name='api-parameter-detail'),
]),
),
path('', ParameterList.as_view(), name='api-parameter-list'),
]),
),
path(
'error-report/',
include([

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