Merge branch 'master' into fix-supplier-import-stock-redirect
This commit is contained in:
commit
59688b57ef
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -9,7 +9,7 @@ on:
|
|||
- l10
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
python_version: 3.12
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
.pkgr.yml
10
.pkgr.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
219
CONTRIBUTING.md
219
CONTRIBUTING.md
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
[](https://inventree.readthedocs.io/en/latest/?badge=latest)
|
||||

|
||||
[](https://app.netlify.com/sites/inventree/deploys)
|
||||
[](https://dev.azure.com/InvenTree/InvenTree%20test%20statistics/_build/latest?definitionId=3&branchName=testing)
|
||||
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7179)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/inventree/InvenTree)
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ flag_management:
|
|||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 40%
|
||||
target: 38%
|
||||
- name: web
|
||||
carryforward: true
|
||||
statuses:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Packages needed for CI/packages
|
||||
requests==2.34.2
|
||||
pyyaml==6.0.3
|
||||
jc==1.25.6
|
||||
jc
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@
|
|||
},
|
||||
{
|
||||
"pattern": "https://opensource.org/license/MIT"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://dev.azure.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue