Merge branch 'master' into fix-supplier-import-stock-redirect

This commit is contained in:
Oliver 2026-06-29 18:06:26 +10:00 committed by GitHub
commit 59688b57ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
188 changed files with 44544 additions and 42832 deletions

View File

@ -1,9 +1,7 @@
# Dockerfile for the InvenTree devcontainer
# This container is used for development of the InvenTree project, and includes all necessary dependencies for both backend and frontend development.
# In contrast with the "production" image (which is based on an Alpine image)
# we use a Debian-based image for the devcontainer
FROM mcr.microsoft.com/devcontainers/python:3.11-bookworm@sha256:e754c29c4e3ffcf6c794c1020e36a0812341d88ec9569a34704b975fa89e8848
FROM mcr.microsoft.com/devcontainers/python:3.12-trixie@sha256:5440cb68898d190ad6c6e8a4634ce89d0645bea47f9c8beb75612bb8e3983711
# InvenTree paths
ENV INVENTREE_HOME="/home/inventree"
@ -25,10 +23,10 @@ RUN chmod +x init.sh
# Install required base packages
RUN apt update && apt install -y \
python3.11-dev python3.11-venv \
python3-dev python3-venv \
postgresql-client \
libldap2-dev libsasl2-dev \
libpango1.0-0 libcairo2 \
libpango-1.0-0 libcairo2 \
poppler-utils weasyprint
# Install packages required for frontend development

View File

@ -1,79 +0,0 @@
trigger:
batch: true
branches:
include:
- master
- stable
- refs/tags/*
paths:
include:
- src/backend
pool:
vmImage: ubuntu-latest
strategy:
matrix:
Python39:
PYTHON_VERSION: '3.11'
maxParallel: 3
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(PYTHON_VERSION)'
architecture: 'x64'
- task: PythonScript@0
displayName: 'Export project path'
inputs:
scriptSource: 'inline'
script: |
"""Search all subdirectories for `manage.py`."""
from glob import iglob
from os import path
# Python >= 3.5
manage_py = next(iglob(path.join('**', 'manage.py'), recursive=True), None)
if not manage_py:
raise SystemExit('Could not find a Django project')
project_location = path.dirname(path.abspath(manage_py))
print('Found Django project in', project_location)
print('##vso[task.setvariable variable=projectRoot]{}'.format(project_location))
- script: |
python -m pip install --upgrade pip setuptools wheel uv
uv pip install --require-hashes -r src/backend/requirements.txt
uv pip install --require-hashes -r src/backend/requirements-dev.txt
sudo apt-get install poppler-utils
sudo apt-get install libpoppler-dev
uv pip install unittest-xml-reporting coverage invoke
displayName: 'Install prerequisites'
env:
UV_SYSTEM_PYTHON: 1
- script: |
pushd '$(projectRoot)'
invoke update --uv
coverage run manage.py test --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner --no-input
coverage xml -i
displayName: 'Run tests'
env:
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_DB_NAME: inventree
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
INVENTREE_BACKUP_DIR: ./backup
INVENTREE_SITE_URL: http://localhost:8000
INVENTREE_PLUGINS_ENABLED: true
UV_SYSTEM_PYTHON: 1
INVENTREE_DEBUG: true
INVENTREE_LOG_LEVEL: INFO
- task: PublishTestResults@2
inputs:
testResultsFiles: "**/TEST-*.xml"
testRunTitle: 'Python $(PYTHON_VERSION)'
condition: succeededOrFailed()
- task: PublishCodeCoverageResults@2
inputs:
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'

View File

@ -9,7 +9,7 @@ on:
- l10
env:
python_version: 3.11
python_version: 3.12
permissions:
contents: read

View File

@ -62,7 +62,7 @@ jobs:
contents: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
python_version: "3.11"
python_version: 3.12
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps:
@ -84,6 +84,16 @@ jobs:
docker run --rm inventree-test test -f /home/inventree/gunicorn.conf.py
docker run --rm inventree-test test -f /home/inventree/src/backend/requirements.txt
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/manage.py
# Check that backend translations were compiled into the image (a few representative languages)
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/locale/de/LC_MESSAGES/django.mo
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/locale/fr/LC_MESSAGES/django.mo
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/locale/ru/LC_MESSAGES/django.mo
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/locale/zh_Hans/LC_MESSAGES/django.mo
# Check that frontend translations (lingui) were compiled into the static build output (a few representative languages)
docker run --rm inventree-test grep -q '"src/locales/de/messages.ts"' /home/inventree/src/backend/InvenTree/web/static/web/.vite/manifest.json
docker run --rm inventree-test grep -q '"src/locales/fr/messages.ts"' /home/inventree/src/backend/InvenTree/web/static/web/.vite/manifest.json
docker run --rm inventree-test grep -q '"src/locales/ru/messages.ts"' /home/inventree/src/backend/InvenTree/web/static/web/.vite/manifest.json
docker run --rm inventree-test grep -q '"src/locales/zh_Hans/messages.ts"' /home/inventree/src/backend/InvenTree/web/static/web/.vite/manifest.json
- name: Build Docker Image
# Build the development docker image (using docker-compose.yml)
run: docker compose --project-directory . -f contrib/container/dev-docker-compose.yml build --no-cache
@ -124,7 +134,7 @@ jobs:
contents: read
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
python_version: "3.11"
python_version: 3.12
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps:
@ -148,7 +158,7 @@ jobs:
id-token: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
python_version: "3.11"
python_version: 3.12
runs-on: ubuntu-latest # in the future we can try to use alternative runners here
steps:

View File

@ -13,8 +13,9 @@ on:
branches-ignore: ["l10*"]
env:
python_version: 3.11
python_version: 3.12
node_version: 24
plugin_creator_version: 1.20.0
# The OS version must be set per job
server_start_sleep: 60
@ -153,7 +154,7 @@ jobs:
run: cd src/frontend && npx playwright install-deps
- name: Install Sample Plugin
run: |
pip install -U inventree-plugin-creator
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
create-inventree-plugin --default
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
- name: Run Playwright tests
@ -195,6 +196,9 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
permissions:
contents: read # Required for actions/checkout
id-token: write # Required for GitHub OIDC
env:
COVERAGE: true
@ -232,7 +236,7 @@ jobs:
run: cd src/frontend && npx playwright install-deps
- name: Install Sample Plugin
run: |
pip install -U inventree-plugin-creator
pip install -U inventree-plugin-creator==${{ env.plugin_creator_version }}
create-inventree-plugin --default
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
- name: Playwright [${{ matrix.shard }} / 4]

View File

@ -15,7 +15,7 @@ permissions:
contents: read
env:
python_version: 3.11
python_version: 3.12
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -9,7 +9,7 @@ on:
branches-ignore: ["l10*"]
env:
python_version: 3.11
python_version: 3.12
node_version: 24
# The OS version must be set per job
server_start_sleep: 60
@ -222,7 +222,7 @@ jobs:
echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action
id: breaking_changes
uses: oasdiff/oasdiff-action/diff@3530478ec30f84adedbfeb28f0d9527a290f50a9 # pin@main
uses: oasdiff/oasdiff-action/diff@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
with:
base: "api.yaml"
revision: "src/backend/InvenTree/schema.yml"
@ -363,7 +363,7 @@ jobs:
pip install .
if: needs.paths-filter.outputs.submit-performance == 'true'
- name: Performance Reporting
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
# check if we are in inventree/inventree - reporting only works in that OIDC context
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
with:
@ -379,7 +379,7 @@ jobs:
continue-on-error: true # continue if a step fails so that coverage gets pushed
strategy:
matrix:
python_version: [3.11, 3.14]
python_version: [3.12, 3.14]
env:
INVENTREE_DB_NAME: ./inventree.sqlite
@ -454,7 +454,7 @@ jobs:
env:
node_version: '>=24'
- name: Performance Reporting
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
with:
mode: walltime
run: inv dev.test --pytest

View File

@ -7,7 +7,7 @@ on:
permissions:
contents: read
env:
python_version: 3.11
python_version: 3.12
jobs:
stable:
@ -151,9 +151,9 @@ jobs:
fail-fast: false
matrix:
target:
- ubuntu:22.04
- ubuntu:24.04
- debian:12
- ubuntu:26.04
- debian:13
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3

View File

@ -6,7 +6,7 @@ on:
- master
env:
python_version: 3.11
python_version: 3.12
node_version: 24
permissions:
@ -56,7 +56,7 @@ jobs:
echo "Resetting to HEAD~"
git reset HEAD~ || true
- name: crowdin action
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # pin@v2
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # pin@v2
with:
upload_sources: true
upload_translations: false

View File

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

View File

@ -37,7 +37,7 @@ repos:
files: src/backend/requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements-dev.in 3.14
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev-3.14.txt, -c, src/backend/requirements-3.14.txt, --python-version=3.14, -c, src/backend/requirements.txt]
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev-3.14.txt, -c, src/backend/requirements-3.14.txt, --python-version=3.14, -c, src/backend/requirements.txt, -c, src/backend/requirements-dev.txt]
files: src/backend/requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
@ -97,4 +97,4 @@ repos:
rev: 0.4.3
hooks:
- id: teyit
language_version: python3.11
language_version: python3.12

2
.vscode/launch.json vendored
View File

@ -47,7 +47,7 @@
"name": "InvenTree invoke schema",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/.venv/lib/python3.11/site-packages/invoke/__main__.py",
"program": "${workspaceFolder}/.venv/lib/python3.12/site-packages/invoke/__main__.py",
"cwd": "${workspaceFolder}",
"args": [
"dev.schema","--ignore-warnings"

5
AGENTS.md Normal file
View File

@ -0,0 +1,5 @@
See [CONTRIBUTING.md](CONTRIBUTING.md) for codebase guidance.
Security reports must follow the security policy in [SECURITY.md](docs/docs/SECURITY.md) and provide how they interact with the [threat model](docs/docs/concepts/threat_model.md).
Do not open a pull request without manual review by a human. Do not file AI generated issues under any circumstances.
Using AI generated content in issue/PR discussions might lead to code of conduct violations and/or bans.

View File

@ -5,7 +5,21 @@ All major notable changes to this project will be documented in this file (start
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased - YYYY-MM-DD
## Unreleased - xxxx.xx.xx
### Breaking Changes
- [#12223](https://github.com/inventree/InvenTree/pull/12223) removes support for python 3.11 and stops providing packages for Debian 11 and Ubuntu 20.04.
### Added
- [#12250](https://github.com/inventree/InvenTree/pull/12250) adds "active" field to the ProjectCode model and API endpoints
### Changed
### Removed
## 1.4.0 - 2026-06-24
### Breaking Changes

5
CLAUDE.md Normal file
View File

@ -0,0 +1,5 @@
See [CONTRIBUTING.md](CONTRIBUTING.md) for codebase guidance.
Security reports must follow the security policy in [SECURITY.md](docs/docs/SECURITY.md) and provide how they interact with the [threat model](docs/docs/concepts/threat_model.md).
Do not open a pull request without manual review by a human. Do not file AI generated issues under any circumstances.
Using AI generated content in issue/PR discussions might lead to code of conduct violations and/or bans.

View File

@ -1,15 +1,28 @@
### Contributing to InvenTree
# Contributing to InvenTree
Hi there, thank you for your interest in contributing!
Please read our contribution guidelines, before submitting your first pull request to the InvenTree codebase.
### Project File Structure
## About InvenTree
InvenTree is an open-source inventory management system designed for makers, engineers, and small manufacturers.
The codebase is split into two independently deployable layers:
**Backend — Django (Python)**
A REST API built with Django and Django REST Framework. It owns all business logic, the database schema, background task processing (Django-Q2), plugin execution, and report generation. The backend can be used standalone via the API without the frontend.
**Frontend — React (TypeScript)**
A single-page application built with React 19, Mantine 9, and Vite. It communicates exclusively through the backend REST API. It has its own build pipeline, test suite (Playwright), and i18n system (Lingui). The frontend lives entirely in `src/frontend/` and has no Django awareness.
When making changes, be clear about which layer you are working in — they have separate toolchains, test runners, and linting rules.
## Project File Structure
The InvenTree project is split into two main components: frontend and backend. This source is located in the `src` directory. All other files are used for project management, documentation, and testing.
```bash
```
InvenTree/
├─ .devops/ # Files for Azure DevOps
├─ .github/ # Files for GitHub
│ ├─ actions/ # Reused actions
│ ├─ ISSUE_TEMPLATE/ # Templates for issues and pull requests
@ -39,11 +52,209 @@ InvenTree/
│ │ ├─ tsconfig.json # Settings for frontend compilation
├─ .pkgr.yml # Build definition for Debian/Ubuntu packages
├─ .pre-commit-config.yaml # Code formatter/linter configuration
├─ biome.json # JS/TS linting and formatting config
├─ CONTRIBUTING.md # Contribution guidelines and overview
├─ Procfile # Process definition for Debian/Ubuntu packages
├─ pyproject.toml # Python tooling config (Ruff, pytest, coverage, djLint, ty)
├─ README.md # General project information and overview
├─ SECURITY.md # Project security policy
├─ tasks.py # Action definitions for development, testing and deployment
```
### Backend app domains
The following Django apps are defined in `src/backend/InvenTree/`:
| Directory | Domain |
|-----------|--------|
| `build/` | Build (manufacturing/assembly) orders |
| `common/` | Shared models, settings, utilities |
| `company/` | Suppliers, customers, manufacturers |
| `data_exporter/` | Data export functionality |
| `generic/` | Shared enums, events, and state machine utilities |
| `importer/` | Data import functionality |
| `InvenTree/` | Core configuration (settings, URLs, middleware) |
| `machine/` | Support for external machines and devices |
| `order/` | Purchase orders and sales orders |
| `part/` | Parts catalogue and categories |
| `stock/` | Stock items and locations |
| `report/` | Report templates and generation |
| `plugin/` | Plugin system |
| `users/` | User accounts and permissions |
| `web/` | Serves the React frontend static build to Django |
### Frontend layout
```
src/frontend/src/
components/ # Reusable React components
pages/ # Page-level route components
tables/ # Data table components
forms/ # Form components
hooks/ # Custom React hooks
states/ # Zustand global state
locales/ # i18n translation catalogues (Lingui)
```
## Development Setup
The project uses [Invoke](https://www.pyinvoketasks.com/) (`tasks.py`) as the task runner.
```bash
# One-time setup: creates venv at dev/venv/, installs deps, sets up pre-commit hooks
invoke dev.setup-dev
# Apply database migrations
invoke migrate
# Create an admin account (required to log in)
invoke superuser
# Terminal 1 — Django dev server (port 8000)
invoke dev.server
# Terminal 2 — Vite dev server with HMR (port 5173)
invoke dev.frontend-server
# Terminal 3 (optional) — Background task worker
invoke worker
```
A VS Code Dev Container configuration is available at `.devcontainer/` and includes PostgreSQL 15 and Redis 7 sidecar services.
To see all available tasks, run `invoke --list`.
## Running Backend Tests
Always pass `--keepdb` to backend test commands. It reuses the existing test database instead of rebuilding it from scratch on every run, which is significantly faster.
```bash
# All backend tests
invoke dev.test --keepdb
# Specific test module
invoke dev.test --keepdb --runtest=company.test_api
# With coverage
invoke dev.test --keepdb --coverage
# Migration tests only (skip --keepdb here — migrations must run against a fresh DB)
invoke dev.test --migrations
```
## Running Frontend Tests
Frontend tests use [Playwright](https://playwright.dev/) and target Chromium and Firefox. The test runner automatically starts the Vite dev server (port 5173), the Django backend (port 8000), and the background worker — no manual server startup is needed.
```bash
# Open Playwright's interactive UI (recommended for local development)
invoke dev.frontend-test
# Run tests in headless mode from the frontend directory
cd src/frontend && npx playwright test
# Run a specific test file
cd src/frontend && npx playwright test tests/pui_login.spec.ts
# Run tests in a specific browser only
cd src/frontend && npx playwright test --project=chromium
```
Test files live in `src/frontend/tests/` and follow the `pui_*.spec.ts` naming convention.
Locally, tests run with a single worker (required for Vite HMR compatibility). In CI they run with multiple workers against a production build for speed.
## Code Style and Linting
All formatting and linting runs automatically on commit via pre-commit hooks. Never skip them.
### Python (Backend)
Tool: **Ruff** (replaces Black, isort, flake8, and others).
```bash
ruff check src/backend/ # Lint
ruff format src/backend/ # Format
```
- Google-style docstrings enforced
- Enabled rule sets: A, B, C, D, F, I, N, SIM, PIE, PLE, PLW, RUF, UP, W
- Type checking: `ty` (configured in `pyproject.toml`)
### Django Templates
Tool: **djLint**
```bash
djlint src/backend/ --check
```
### JavaScript / TypeScript (Frontend)
Tool: **Biome** (replaces ESLint + Prettier).
```bash
cd src/frontend
yarn biome check . # Lint + format check
yarn biome format . # Format only
```
- Single quotes, space indentation
- `noUnusedImports` is an error
## Making Changes
### Backend
- Each Django app has its own `models.py`, `serializers.py`, `views.py`, `urls.py`, and `filters.py`.
- API endpoints use Django REST Framework; document them with drf-spectacular decorators.
- After changing models, create a migration: `invoke dev.makemigrations`.
- Tests live in `test_*.py` files within each app directory.
- Background tasks go through Django-Q2; define them in the app's `tasks.py`.
### Frontend
- Pages register themselves in `src/frontend/src/router.tsx`.
- Server state fetching uses TanStack Query (React Query); avoid raw `useEffect` for data fetching.
- Global UI state uses Zustand stores in `states/`.
- UI components come from Mantine 9.x; use the Mantine component library before writing custom CSS.
- Add i18n strings with Lingui (`t` macro / `Trans` component); run `invoke int.frontend-trans` to extract from source and compile. (`invoke dev.translate` is for the full translation pipeline and is not intended for local use.)
- Playwright tests live in `src/frontend/tests/`.
### Dependencies
**Python:** Add packages to `src/backend/requirements.in` (or `requirements-dev.in` for dev-only tools). The pre-commit hook runs `pip-compile` automatically on commit to regenerate `requirements.txt`. Never edit `requirements.txt` directly.
**Frontend:** Add packages with `yarn add <package>` from `src/frontend/`.
### Migrations
- Never edit existing migration files; always generate new ones.
- Keep migrations reversible where possible.
- Migration tests run in CI under the tag `migration_test`.
## CI / CD
GitHub Actions workflows live in `.github/workflows/`. Key workflows:
| Workflow | Purpose |
|----------|---------|
| `qc_checks.yaml` | Lint, type-check, backend tests, API schema |
| `frontend.yaml` | Playwright E2E tests, frontend build |
| `docker.yaml` | Docker image builds |
| `translations.yaml` | Crowdin i18n sync |
CI uses path-based filtering — only affected jobs run per PR. Coverage tracked via Codecov; quality analysis via SonarCloud.
## Key Conventions
- **REST API versioning:** Endpoint changes must not break the published API schema. Run `invoke dev.schema` and check the diff before opening a PR.
- **Plugin safety:** The plugin system (`plugin/`) is a public extension point; avoid breaking its interfaces.
- **No hardcoded secrets:** gitleaks runs in pre-commit and CI — any credential-shaped string will block merges.
- **Database portability:** Code must work on PostgreSQL, MySQL/MariaDB, and SQLite. Avoid database-specific SQL or ORM features.
- **Frontend translations:** Every user-visible string must be wrapped in a Lingui `t` macro or `<Trans>` component.
- **Test tagging:** Tag slow or special tests (`@tag('migration_test')`, `@tag('performance_test')`) so CI can filter them selectively.
---
Refer to our [contribution guidelines](https://docs.inventree.org/en/latest/develop/contributing/) for more information!

View File

@ -9,7 +9,6 @@
[![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
[![Netlify Status](https://api.netlify.com/api/v1/badges/9bbb2101-0a4d-41e7-ad56-b63fb6053094/deploy-status)](https://app.netlify.com/sites/inventree/deploys)
[![Performance Testing](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_apis/build/status%2Fmatmair.InvenTree?branchName=testing)](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_build/latest?definitionId=3&branchName=testing)
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7179/badge)](https://bestpractices.coreinfrastructure.org/projects/7179)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/inventree/InvenTree/badge)](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
@ -202,6 +201,7 @@ Find a full list of used third-party libraries in the license information dialog
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-bg.svg" alt="Deploys by Netlify" /> </a>
<a href="https://crowdin.com"> <img src="https://crowdin.com/images/crowdin-logo.svg" alt="Translation by Crowdin" /> </a> <br>
<a href="https://codspeed.io/inventree/InvenTree?utm_source=badge"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"/></a>
<a href="https://flakiness.io/InvenTree/InvenTree"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fflakiness.io%2Fapi%2Fbadge%3Finput%3D%257B%2522badgeToken%2522%253A%2522badge-35mqq5Ht4uL3vGF8lR9P2D%2522%257D" alt="Flakiness Badge"/></a>
</p>

View File

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

View File

@ -119,7 +119,8 @@ RUN pip install --user --require-hashes -r base_requirements.txt --no-cache-dir
rm -rf /root/.cache/pip
# Install frontend build dependencies
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash && \
# pinned to v0.40.5
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/1889911f0841e669de0be5bd02c737a3f1fd20fa/install.sh | bash && \
bash -c "export NVM_DIR="$HOME/.nvm" && source $HOME/.nvm/nvm.sh && \
nvm install $NODE_VERSION && corepack enable yarn && yarn config set network-timeout 600000 -g"
RUN bash -c "source $HOME/.nvm/nvm.sh && cd '${INVENTREE_HOME}' && invoke int.frontend-compile --extract"
@ -145,6 +146,9 @@ ENV PATH=/root/.local/bin:$PATH
COPY --from=builder_stage ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web
COPY --from=builder_stage /root/.local /root/.local
# Compile Django backend translations during image build
RUN bash -c "cd '${INVENTREE_HOME}' && invoke int.backend-compilemessages"
# Launch the production server
CMD ["sh", "-c", "exec gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT} --chdir ${INVENTREE_BACKEND_DIR}/InvenTree"]

View File

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

View File

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

View File

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

View File

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

View File

@ -79,7 +79,7 @@ function detect_python() {
echo "${On_Red}"
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.11'."
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.12'."
echo "${Color_Off}"
exit 1
fi

View File

@ -26,7 +26,7 @@ export DATA_DIR=${APP_HOME}/data
export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false}
export SETUP_PYTHON=${SETUP_PYTHON:-python3.11}
export SETUP_PYTHON=${SETUP_PYTHON:-python3.12}
export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
# SETUP_DEBUG can be set to get debug info
# SETUP_EXTRA_PIP can be set to install extra pip packages

View File

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

View File

@ -54,14 +54,14 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
VERSION = "0.0.1"
AUTHOR = "Michael"
def scan(self, barcode_data):
def scan(self, barcode_data, user, **kwargs):
if barcode_data.startswith("PART-"):
try:
pk = int(barcode_data.split("PART-")[1])
instance = Part.objects.get(pk=pk)
label = Part.barcode_model_type()
return {label: instance.format_matched_response()}
return {label: instance.format_matched_response(user=user)}
except Part.DoesNotExist:
pass
```

View File

@ -14,6 +14,8 @@ Individuals and companies can also support via [GitHub sponsors](https://github.
- [Codecov](https://codecov.io) - Code coverage as a service, used to track the code coverage of the various components
- [Netlify](https://www.netlify.com/) - Static site hosting provider, used to test deploy the frontend and website
- [Depot](https://depot.dev/?utm_source=inventree) - Docker build accelerator, used to build the multi-arch images for the InvenTree docker image
- [CodSpeed](https://codspeed.io/) - Performance testing platform, used to track the performance of a few performance benchmarks
- [Flakiness](https://flakiness.io/) - Frontend test flakiness detection, used to track the flakiness of the various test harnesses
## Past Supporters

View File

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

View File

@ -325,7 +325,7 @@ ARG INVENTREE_TAG
# prebuild stage - needs a lot of build dependencies
# make sure, the alpine and python version matches the version used in the inventree base image
FROM python:3.11-alpine3.18 as prebuild
FROM python:3.12-alpine3.18 as prebuild
# Install whatever development dependency is needed (e.g. cups-dev, gcc, the musl-dev build tools and the pip pycups package)
RUN apk add --no-cache cups-dev gcc musl-dev && \

View File

@ -366,7 +366,7 @@ extra:
# provider: google
# property: UA-143467500-1
min_python_version: 3.11
min_python_version: 3.12
min_invoke_version: 2.0.0
django_version: 5.2
docker_postgres_version: 17

View File

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

View File

@ -97,25 +97,24 @@ skip-magic-trailing-comma = true
line-ending = "auto"
[tool.uv.pip]
python-version = "3.11"
python-version = "3.12"
no-strip-extras=true
generate-hashes=true
[tool.ty.environment]
root = ["src/backend/InvenTree"]
extra-paths = ["src/backend/InvenTree"]
[tool.ty.rules]
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
unresolved-attribute="ignore" # 837 # need Plugin Mixin typing
call-non-callable="ignore" # 8 ##
invalid-assignment="ignore" # 17 # need to wait for better django field stubs
# invalid-method-override="ignore" # 99
invalid-return-type="ignore" # 22 ##
# possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
unknown-argument="ignore" # 3 # need to wait for better django field stubs
invalid-assignment="ignore" # 40 # need to wait for better django field stubs
invalid-method-override="ignore" # 103
invalid-return-type="ignore" # 44 ##
invalid-argument-type="ignore" # 49
no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
possibly-unbound-attribute="ignore" # 21
no-matching-overload="ignore" # 10 # need to wait for betterdjango field stubs
# warn about unused ignore comments
unused-ignore-comment = "error"
[tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"]

View File

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

View File

@ -494,7 +494,7 @@ class BulkCreateMixin:
if unique_create_fields := getattr(self, 'unique_create_fields', None):
existing = collections.defaultdict(list)
for idx, item in enumerate(data):
key = tuple(item[v] for v in list(unique_create_fields)) # type: ignore[not-subscriptable]
key = tuple(item[v] for v in list(unique_create_fields)) # ty: ignore[not-subscriptable]
existing[key].append(idx)
unique_errors = [[] for _ in range(len(data))]

View File

@ -1,11 +1,19 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 511
INVENTREE_API_VERSION = 513
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v513 -> 2026-06-25 : https://github.com/inventree/InvenTree/pull/12250
- Adds "active" field to the ProjectCode model and API endpoints
v512 -> 2026-06-20 : https://github.com/inventree/InvenTree/pull/12022
- Adds optional "merge" field to each item in the Stock Transfer API endpoint
- When merge is enabled, transferred stock is combined into compatible existing stock at the destination
- Stock merge tracking entries now include an "added" delta field.
v511 -> 2026-06-19 : https://github.com/inventree/InvenTree/pull/12204
- Adds new filtering options to PartCategoryTree and StockLocationTree API endpoints

View File

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

View File

@ -106,6 +106,32 @@ apps_mfa_bypass = [
"""App namespaces that bypass MFA enforcement - normal security model is still enforced."""
def csrf_failure(request, reason=''):
"""Custom CSRF failure handler.
Returns a JSON response for API/headless requests so the frontend can
provide a meaningful error message to the user
"""
from django.views.csrf import csrf_failure as django_default
if (
request.path.startswith('/_allauth/')
or request.path.startswith('/api/')
or 'application/json' in request.headers.get('Accept', '')
or 'application/json' in request.headers.get('Content-Type', '')
):
return JsonResponse(
{
'detail': _(
'CSRF verification failed. Ensure INVENTREE_SITE_URL and INVENTREE_TRUSTED_ORIGINS are configured correctly.'
)
},
status=403,
)
return django_default(request, reason=reason)
class AuthRequiredMiddleware:
"""Check for user to be authenticated."""

View File

@ -23,6 +23,7 @@ from django_q.models import Task
from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
from rest_framework.exceptions import PermissionDenied
from stdimage.models import StdImageField
from taggit.managers import TaggableManager
@ -1334,8 +1335,16 @@ class InvenTreeBarcodeMixin(models.Model):
return generate_barcode(self)
def format_matched_response(self):
def format_matched_response(self, user, **kwargs):
"""Format a standard response for a matched barcode."""
# Check permission for this object
from users.permissions import check_user_permission
if not check_user_permission(user, self, 'view'):
raise PermissionDenied(
_('User does not have permission to view this model')
)
data = {'pk': self.pk}
if hasattr(self, 'get_api_url'):

View File

@ -282,7 +282,7 @@ def postprocess_print_stats(result, generator, request, public):
scopes = {}
for path, details in rlt_dict.items():
if details['oauth']:
for scope in details['oauth']:
for scope in list(details['oauth']):
if scope not in scopes:
scopes[scope] = []
scopes[scope].append(path)
@ -343,7 +343,7 @@ def schema_for_view_output_options(view_class):
return extended_view
def exclude_from_schema(klass: type, alternative_path: str) -> type:
def exclude_from_schema(klass: type[Any], alternative_path: str) -> type[Any]:
"""Decorator to exclude a view from the OpenAPI schema.
This is used to hide legacy endpoints from the schema, while still retaining them for backwards compatibility.

View File

@ -9,8 +9,8 @@ def get_ldap_config(debug: bool = False) -> dict:
The returned settings will be updated into the globals() object,
and will be used to configure the LDAP authentication backend.
"""
import django_auth_ldap.config # type: ignore[unresolved-import]
import ldap # type: ignore[unresolved-import]
import django_auth_ldap.config # ty: ignore[unresolved-import]
import ldap # ty: ignore[unresolved-import]
# get global options from dict and use ldap.OPT_* as keys and values
global_options_dict = get_setting(

View File

@ -851,6 +851,7 @@ valid_cookie_modes = ['lax', 'strict', 'none']
COOKIE_MODE = COOKIE_MODE.capitalize() if COOKIE_MODE in valid_cookie_modes else False
# Additional CSRF settings
CSRF_FAILURE_VIEW = 'InvenTree.middleware.csrf_failure'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_COOKIE_NAME = 'csrftoken'

View File

@ -2,8 +2,7 @@
from unittest.mock import patch
from django.conf import settings
from django.http import Http404
from django.http import Http404, HttpRequest
from django.urls import reverse
from error_report.models import Error
@ -289,18 +288,63 @@ class MiddlewareTests(InvenTreeTestCase):
response, 'window.INVENTREE_SETTINGS', status_code=500
)
# Log stuff # TODO remove
print(
'###DBG-TST###',
'site',
settings.SITE_URL,
'trusted',
settings.CSRF_TRUSTED_ORIGINS,
)
# Check that the correct step triggers the error message
self.assertContains(
response,
'INVE-E7: The visited path `http://testserver` does not match',
status_code=500,
)
def test_csrf_failure(self):
"""Test the custom CSRF failure handler."""
from InvenTree.middleware import csrf_failure
EXPECTED_DETAIL = 'CSRF verification failed. Ensure INVENTREE_SITE_URL and INVENTREE_TRUSTED_ORIGINS are configured correctly.'
def make_request(path, headers=None):
request = HttpRequest()
request.path = path
request.META['SERVER_NAME'] = 'testserver'
request.META['SERVER_PORT'] = '80'
for key, value in (headers or {}).items():
request.META[f'HTTP_{key.upper().replace("-", "_")}'] = value
return request
# API path -> JSON 403 with meaningful message
response = csrf_failure(
make_request('/api/part/'), reason='origin check failed'
)
self.assertEqual(response.status_code, 403)
import json
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# allauth headless path -> JSON 403
response = csrf_failure(make_request('/_allauth/browser/v1/auth/login'))
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Accept: application/json header -> JSON 403
response = csrf_failure(
make_request('/some/other/path/', {'Accept': 'application/json'}),
reason='origin check failed',
)
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Content-Type: application/json header -> JSON 403
response = csrf_failure(
make_request('/some/other/path/', {'Content-Type': 'application/json'}),
reason='origin check failed',
)
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Plain browser request -> falls back to Django default CSRF page (403 HTML, not JSON)
response = csrf_failure(make_request('/web/'), reason='origin check failed')
self.assertEqual(response.status_code, 403)
self.assertNotIn(b'application/json', response.get('Content-Type', '').encode())

View File

@ -15,10 +15,10 @@ from datetime import timedelta as td
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '1.4.0 dev'
INVENTREE_SW_VERSION = '1.5.0 dev'
# Minimum supported Python version
MIN_PYTHON_VERSION = (3, 11)
MIN_PYTHON_VERSION = (3, 12)
logger = logging.getLogger('inventree')

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from django.contrib.auth.models import User
from django.db.models import F, OuterRef, Q, Subquery, Sum
from django.db.models import DecimalField, F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.urls import include, path
from django.utils.translation import gettext_lazy as _
@ -494,9 +494,25 @@ class BuildLineFilter(FilterSet):
def filter_allocated(self, queryset, name, value):
"""Filter by whether each BuildLine is fully allocated."""
allocated_subquery = (
BuildItem.objects
.filter(build_line=OuterRef('pk'))
.values('build_line')
.annotate(total=Sum('quantity'))
.values('total')
)
queryset = queryset.alias(
allocated_quantity=Coalesce(
Subquery(allocated_subquery), 0, output_field=DecimalField()
)
)
if str2bool(value):
return queryset.filter(allocated__gte=F('quantity') - F('consumed'))
return queryset.filter(allocated__lt=F('quantity') - F('consumed'))
return queryset.filter(
allocated_quantity__gte=F('quantity') - F('consumed')
)
return queryset.filter(allocated_quantity__lt=F('quantity') - F('consumed'))
consumed = rest_filters.BooleanFilter(label=_('Consumed'), method='filter_consumed')
@ -528,7 +544,9 @@ class BuildLineFilter(FilterSet):
)
queryset = queryset.alias(
allocated_quantity=Coalesce(Subquery(allocated_subquery), 0)
allocated_quantity=Coalesce(
Subquery(allocated_subquery), 0, output_field=DecimalField()
)
)
# A query filter construct to determine the total quantity available for this BuildLine,

View File

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

View File

@ -491,7 +491,7 @@ class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['code']
filterset_fields = ['active']
search_fields = ['code', 'description']
@ -577,7 +577,7 @@ class CustomUnitViewset(DataExportViewMixin, viewsets.ModelViewSet):
return {
'name': k,
'is_alias': reg.get_name(k) == k,
'compatible_units': [str(a) for a in unit.compatible_units()],
'compatible_units': [str(a) for a in unit.compatible_units()], # ty:ignore[missing-argument]
'isdimensionless': unit.dimensionless,
}

View File

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

View File

@ -181,6 +181,12 @@ class ProjectCode(InvenTree.models.InvenTreeMetadataModel):
help_text=_('Project description'),
)
active = models.BooleanField(
default=True,
verbose_name=_('Active'),
help_text=_('Is this project code active?'),
)
responsible = models.ForeignKey(
users.models.Owner,
on_delete=models.SET_NULL,

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ TESTING = 'test' in sys.argv
def clear():
if not TESTING: # pragma: no cover
os.system('cls' if os.name == 'nt' else 'clear')
os.system('cls' if os.name == 'nt' else 'clear') # ty:ignore[deprecated]
def reverse_association(apps, schema_editor): # pragma: no cover

View File

@ -99,6 +99,10 @@ class DataExportSerializerMixin:
fields.update(self.get_child_fields(name, field))
continue
# Skip 'many' fields (e.g. nested serializers)
if getattr(field, 'many', False):
continue
fields[name] = field
return fields

View File

@ -6,6 +6,7 @@ from datetime import datetime
from typing import Optional
from django.contrib.auth.models import User
from django.core.exceptions import FieldDoesNotExist
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
@ -200,7 +201,7 @@ class DataImportSession(models.Model):
related_field = model_class._meta.get_field(field_name)
model = related_field.remote_field.model
return model
except (AttributeError, models.FieldDoesNotExist):
except (AttributeError, FieldDoesNotExist):
return None
def extract_columns(self) -> None:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -272,7 +272,7 @@ class ReturnOrderReportContext(report.mixins.BaseReportContext, TypedDict):
customer: Optional[Company]
class TransferOrderReportContext(report.mixins.BaseReportContext, TypedDict):
class TransferOrderReportContext(BaseOrderReportContext, TypedDict):
"""Context for the transfer order model.
Attributes:
@ -639,7 +639,8 @@ class PurchaseOrder(TotalPriceMixin, Order):
def report_context(self) -> PurchaseOrderReportContext:
"""Return report context data for this PurchaseOrder."""
return_ctx = super().report_context()
return_ctx.update({'supplier': self.supplier})
return_ctx.update({'supplier': self.supplier}) # ty:ignore[invalid-key]
return return_ctx
def get_absolute_url(self) -> str:
@ -1372,7 +1373,8 @@ class SalesOrder(TotalPriceMixin, Order):
def report_context(self) -> SalesOrderReportContext:
"""Generate report context data for this SalesOrder."""
return_ctx = super().report_context()
return_ctx.update({'customer': self.customer})
return_ctx.update({'customer': self.customer}) # ty:ignore[invalid-key]
return return_ctx
def get_absolute_url(self) -> str:
@ -2931,7 +2933,8 @@ class ReturnOrder(TotalPriceMixin, Order):
def report_context(self) -> ReturnOrderReportContext:
"""Generate report context data for this ReturnOrder."""
return_ctx = super().report_context()
return_ctx.update({'customer': self.customer})
return_ctx.update({'customer': self.customer}) # ty:ignore[invalid-key]
return return_ctx
def get_absolute_url(self):

View File

@ -153,7 +153,9 @@ class BarcodeView(CreateAPIView):
for current_plugin in plugins:
try:
result = current_plugin.scan(barcode)
result = current_plugin.scan(barcode, user=request.user, **kwargs)
except PermissionDenied as exc:
raise exc
except Exception:
log_error('BarcodeView.scan_barcode', plugin=current_plugin.slug)
continue
@ -282,7 +284,7 @@ class BarcodeAssign(BarcodeView):
# First check if the provided barcode matches an existing database entry
if inventree_barcode_plugin:
result = inventree_barcode_plugin.scan(barcode)
result = inventree_barcode_plugin.scan(barcode, user=request.user, **kwargs)
if result is not None:
result['error'] = _('Barcode matches existing item')
@ -459,7 +461,9 @@ class BarcodePOAllocate(BarcodeView):
manufacturer_part=response.get('manufacturerpart', None),
)
response['success'] = _('Matched supplier part')
response['supplierpart'] = supplier_part.format_matched_response()
response['supplierpart'] = supplier_part.format_matched_response(
user=request.user
)
except ValidationError as e:
response['error'] = str(e)
@ -524,7 +528,7 @@ class BarcodePOReceive(BarcodeView):
filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins)
)
if result := internal_barcode_plugin.scan(barcode):
if result := internal_barcode_plugin.scan(barcode, user=request.user, **kwargs):
if 'stockitem' in result:
response['error'] = _('Item has already been received')
self.log_scan(request, response, False)

View File

@ -42,7 +42,7 @@ class BarcodeMixin:
"""Does this plugin have everything needed to process a barcode."""
return True
def scan(self, barcode_data):
def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
"""Scan a barcode against this plugin.
This method is explicitly called from the /scan/ API endpoint,
@ -261,7 +261,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
'extract_barcode_fields must be implemented by each plugin'
)
def scan(self, barcode_data: str) -> dict | None:
def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
"""Perform a generic 'scan' operation on a supplier barcode.
The supplier barcode may provide sufficient information to match against
@ -297,7 +297,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
for k, v in matches.items():
if v and hasattr(v, 'pk'):
has_match = True
data[k] = v.format_matched_response()
data[k] = v.format_matched_response(user=user)
if not has_match:
return None

View File

@ -15,6 +15,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
"""Tests for barcode api."""
fixtures = ['category', 'part', 'location', 'stock']
roles = ['stock.view', 'stock_location.view', 'part.view']
def setUp(self):
"""Setup for all tests."""
@ -259,6 +260,7 @@ class SOAllocateTest(InvenTreeAPITestCase):
"""Unit tests for the barcode endpoint for allocating items to a sales order."""
fixtures = ['category', 'company', 'part', 'location', 'stock']
roles = ['stock.view']
@classmethod
def setUpTestData(cls):
@ -343,10 +345,14 @@ class SOAllocateTest(InvenTreeAPITestCase):
# Test with barcode which points to a *part* instance
item.part.assign_barcode(barcode_data='abcde')
# missing permission for viewing the part - error
self.postBarcode('abcde', sales_order=self.sales_order.pk, expected_code=403)
# Add part.view role and test again
self.assignRole('part.view')
result = self.postBarcode(
'abcde', sales_order=self.sales_order.pk, expected_code=400
)
self.assertIn('does not match an existing stock item', str(result['error']))
def test_submit(self):

View File

@ -7,4 +7,4 @@ class BrokenFileIntegrationPlugin(InvenTreePlugin):
"""An very broken plugin."""
aaa = bb # noqa: F821
aaa = bb # noqa: F821 # ty:ignore[unresolved-reference]

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