Compare commits

...

28 Commits

Author SHA1 Message Date
Oliver 4789a202ee
Disable broken unit test (#11549) 2026-03-17 22:40:11 +11:00
Oliver 60f2d9d314
Fix docs link for redis (#11534) 2026-03-16 11:21:47 +11:00
Oliver c173c4e051
Fix stuck-in-past expiry date filter (#11518) (#11523)
* fix stuck-in-past expiry date filter

* also fix OVERDUE_FILTER date problem

Co-authored-by: Jacob Felknor <jacobfelknor073@gmail.com>
2026-03-16 00:12:38 +11:00
github-actions[bot] 73cab199f1
Fix table for s3 backup in docs (#11512) (#11524)
(cherry picked from commit da982dc3dd)

Co-authored-by: Knirps1 <52383078+Knirps1@users.noreply.github.com>
2026-03-14 23:37:28 +11:00
github-actions[bot] e749ece58e
fix: low-privilege user token creation (#11492) (#11521)
* [bug] Users cannot create their own API tokens
Fixes #11486

* fix detection of metadata

* make easier to read

* add handler for IsAuthenticated

* use correct method

* fix style see #11487

* add frontend test

* make test more reliable?

(cherry picked from commit fda3204e33)

Co-authored-by: Matthias Mair <code@mjmair.com>
2026-03-14 12:35:28 +11:00
github-actions[bot] e5ace7d761
Add poppler-utils to packager.io dependencies (#11508) (#11509)
Required for label printing - pdf2image cannot convert PDFs to images without poppler's pdfinfo binary being present on the system.


(cherry picked from commit 6deed010a3)

Co-authored-by: jbell <jbell101@gmail.com>
Co-authored-by: Justyn Bell <justyn.bell@subinitial.com>
2026-03-13 22:33:44 +11:00
github-actions[bot] 5bf41f1463
[UI] Fix hook deps for BuildOutputTable (#11483) (#11495)
- Closes https://github.com/inventree/InvenTree/issues/11280

(cherry picked from commit aafda4bece)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-03-12 13:30:28 +11:00
github-actions[bot] 59f3b9f2f6
fix: wrap create_default_labels in transaction.atomic() savepoint to prevent PostgreSQL transaction abort (#11484) (#11490)
When the plugin registry reloads during an active HTTP request,
create_default_labels() raises ValidationError on duplicate template
inserts. On PostgreSQL this aborts the entire outer atomic() block,
causing TransactionManagementError on all stock operations in that request.

Fixes #11469


(cherry picked from commit a964d6682e)

Co-authored-by: tigger2000ttfn <tigger2000ttfn@gmail.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-03-11 22:28:22 +11:00
Matthias Mair f2fa65ff1d
chore(deps): bump pypdf from 6.7.4 to 6.7.5 in /src/backend (#11454) (#11462)
* chore(deps): bump pypdf from 6.7.4 to 6.7.5 in /src/backend

Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.7.4 to 6.7.5.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.7.4...6.7.5)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix style

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
(cherry picked from commit 3ed21e00cf)

# Conflicts:
#	src/backend/requirements-3.14.txt
#	src/backend/requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 09:44:49 +11:00
github-actions[bot] 9cfec02a69
[API] Enforce pk ordering for API endpoints (#11446) (#11447)
* Add unit test to detect unreliable pagination

* Enforce PK field ordering

- Append 'pk' ordering to InvenTreeOrderingFilter

* Use our ordering filter everywhere

* Simplify ordering options

* Enforce list

* Use last term for ordering checks

* Individual delete to fix mysql issue

(cherry picked from commit 3bbdddf51d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-03-02 23:07:00 +11:00
github-actions[bot] c35ab145a4
Prevent exchange rate update during migration (#11429) (#11430)
(cherry picked from commit fb3b1189fd)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-27 09:36:53 +11:00
Oliver b611960d9f
Bump software version to 1.2.4 (#11402) 2026-02-23 09:10:40 +11:00
Oliver 12da3b01fc
Merge commit from fork (#11400)
- Change one renderer from jinja to django
- Move the other to a sandboxed environment
2026-02-22 23:25:31 +11:00
Oliver 2df9eb80ca
[bug] Fix custom logo and splash (#11391) (#11399)
* Add custom logo and splash to playwright fixtures

* Render custom logo in frontend

* Add playwright tests for customization settings

* Separate playwright run for customization

* Update ci file

* Update playwright config file

* Do not call get_custom_file from withing settings.py

- django app not fully initialized yet
- can cause loops

* Disable default tests (for now)

* Re-enable normal tests

* Update qc checks

* Order of operations?

* Debug output

* Use env

* Add more debug info

* More debug

* Further debug

* Add collectstatic check

* Run normal tests too

* Remove debug prints
2026-02-22 22:46:25 +11:00
github-actions[bot] 44387f3bb4
[bug] Check for null plugin config (#11394) (#11395)
* Check for null plugin config

* Handle potential errors

(cherry picked from commit 2e675ee87a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-22 12:56:57 +11:00
Oliver ed8331e26a
Bump InvenTree software version to 1.2.3 (#11382) 2026-02-19 22:57:39 +11:00
Oliver 228577c871
[UI] Bug fix for stock column ordering (#11376) (#11380)
- Cannot have a blank accessor
2026-02-19 22:09:58 +11:00
github-actions[bot] fd7d0bda0f
Batch update of records during migration (#11371) (#11373)
* Batch update of records during migration

Reduce issues with very large datasets

* Add short-circuit logic

* Filter and prefetch

(cherry picked from commit a4a399022f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-19 12:07:16 +11:00
Oliver 90e5169283
Bump software version to 1.2.2 (#11360) 2026-02-18 10:51:42 +11:00
github-actions[bot] 198594d7d3
Prevent migration check if performing DB update operations (#11356) (#11358)
* Prevent migration check if performing DB update operations

* Prevent settings write during data imports

* Prevent exchange rate update during migration processes

* Allow exchange rate updates during tests or shell session

(cherry picked from commit 951586a698)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-18 00:27:32 +11:00
github-actions[bot] dededb0aa1
refactor(backend): switch release pipeline to new pkgr version (#11336) (#11355)
* refactor(backend): switch release pipeline to new pkgr version

* fix wrong assign

* fix warnings

* also publish to release artifacts

(cherry picked from commit 1cc1f059c8)

Co-authored-by: Matthias Mair <code@mjmair.com>
2026-02-17 21:48:48 +11:00
github-actions[bot] eb413bcc57
disable traceid gen in unsecure context (#11349) (#11350)
(cherry picked from commit b8cf2db68e)

Co-authored-by: Matthias Mair <code@mjmair.com>
2026-02-17 10:47:32 +11:00
Oliver 9b11cd294e
Bump software version to 1.2.1 (#11313)
* Bump software version to 1.2.1

* Fix style issues

* [CI] Allow more test time

* Disable performance testing

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
2026-02-16 12:46:45 +11:00
github-actions[bot] ccf5b39bf1
[bug] FIx for data migration (#11329) (#11330)
* [bug] FIx for data migration

Prevent writing of default currency to database during migration or backup

* Ignore error

(cherry picked from commit 7a646946c3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-15 22:44:21 +11:00
github-actions[bot] eceaa68c32
Tweak docs for database migration (#11327) (#11328)
(cherry picked from commit 867f5a6cd5)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-15 10:50:34 +11:00
github-actions[bot] ccb4ea248b
Update base image in Dockerfile (#11309) (#11322)
* Bump version number to 1.2.0 (#11299)

* Bump version number to 1.2.0

* Update CHANGELOG.md

* Update base image in Dockerfile

Updated base image to the latest version as of 2026-02-12.

* Fix for devcontainer build (#11311) (#11312)

- Install yarn a different way

(cherry picked from commit 2a613809ec)



---------



(cherry picked from commit 723a4be24a)

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-14 12:55:47 +11:00
github-actions[bot] 9b3b68c8d8
Fix for devcontainer build (#11311) (#11312)
- Install yarn a different way

(cherry picked from commit 2a613809ec)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2026-02-13 11:17:52 +11:00
Oliver 3752b610c8
Bump version number to 1.2.0 (#11299)
* Bump version number to 1.2.0

* Update CHANGELOG.md
2026-02-12 19:53:08 +11:00
55 changed files with 539 additions and 198 deletions

View File

@ -37,9 +37,10 @@ RUN apt install -y nodejs npm
# Update to the latest stable node version
RUN npm install -g n --ignore-scripts && n lts
# Enable yarn
RUN corepack enable && corepack prepare yarn@stable --activate
# Install yarn
RUN npm install -g yarn
# Ensure node and yarn are available at the command line
RUN node --version && yarn --version
RUN yarn config set httpTimeout 600000 --home

View File

@ -23,6 +23,8 @@ env:
INVENTREE_SITE_URL: http://localhost:8000
INVENTREE_DEBUG: true
use_performance: false
permissions:
contents: read
@ -78,7 +80,7 @@ jobs:
- name: Which runner to use?
id: runner-perf
# decide if we are running in inventree/inventree -> use codspeed-macro runner else ubuntu-24.04
run: echo "runner=$([[ '${{ github.repository }}' == 'inventree/InvenTree' ]] && echo 'codspeed-macro' || echo 'ubuntu-24.04')" >> $GITHUB_OUTPUT
run: echo "runner=$([[ '${{ github.repository }}' == 'inventree/InvenTree' ]] && '${{ env.use_performance }}' == 'true' ]] && echo 'codspeed-macro' || echo 'ubuntu-24.04')" >> $GITHUB_OUTPUT
pre-commit:
name: Style [pre-commit]
@ -692,7 +694,13 @@ jobs:
cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests
id: tests
run: cd src/frontend && npx nyc playwright test
run: |
cd src/frontend
cp ./tests/fixtures/playwright_custom_logo.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_logo.png
cp ./tests/fixtures/playwright_custom_splash.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_splash.png
invoke static
env INVENTREE_CUSTOM_SPLASH="img/playwright_custom_splash.png" INVENTREE_CUSTOM_LOGO="img/playwright_custom_logo.png" npx nyc playwright test --project=customization
npx nyc playwright test --project=chromium --project=firefox
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with:

View File

@ -85,6 +85,11 @@ jobs:
asset_name: frontend-build.zip
tag: ${{ github.ref }}
overwrite: true
- name: Upload frontend to artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
with:
name: frontend-build
path: src/backend/InvenTree/web/static/frontend-build.zip
- name: Upload Attestation
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
with:
@ -136,3 +141,98 @@ jobs:
asset_name: docs-html.zip
tag: ${{ github.ref }}
overwrite: true
build-pkgr:
if: github.repository == 'inventree/InvenTree'
name: ${{ matrix.target }}
runs-on: ubuntu-latest
needs: [build, docs]
strategy:
fail-fast: false
matrix:
target:
- ubuntu:22.04
- ubuntu:24.04
- debian:12
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Get frontend artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # pin@v7.0.0
with:
name: frontend-build
- name: Setup
id: setup
env:
NODE_ID: ${{ github.event.release.node_id }}
BRANCH: ${{ github.event.release.target_commitish }}
TARGET: ${{github.event.release.target_commitish}}
REPO: ${{ github.repository }}
run: |
# Get info
gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/$REPO/commits/$TARGET > commit.json
# Extract info
echo "INFO extract | Extracting info from github"
DATE=$(jq -r '.commit.committer.date' commit.json)
SIGNATURE=$(jq -r '.commit.verification.signature' commit.json)
FULL_SHA=$(jq -r '.sha' commit.json)
SHA=$(echo "$FULL_SHA" | cut -c1-7)
echo "INFO write | Write VERSION information"
echo "$VERSION" > VERSION
echo "INVENTREE_COMMIT_HASH='$SHA'" >> VERSION
echo "INVENTREE_COMMIT_SHA='$FULL_SHA'" >> VERSION
echo "INVENTREE_COMMIT_DATE='$DATE'" >> VERSION
echo "INVENTREE_PKG_INSTALLER='PKG'" >> VERSION
echo "INVENTREE_PKG_BRANCH='$BRANCH'" >> VERSION
echo "INVENTREE_PKG_TARGET='$TARGET'" >> VERSION
echo "NODE_ID='$NODE_ID'" >> VERSION
echo "SIGNATURE='$SIGNATURE'" >> VERSION
echo "INFO write | Written VERSION information"
echo "### VERSION ###"
cat VERSION
echo "### VERSION ###"
# Move frontend build into place
mkdir -p src/backend/InvenTree/web/static
unzip -qq frontend-build.zip -d src/backend/InvenTree/web/static/web
- name: Package
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
id: package
with:
target: ${{ matrix.target }}
version: ${{ steps.setup.outputs.version }}
debug: true
cache_prefix: ${{ github.ref_name }}
env: |
INVENTREE_DB_ENGINE=sqlite3
INVENTREE_DB_NAME=database.sqlite3
INVENTREE_PLUGINS_ENABLED=true
INVENTREE_MEDIA_ROOT=/opt/inventree/media
INVENTREE_STATIC_ROOT=/opt/inventree/static
INVENTREE_BACKUP_DIR=/opt/inventree/backup
INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
APP_REPO=inventree/InvenTree
- name: Publish to go.packager.io
uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
with:
target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
repository: inventree/inventree
channel: ${{ github.ref_name }}
file: ${{ steps.package.outputs.package_path }}
- name: Publish to artifact
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.package.outputs.package_path }}
asset_name: ${{ matrix.target }}-{{ steps.setup.outputs.version }}.tar.gz
tag: ${{ github.ref }}
overwrite: true

View File

@ -6,7 +6,7 @@ buildpack: https://github.com/matmair/null-buildpack#master
env:
- INVENTREE_DB_ENGINE=sqlite3
- INVENTREE_DB_NAME=database.sqlite3
- INVENTREE_PLUGINS_ENABLED
- INVENTREE_PLUGINS_ENABLED=true
- INVENTREE_MEDIA_ROOT=/opt/inventree/media
- INVENTREE_STATIC_ROOT=/opt/inventree/static
- INVENTREE_BACKUP_DIR=/opt/inventree/backup
@ -20,6 +20,7 @@ before:
- contrib/packager.io/before.sh
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"

View File

@ -18,7 +18,7 @@ repos:
exclude: mkdocs.yml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
rev: v0.15.1
hooks:
- id: ruff-format
args: [--preview]
@ -29,7 +29,7 @@ repos:
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.22
rev: 0.10.2
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
@ -79,7 +79,7 @@ repos:
src/frontend/vite.config.ts |
)$
- repo: https://github.com/biomejs/pre-commit
rev: v2.3.10
rev: v2.3.15
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.4"]

View File

@ -1,15 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file (starting with 1.0.0).
All major notable changes to this project will be documented in this file (starting with 1.0.0).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased - YYYY-MM-DD
## 1.2.0 - 2026-02-12
### Breaking Changes
- [#10699](https://github.com/inventree/InvenTree/pull/10699) removes the `PartParameter` and `PartParameterTempalate` models (and associated API endpoints). These have been replaced with generic `Parameter` and `ParameterTemplate` models (and API endpoints). Any external client applications which made use of the old endpoints will need to be updated.
- [#10699](https://github.com/inventree/InvenTree/pull/10699) removes the `PartParameter` and `PartParameterTemplate` models (and associated API endpoints). These have been replaced with generic `Parameter` and `ParameterTemplate` models (and API endpoints). Any external client applications which made use of the old endpoints will need to be updated.
- [#11035](https://github.com/inventree/InvenTree/pull/11035) moves to a single endpoint for all metadata operations. The previous endpoints for PartMetadata, SupplierPartMetadata, etc have been removed. Any external client applications which made use of the old endpoints will need to be updated.
### Added
@ -19,10 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Adds parameter support for multiple new model types in [#10699](https://github.com/inventree/InvenTree/pull/10699)
- Allows report generator to produce PDF input controls in [#10969](https://github.com/inventree/InvenTree/pull/10969)
- UI overhaul of parameter management in [#10699](https://github.com/inventree/InvenTree/pull/10699)
- Allow input controls within generated PDF reports in [#10969](https://github.com/inventree/InvenTree/pull/10969)
### Changed
-
- Improved stocktake functionality in [#11257](https://github.com/inventree/InvenTree/pull/11257)
### Removed
- Removed python 3.9 / 3.10 support as part of Django 5.2 upgrade in [#10730](https://github.com/inventree/InvenTree/pull/10730)

View File

@ -9,7 +9,8 @@
# - Runs InvenTree web server under django development server
# - Monitors source files for any changes, and live-reloads server
FROM python:3.11-slim-trixie@sha256:1d6131b5d479888b43200645e03a78443c7157efbdb730e6b48129740727c312 AS inventree_base
# Base image last bumped 2026-02-12
FROM python:3.11-slim-trixie@sha256:0b23cfb7425d065008b778022a17b1551c82f8b4866ee5a7a200084b7e2eafbf AS inventree_base
# Build arguments for this image
ARG commit_tag=""

View File

@ -401,14 +401,10 @@ It is also possible to use alternative storage backends for static and media fil
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_S3_ACCESS_KEY | storage.s3.access_key | Access key | *Not specified* |
| INVENTREE_S3_SECRET_KEY | storage.s3.secret_key | Secret key |
| *Not specified* |
| INVENTREE_S3_BUCKET_NAME | storage.s3.bucket_name | Bucket name, required by most providers |
| *Not specified* |
| INVENTREE_S3_REGION_NAME | storage.s3.region_name | S3 region name |
| *Not specified* |
| INVENTREE_S3_ENDPOINT_URL | storage.s3.endpoint_url | Custom S3 endpoint URL, defaults to AWS endpoints if not set |
| *Not specified* |
| INVENTREE_S3_SECRET_KEY | storage.s3.secret_key | Secret key | *Not specified* |
| INVENTREE_S3_BUCKET_NAME | storage.s3.bucket_name | Bucket name, required by most providers | *Not specified* |
| INVENTREE_S3_REGION_NAME | storage.s3.region_name | S3 region name | *Not specified* |
| INVENTREE_S3_ENDPOINT_URL | storage.s3.endpoint_url | Custom S3 endpoint URL, defaults to AWS endpoints if not set | *Not specified* |
| INVENTREE_S3_LOCATION | storage.s3.location | Sub-Location that should be used | inventree-server |
| INVENTREE_S3_DEFAULT_ACL | storage.s3.default_acl | Default ACL for uploaded files, defaults to provider default if not set | *Not specified* |
| INVENTREE_S3_VERIFY_SSL | storage.s3.verify_ssl | Verify SSL certificate for S3 endpoint | True |

View File

@ -117,7 +117,7 @@ Once the migration process completes, the database records are now updated! Rest
## Migrating Between Incompatible Database Versions
There may be occasions when InvenTree data needs to be migrated between two database installations running *incompatible* versions of the database software. For example, InvenTree may be running on a Postgres database running on version 12, and the administrator wishes to migrate the data to a Postgres version 17 database.
There may be occasions when InvenTree data needs to be migrated between two database installations running *incompatible* versions of the database software. For example, InvenTree may be running on a Postgres database running on version 12, and the administrator wishes to migrate the data to a Postgres version {{ config.extra.docker_postgres_version }} database.
!!! warning "Advanced Procedure"
The following procedure is *advanced*, and should only be attempted by experienced database administrators. Always ensure that database backups are made before attempting any migration procedure.
@ -127,7 +127,7 @@ Due to inherit incompatibilities between different major versions of database so
!!! warning "InvenTree Version"
It is *crucial* that both InvenTree database installations are running the same version of InvenTree software! If this is not the case, data migration may fail, and there is a possibility that data corruption can occur. Ensure that the original database is up to date, by running `invoke update`.
The following instructions assume that the source (old) database is Postgres version 15, and the target (new) database is Postgres version 17. Additionally, it assumes that the InvenTree installation is running under [docker / docker compose](./docker.md), for simplicity. Adjust commands as required for other InvenTree configurations or database software.
The following instructions assume that the source (old) database is Postgres version 15, and the target (new) database is Postgres version {{ config.extra.docker_postgres_version }}. Additionally, it assumes that the InvenTree installation is running under [docker / docker compose](./docker.md), for simplicity. Adjust commands as required for other InvenTree configurations or database software.
The overall procedure is as follows:
@ -154,13 +154,13 @@ docker compose down
### Remove Old Database Files
The raw database files are incompatible between different major versions of Postgres. Thus, the old database files must be removed before starting the new database.
The raw database files are incompatible between different major versions of Postgres. Thus, the old database files must be removed before starting the new database. Rather than removing the database directory, we will move the database files to a temporary location, just in case we need to revert back to the old database.
!!! warning "Data Loss"
Ensure that a complete backup of the old database has been made before proceeding! Removing the database files will result in data loss if a backup does not exist.
```
rm -rf ./path/to/database/*
mv ./path/to/database ./path/to/database_backup
```
!!! info "Database Location"
@ -168,7 +168,7 @@ rm -rf ./path/to/database/*
### Start New Database
Update the InvenTree docker configuration to use the new version of Postgres (e.g. `postgres:17`), and then start the InvenTree installation:
Update the InvenTree docker configuration to use the new version of Postgres (e.g. `postgres:{{ config.extra.docker_postgres_version }}`), and then start the InvenTree installation:
```
docker compose up -d

View File

@ -122,7 +122,7 @@ InvenTree uses the [Redis](https://redis.io/) cache server to manage cache data.
!!! info "Redis on Docker"
Docker adds an additional network layer - that might lead to lower performance than bare metal.
To optimize and configure your redis deployment follow the [official docker guide](https://redis.io/docs/getting-started/install-stack/docker/#configuration).
To optimize and configure your redis deployment follow the [official docker guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/docker/).
!!! tip "Enable Cache"
While a redis container is provided in the default configuration, by default it is not enabled in the InvenTree server. You can enable redis cache support by following the [caching configuration guide](./config.md#caching)

View File

@ -1,2 +1,5 @@
# Files generated during unit testing
_testfolder/
# Playwright files for CI
InvenTree/static/img/playwright*.png

View File

@ -331,6 +331,11 @@ class InvenTreeConfig(AppConfig):
if MIGRATIONS_CHECK_DONE:
return
# Exit early if we are not in a state where we can access the database,
# otherwise we might end up in a deadlock situation
if not InvenTree.ready.canAppAccessDatabase():
return
if not InvenTree.tasks.check_for_migrations():
# Detect if this an empty database - if so, start with a fresh migration
if (

View File

@ -111,9 +111,11 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
def get_ordering(self, request, queryset, view):
"""Override ordering for supporting aliases."""
ordering = super().get_ordering(request, queryset, view)
ordering = list(super().get_ordering(request, queryset, view) or [])
aliases = getattr(view, 'ordering_field_aliases', None)
lookup_field = getattr(view, 'lookup_field', 'pk')
lookup_reversed = len(ordering) > 0 and ordering[-1].startswith('-')
# Attempt to map ordering fields based on provided aliases
if ordering is not None and aliases is not None:
@ -123,9 +125,8 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
ordering = []
for field in ordering_initial:
reverse = field.startswith('-')
if reverse:
field_reversed = field.startswith('-')
if field_reversed:
field = field[1:]
# Are aliases defined for this field?
@ -153,11 +154,22 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
continue
for a in alias:
if reverse:
if field_reversed:
a = '-' + a
ordering.append(a)
# Ensure that any API filtering appends the primary-key field
# This is to prevent "ambiguous ordering" errors across pagination boundaries
# Ref: https://github.com/inventree/InvenTree/issues/11442
if lookup_field and not any(
field in ordering for field in [lookup_field, f'-{lookup_field}']
):
if lookup_reversed:
ordering.append(f'-{lookup_field}')
else:
ordering.append(lookup_field)
return ordering
@ -220,18 +232,10 @@ class NumericInFilter(rest_filters.BaseInFilter):
return super().filter(qs, numeric_values)
SEARCH_ORDER_FILTER = [
drf_backend.DjangoFilterBackend,
InvenTreeSearchFilter,
filters.OrderingFilter,
]
ORDER_FILTER = [drf_backend.DjangoFilterBackend, InvenTreeOrderingFilter]
SEARCH_ORDER_FILTER_ALIAS = [
SEARCH_ORDER_FILTER = [
drf_backend.DjangoFilterBackend,
InvenTreeSearchFilter,
InvenTreeOrderingFilter,
]
ORDER_FILTER = [drf_backend.DjangoFilterBackend, filters.OrderingFilter]
ORDER_FILTER_ALIAS = [drf_backend.DjangoFilterBackend, InvenTreeOrderingFilter]

View File

@ -250,7 +250,6 @@ def getBlankThumbnail():
def getLogoImage(as_file=False, custom=True):
"""Return the InvenTree logo image, or a custom logo if available."""
"""Return the path to the logo-file."""
if custom and settings.CUSTOM_LOGO:
static_storage = StaticFilesStorage()

View File

@ -5,7 +5,7 @@ from django.http import Http404
from django.urls import reverse
import structlog
from rest_framework import exceptions, serializers
from rest_framework import exceptions, permissions, serializers
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
from rest_framework.request import clone_request
@ -131,10 +131,26 @@ class InvenTreeMetadata(SimpleMetadata):
# Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items():
# general model / role permission
result = check_user_permission(user, self.model, permission) or (
role_required and check_user_role(user, role_required, permission)
)
# check if simple IsAuthenticated permission class is used
if not result:
result = (
view.permission_classes
and len(view.permission_classes) == 1
and any(
perm
in [
permissions.IsAuthenticated,
InvenTree.permissions.IsAuthenticatedOrReadScope,
]
for perm in view.permission_classes
)
)
if method in actions and not result:
del actions[method]

View File

@ -26,12 +26,7 @@ from corsheaders.defaults import default_headers as default_cors_headers
import InvenTree.backup
from InvenTree.cache import get_cache_config, is_global_cache_enabled
from InvenTree.config import (
get_boolean_setting,
get_custom_file,
get_oidc_private_key,
get_setting,
)
from InvenTree.config import get_boolean_setting, get_oidc_private_key, get_setting
from InvenTree.ready import isInMainThread
from InvenTree.sentry import default_sentry_dsn, init_sentry
from InvenTree.version import checkMinPythonVersion, inventreeCommitHash
@ -1453,12 +1448,9 @@ if len(GLOBAL_SETTINGS_OVERRIDES) > 0:
logger.debug('- Override value for %s = ********', key)
# User interface customization values
CUSTOM_LOGO = get_custom_file(
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True
)
CUSTOM_SPLASH = get_custom_file(
'INVENTREE_CUSTOM_SPLASH', 'customize.splash', 'custom splash'
)
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', typecast=str)
CUSTOM_SPLASH = get_setting('INVENTREE_CUSTOM_SPLASH', 'customize.splash', typecast=str)
CUSTOMIZE = get_setting(
'INVENTREE_CUSTOMIZE', 'customize', default_value=None, typecast=dict

View File

@ -587,6 +587,15 @@ def update_exchange_rates(force: bool = False):
Arguments:
force: If True, force the update to run regardless of the last update time
"""
from InvenTree.ready import canAppAccessDatabase, isRunningMigrations
if isRunningMigrations():
return
# Do not update exchange rates if we cannot access the database
if not canAppAccessDatabase(allow_test=True, allow_shell=True):
return
try:
from djmoney.contrib.exchange.models import Rate

View File

@ -17,7 +17,7 @@ from django.conf import settings
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '1.2.0 dev'
INVENTREE_SW_VERSION = '1.2.4'
# Minimum supported Python version
MIN_PYTHON_VERSION = (3, 11)

View File

@ -27,7 +27,7 @@ from generic.states.api import StatusView
from InvenTree.api import BulkDeleteMixin, ParameterListMixin, meta_path
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
SEARCH_ORDER_FILTER_ALIAS,
SEARCH_ORDER_FILTER,
InvenTreeDateFilter,
NumberOrNullFilter,
)
@ -145,8 +145,8 @@ class BuildFilter(FilterSet):
def filter_overdue(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are overdue."""
if str2bool(value):
return queryset.filter(Build.OVERDUE_FILTER)
return queryset.exclude(Build.OVERDUE_FILTER)
return queryset.filter(Build.get_overdue_filter())
return queryset.exclude(Build.get_overdue_filter())
assigned_to_me = rest_filters.BooleanFilter(
label=_('Assigned to me'), method='filter_assigned_to_me'
@ -343,7 +343,7 @@ class BuildList(
output_options = BuildListOutputOptions
filterset_class = BuildFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'reference',
'part',
@ -594,7 +594,7 @@ class BuildLineList(
"""API endpoint for accessing a list of BuildLine objects."""
filterset_class = BuildLineFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = BuildLineOutputOptions
ordering_fields = [
'part',
@ -951,7 +951,7 @@ class BuildItemList(
output_options = BuildItemOutputOptions
filterset_class = BuildItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
def get_queryset(self):
"""Override the queryset method, to perform custom prefetch."""

View File

@ -123,11 +123,14 @@ class Build(
order_insertion_by = ['reference']
OVERDUE_FILTER = (
Q(status__in=BuildStatusGroups.ACTIVE_CODES)
& ~Q(target_date=None)
& Q(target_date__lte=InvenTree.helpers.current_date())
)
@classmethod
def get_overdue_filter(cls):
"""Filter for determining if a build order is overdue."""
return (
Q(status__in=BuildStatusGroups.ACTIVE_CODES)
& ~Q(target_date=None)
& Q(target_date__lte=InvenTree.helpers.current_date())
)
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
@ -456,7 +459,7 @@ class Build(
bool: Is the build overdue
"""
query = Build.objects.filter(pk=self.pk)
query = query.filter(Build.OVERDUE_FILTER)
query = query.filter(Build.get_overdue_filter())
return query.exists()

View File

@ -168,7 +168,8 @@ class BuildSerializer(
queryset = queryset.annotate(
overdue=Case(
When(
Build.OVERDUE_FILTER, then=Value(True, output_field=BooleanField())
Build.get_overdue_filter(),
then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField()),
)

View File

@ -48,11 +48,7 @@ from InvenTree.api import (
meta_path,
)
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.filters import (
ORDER_FILTER,
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
)
from InvenTree.filters import ORDER_FILTER, SEARCH_ORDER_FILTER
from InvenTree.helpers import inheritors, str2bool
from InvenTree.helpers_email import send_email
from InvenTree.mixins import (
@ -918,7 +914,7 @@ class ParameterList(
"""List API endpoint for Parameter objects."""
filterset_class = ParameterFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['name', 'data', 'units', 'template', 'updated', 'updated_by']

View File

@ -19,6 +19,11 @@ logger = structlog.get_logger('inventree')
def currency_code_default(create: bool = True):
"""Returns the default currency code (or USD if not specified)."""
from common.settings import get_global_setting
from InvenTree.ready import isRunningBackup, isRunningMigrations
if isRunningMigrations() or isRunningBackup():
# Prevent database writes during migration or backup operations
create = False
code = ''

View File

@ -1216,7 +1216,9 @@ class InvenTreeSetting(BaseInvenTreeSetting):
even if that key does not exist.
"""
SETTINGS: dict[str, InvenTreeSettingsKeyType]
from common.setting.system import SYSTEM_SETTINGS
SETTINGS: dict[str, InvenTreeSettingsKeyType] = SYSTEM_SETTINGS
CHECK_SETTING_KEY = True
@ -1282,9 +1284,6 @@ class InvenTreeSetting(BaseInvenTreeSetting):
The keys must be upper-case
"""
from common.setting.system import SYSTEM_SETTINGS
SETTINGS = SYSTEM_SETTINGS
typ = 'inventree'
@ -1310,6 +1309,8 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
import common.setting.user
SETTINGS = common.setting.user.USER_SETTINGS
CHECK_SETTING_KEY = True
class Meta:
@ -1321,8 +1322,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
models.UniqueConstraint(fields=['key', 'user'], name='unique key and user')
]
SETTINGS = common.setting.user.USER_SETTINGS
typ = 'user'
extra_unique_fields = ['user']

View File

@ -11,7 +11,7 @@ from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
from django.utils.translation import gettext_lazy as _
from jinja2 import Template
from jinja2.sandbox import SandboxedEnvironment
import build.validators
import common.currency
@ -52,7 +52,7 @@ def validate_part_name_format(value):
p = Part(name='test part', description='some test part')
try:
Template(value).render({'part': p})
SandboxedEnvironment().from_string(value).render({'part': p})
except Exception as exc:
raise ValidationError({'value': str(exc)})

View File

@ -44,6 +44,13 @@ def get_global_setting(key, backup_value=None, environment_key=None, **kwargs):
def set_global_setting(key, value, change_user=None, create=True, **kwargs):
"""Set the value of a global setting using the provided key."""
from common.models import InvenTreeSetting
from InvenTree.ready import canAppAccessDatabase
if not canAppAccessDatabase(allow_shell=True, allow_test=True):
logger.warning(
f'Cannot set global setting "{key}" - database is not accessible'
)
return False
kwargs['change_user'] = change_user
kwargs['create'] = create

View File

@ -11,7 +11,7 @@ import part.models
from data_exporter.mixins import DataExportViewMixin
from InvenTree.api import ListCreateDestroyAPIView, ParameterListMixin, meta_path
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import (
ListCreateAPI,
OutputOptionsMixin,
@ -197,7 +197,7 @@ class ManufacturerPartList(
"""
filterset_class = ManufacturerPartFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = ManufacturerOutputOptions
ordering_fields = ['part', 'IPN', 'MPN', 'manufacturer']
@ -358,7 +358,7 @@ class SupplierPartList(
"""
filterset_class = SupplierPartFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SupplierPartOutputOptions
ordering_fields = [
@ -472,7 +472,7 @@ class SupplierPriceBreakList(
output_options = SupplierPriceBreakOutputOptions
filterset_class = SupplierPriceBreakFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
search_fields = ['part__SKU', 'part__supplier__name']

View File

@ -35,11 +35,7 @@ from InvenTree.api import (
meta_path,
)
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
)
from InvenTree.filters import SEARCH_ORDER_FILTER, InvenTreeDateFilter
from InvenTree.helpers import str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import (
@ -391,7 +387,7 @@ class PurchaseOrderList(
"""
filterset_class = PurchaseOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = PurchaseOrderOutputOptions
ordering_field_aliases = {
@ -700,7 +696,7 @@ class PurchaseOrderLineItemList(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_field_aliases = {
'MPN': 'part__manufacturer_part__MPN',
@ -859,7 +855,7 @@ class SalesOrderList(
"""
filterset_class = SalesOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SalesOrderOutputOptions
ordering_field_aliases = {
@ -1043,7 +1039,7 @@ class SalesOrderLineItemList(
filterset_class = SalesOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SalesOrderLineItemOutputOptions
@ -1289,7 +1285,7 @@ class SalesOrderAllocationList(
"""API endpoint for listing SalesOrderAllocation objects."""
filterset_class = SalesOrderAllocationFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SalesOrderAllocationOutputOptions
ordering_fields = [
@ -1399,7 +1395,7 @@ class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI):
"""API list endpoint for SalesOrderShipment model."""
filterset_class = SalesOrderShipmentFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['reference', 'delivery_date', 'shipment_date', 'allocated_items']
search_fields = [
@ -1528,7 +1524,7 @@ class ReturnOrderList(
"""API endpoint for accessing a list of ReturnOrder objects."""
filterset_class = ReturnOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = ReturnOrderOutputOptions
@ -1674,7 +1670,7 @@ class ReturnOrderLineItemList(
filterset_class = ReturnOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = ReturnOrderLineItemOutputOptions

View File

@ -1863,12 +1863,14 @@ class PurchaseOrderLineItem(OrderLineItem):
verbose_name = _('Purchase Order Line Item')
# Filter for determining if a particular PurchaseOrderLineItem is overdue
OVERDUE_FILTER = (
Q(received__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@classmethod
def get_overdue_filter(cls):
"""Filter for determining if a particular PurchaseOrderLineItem is overdue."""
return (
Q(received__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@staticmethod
def get_api_url() -> str:
@ -2067,12 +2069,14 @@ class SalesOrderLineItem(OrderLineItem):
verbose_name = _('Sales Order Line Item')
# Filter for determining if a particular SalesOrderLineItem is overdue
OVERDUE_FILTER = (
Q(shipped__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@classmethod
def get_overdue_filter(cls):
"""Filter for determining if a particular SalesOrderLineItem is overdue."""
return (
Q(shipped__lt=F('quantity'))
& ~Q(target_date=None)
& Q(target_date__lt=InvenTree.helpers.current_date())
)
@staticmethod
def get_api_url():

View File

@ -577,7 +577,7 @@ class PurchaseOrderLineItemSerializer(
queryset = queryset.annotate(
overdue=Case(
When(
order.models.PurchaseOrderLineItem.OVERDUE_FILTER,
order.models.PurchaseOrderLineItem.get_overdue_filter(),
then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField()),
@ -1154,7 +1154,7 @@ class SalesOrderLineItemSerializer(
overdue=Case(
When(
Q(order__status__in=SalesOrderStatusGroups.OPEN)
& order.models.SalesOrderLineItem.OVERDUE_FILTER,
& order.models.SalesOrderLineItem.get_overdue_filter(),
then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField()),

View File

@ -24,9 +24,7 @@ from InvenTree.api import (
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
ORDER_FILTER,
ORDER_FILTER_ALIAS,
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
InvenTreeSearchFilter,
NumberOrNullFilter,
@ -302,7 +300,7 @@ class CategoryTree(ListAPI):
queryset = PartCategory.objects.all()
serializer_class = part_serializers.CategoryTree
filter_backends = ORDER_FILTER_ALIAS
filter_backends = ORDER_FILTER
ordering_fields = ['level', 'name', 'subcategories']
@ -1080,7 +1078,7 @@ class PartList(
filterset_class = PartFilter
is_create = True
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = [
'name',
@ -1442,7 +1440,7 @@ class BomList(
output_options = BomOutputOptions
filterset_class = BomFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
search_fields = [
'reference',

View File

@ -473,12 +473,19 @@ class PluginsRegistry:
# Ensure that each loaded plugin has a valid configuration object in the database
for plugin in self.plugins.values():
config = self.get_plugin_config(plugin.slug)
if config := self.get_plugin_config(plugin.slug):
# Ensure mandatory plugins are marked as active
if config.is_mandatory() and not config.active:
config.active = True
# Ensure mandatory plugins are marked as active
if config.is_mandatory() and not config.active:
config.active = True
config.save(no_reload=True)
try:
config.save(no_reload=True)
except (OperationalError, ProgrammingError):
# Database is not ready, cannot save config
logger.warning(
"Database not ready - cannot set mandatory flag for plugin '%s'",
plugin.slug,
)
except Exception as e:
logger.exception('Unexpected error during plugin reload: %s', e)

View File

@ -68,7 +68,7 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
if isinstance(instance, part.models.BomItem):
if self.get_setting('BOM_ITEM_INTEGER'):
if float(instance.quantity) != int(instance.quantity):
if float(instance.quantity) != int(instance.quantity): # noqa: RUF069
self.raise_error({
'quantity': 'Bom item quantity must be an integer'
})

View File

@ -69,8 +69,11 @@ class SampleSupplierTest(InvenTreeAPITestCase):
self.assertEqual(len(res.data), 15)
self.assertEqual(res.data[0]['sku'], 'BOLT-Steel-M5-5')
def test_import_part(self):
"""Test importing a part by supplier."""
def _disabled_test_import_part(self):
"""Test importing a part by supplier.
Note: This test is disabled for the 1.2.x branch, as a fix has not been back-ported for the broken test
"""
# Activate plugin
plugin = registry.get_plugin('samplesupplier', active=None)
config = plugin.plugin_config()

View File

@ -7,6 +7,7 @@ from django.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady, ValidationError
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db import transaction
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
import structlog
@ -160,9 +161,10 @@ class ReportConfig(AppConfig):
# Otherwise, create a new entry
try:
# Create a new entry
report.models.LabelTemplate.objects.create(
**template, template=self.file_from_template('label', filename)
)
with transaction.atomic():
report.models.LabelTemplate.objects.create(
**template, template=self.file_from_template('label', filename)
)
logger.info("Creating new label template: '%s'", template['name'])
except ValidationError:
logger.warning(

View File

@ -37,9 +37,8 @@ from InvenTree.api import (
)
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import (
ORDER_FILTER_ALIAS,
ORDER_FILTER,
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
NumberOrNullFilter,
)
@ -455,7 +454,7 @@ class StockLocationTree(ListAPI):
queryset = StockLocation.objects.all()
serializer_class = StockSerializers.LocationTreeSerializer
filter_backends = ORDER_FILTER_ALIAS
filter_backends = ORDER_FILTER
ordering_fields = ['level', 'name', 'sublocations']
@ -686,8 +685,8 @@ class StockFilter(FilterSet):
return queryset
if str2bool(value):
return queryset.filter(StockItem.EXPIRED_FILTER)
return queryset.exclude(StockItem.EXPIRED_FILTER)
return queryset.filter(StockItem.get_expired_filter())
return queryset.exclude(StockItem.get_expired_filter())
external = rest_filters.BooleanFilter(
label=_('External Location'), method='filter_external'
@ -1260,7 +1259,7 @@ class StockList(
headers=self.get_success_headers(serializer.data),
)
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
ordering_field_aliases = {
'part': 'part__name',

View File

@ -4,8 +4,7 @@ from inspect import signature
from typing import Optional
from django.core.exceptions import ValidationError
from jinja2 import Template
from django.template import Context, Template
import common.models
import InvenTree.exceptions
@ -76,7 +75,7 @@ def generate_batch_code(**kwargs):
'STOCK_BATCH_CODE_TEMPLATE', ''
)
return Template(batch_template).render(context)
return Template(batch_template).render(Context(context))
def generate_serial_number(part=None, quantity=1, **kwargs) -> Optional[str]:

View File

@ -10,24 +10,40 @@ def add_part_links(apps, schema_editor):
history_entries = []
for tracking in StockItemTracking.objects.all():
N = StockItemTracking.objects.count()
if N > 0:
print(f"\nUpdating {N} StockItemTracking entries with part links...")
for tracking in StockItemTracking.objects.filter(part__isnull=True).select_related('item__part'):
item = tracking.item
# No item link - skip
if item is None:
continue
part = item.part
# No part link - skip
if part is None:
continue
# Already linked to the correct part - skip
if tracking.part == part:
continue
tracking.part = part
history_entries.append(tracking)
# Process in batches to avoid issues with very large datasets
if len(history_entries) >= 100:
StockItemTracking.objects.bulk_update(history_entries, ['part'])
history_entries = []
print(".", end='', flush=True)
if len(history_entries) > 0:
StockItemTracking.objects.bulk_update(history_entries, ['part'])
print(f"\nUpdated {len(history_entries)} StockItemTracking entries with part links")
def remove_null_items(apps, schema_editor):

View File

@ -518,12 +518,14 @@ class StockItem(
status__in=StockStatusGroups.AVAILABLE_CODES,
)
# A query filter which can be used to filter StockItem objects which have expired
EXPIRED_FILTER = (
IN_STOCK_FILTER
& ~Q(expiry_date=None)
& Q(expiry_date__lt=InvenTree.helpers.current_date())
)
@classmethod
def get_expired_filter(cls):
"""A query filter which can be used to filter StockItem objects which have expired."""
return (
cls.IN_STOCK_FILTER
& ~Q(expiry_date=None)
& Q(expiry_date__lt=InvenTree.helpers.current_date())
)
@classmethod
def _create_serial_numbers(cls, serials: list, **kwargs) -> QuerySet:

View File

@ -516,7 +516,7 @@ class StockItemSerializer(
queryset = queryset.annotate(
expired=Case(
When(
StockItem.EXPIRED_FILTER,
StockItem.get_expired_filter(),
then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField()),

View File

@ -575,6 +575,81 @@ class StockItemListTest(StockAPITestCase):
for ordering in ['part', 'location', 'stock', 'status', 'IPN', 'MPN', 'SKU']:
self.run_ordering_test(self.list_url, ordering)
def test_pagination(self):
"""Test that pagination boundaries are observed correctly.
Ref: https://github.com/inventree/InvenTree/issues/11442
"""
location = StockLocation.objects.first()
part = Part.objects.first()
items = []
# Delete all existing stock item objects
for item in StockItem.objects.all():
item.delete()
for idx in range(1000):
items.append(
StockItem(
part=part,
location=location,
quantity=idx % 10,
level=0,
lft=0,
rght=0,
tree_id=0,
)
)
if len(items) >= 100:
StockItem.objects.bulk_create(items)
items = []
self.assertEqual(StockItem.objects.count(), 1000)
url = reverse('api-stock-list')
# Keep track of the unique PKs we have seen in the results
unique_pks = set()
for idx in range(0, 100, 10):
data = self.get(url, {'limit': 10, 'offset': idx}).data
self.assertEqual(data['count'], 1000)
self.assertEqual(len(data['results']), 10)
for item in data['results']:
self.assertNotIn(
item['pk'],
unique_pks,
f'Duplicate PK {item["pk"]} found in paginated results @ page {idx // 10}',
)
unique_pks.add(item['pk'])
self.assertEqual(
len(unique_pks), 100, 'Expected to see 100 unique PKs in paginated results'
)
# Run same test again, with reverse ordering on part IPN
unique_pks = set()
for idx in range(0, 100, 10):
data = self.get(url, {'limit': 10, 'offset': idx, 'ordering': '-IPN'}).data
self.assertEqual(data['count'], 1000)
self.assertEqual(len(data['results']), 10)
for item in data['results']:
self.assertNotIn(
item['pk'],
unique_pks,
f'Duplicate PK {item["pk"]} found in paginated results @ page {idx // 10} with reverse ordering',
)
unique_pks.add(item['pk'])
self.assertEqual(
len(unique_pks), 100, 'Expected to see 100 unique PKs in paginated results'
)
def test_top_level_filtering(self):
"""Test filtering against "top level" stock location."""
# No filters, should return *all* items
@ -962,7 +1037,9 @@ class StockItemListTest(StockAPITestCase):
# Note: While the export is quick on pgsql, it is still quite slow on sqlite3
with self.export_data(
self.list_url, max_query_count=50, max_query_time=9.0
self.list_url,
max_query_count=50,
max_query_time=12.0, # Test time increased due to worker variability
) as data_file:
data = self.process_csv(data_file)

View File

@ -367,49 +367,44 @@ class GetAuthToken(GenericAPIView):
- Existing tokens are *never* exposed again via the API
- Once the token is provided, it can be used for auth until it expires
"""
if request.user.is_authenticated:
user = request.user
name = request.query_params.get('name', '')
if not request.user.is_authenticated:
raise exceptions.NotAuthenticated() # pragma: no cover
name = ApiToken.sanitize_name(name)
user = request.user
name = request.query_params.get('name', '')
today = datetime.date.today()
name = ApiToken.sanitize_name(name)
# Find existing token, which has not expired
token = ApiToken.objects.filter(
user=user, name=name, revoked=False, expiry__gte=today
).first()
today = datetime.date.today()
if not token:
# User is authenticated, and requesting a token against the provided name.
token = ApiToken.objects.create(user=request.user, name=name)
# Find existing token, which has not expired
token = ApiToken.objects.filter(
user=user, name=name, revoked=False, expiry__gte=today
).first()
logger.info(
"Created new API token for user '%s' (name='%s')",
user.username,
name,
)
if not token:
# User is authenticated, and requesting a token against the provided name.
token = ApiToken.objects.create(user=request.user, name=name)
logger.info(
"Created new API token for user '%s' (name='%s')", user.username, name
)
# Add some metadata about the request
token.set_metadata('user_agent', request.headers.get('user-agent', ''))
token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', ''))
token.set_metadata('remote_host', request.META.get('REMOTE_HOST', ''))
token.set_metadata('remote_user', request.META.get('REMOTE_USER', ''))
token.set_metadata('server_name', request.META.get('SERVER_NAME', ''))
token.set_metadata('server_port', request.META.get('SERVER_PORT', ''))
token.set_metadata('user_agent', request.headers.get('user-agent', ''))
token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', ''))
token.set_metadata('remote_host', request.META.get('REMOTE_HOST', ''))
token.set_metadata('remote_user', request.META.get('REMOTE_USER', ''))
token.set_metadata('server_name', request.META.get('SERVER_NAME', ''))
token.set_metadata('server_port', request.META.get('SERVER_PORT', ''))
data = {'token': token.key, 'name': token.name, 'expiry': token.expiry}
data = {'token': token.key, 'name': token.name, 'expiry': token.expiry}
# Ensure that the users session is logged in
if not get_user(request).is_authenticated:
login(
request, user, backend='django.contrib.auth.backends.ModelBackend'
)
# Ensure that the users session is logged in
if not get_user(request).is_authenticated:
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
return Response(data)
else:
raise exceptions.NotAuthenticated() # pragma: no cover
return Response(data)
class TokenMixin:

View File

@ -1659,9 +1659,9 @@ pynacl==1.6.2 \
# via
# -c src/backend/requirements.txt
# paramiko
pypdf==6.6.2 \
--hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \
--hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba
pypdf==6.7.5 \
--hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \
--hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in

View File

@ -1467,9 +1467,9 @@ pynacl==1.6.2 \
--hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \
--hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9
# via paramiko
pypdf==6.6.2 \
--hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \
--hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba
pypdf==6.7.5 \
--hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \
--hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d
# via -r src/backend/requirements.in
pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \

View File

@ -48,13 +48,22 @@ export default defineConfig({
name: 'chromium',
use: {
...devices['Desktop Chrome']
}
},
testIgnore: /customization/ // Ignore all tests in the "customization" folder for this project
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox']
}
},
testIgnore: /customization/ // Ignore all tests in the "customization" folder for this project
},
{
name: 'customization',
use: {
...devices['Desktop Firefox']
},
testIgnore: /pui_.*\.spec\.ts/ // Ignore all "pui_*.spec.ts" tests for this project
}
],

View File

@ -36,6 +36,11 @@ export const queryClient = new QueryClient({
}
});
export function setTraceId() {
// check if we are in a secure context (https) - if not use of crypto is not allowed
if (!window.isSecureContext) {
return '';
}
const runID = crypto.randomUUID().replace(/-/g, '');
const traceid = `00-${runID}-${frontendID}-01`;
api.defaults.headers['traceparent'] = traceid;

View File

@ -1,8 +1,10 @@
import { t } from '@lingui/core/macro';
import { ActionIcon } from '@mantine/core';
import { forwardRef } from 'react';
import { type ReactNode, forwardRef } from 'react';
import { NavLink } from 'react-router-dom';
import { useShallow } from 'zustand/react/shallow';
import { useServerApiState } from '../../states/ServerApiState';
import InvenTreeIcon from './inventree.svg';
export const InvenTreeLogoHomeButton = forwardRef<HTMLDivElement>(
@ -19,6 +21,21 @@ export const InvenTreeLogoHomeButton = forwardRef<HTMLDivElement>(
}
);
export const InvenTreeLogo = () => {
/*
* Render the InvenTree logo
* - Uses the custom logo if one is defined on the server
* - Otherwise, uses the default logo
*/
export function InvenTreeLogo(): ReactNode {
const [server] = useServerApiState(
useShallow((state) => [state.server, state.fetchServerApiState])
);
if (server.server && server.customize?.logo) {
return (
<img src={server.customize.logo} alt={t`InvenTree Logo`} height={28} />
);
}
return <img src={InvenTreeIcon} alt={t`InvenTree Logo`} height={28} />;
};
}

View File

@ -114,9 +114,10 @@ export type StockColumnProps = TableColumnProps & {
// Render a StockItem instance within a table
export function StockColumn(props: StockColumnProps): TableColumn {
return {
accessor: props.accessor ?? 'stock_item',
title: t`Stock Item`,
...props,
ordering: props.ordering || 'stock',
accessor: props.accessor || 'stock',
render: (record: any) => {
const stock_item =
resolveItem(record, props.accessor ?? 'stock_item_detail') ?? {};

View File

@ -486,6 +486,10 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableState.setPage(1);
setSortStatus(status);
if (!status.columnAccessor) {
console.error('Invalid column accessor provided for table sorting');
}
setTableSorting(cacheKey)(status);
},
[cacheKey]

View File

@ -533,6 +533,7 @@ export default function BuildOutputTable({
title: t`Complete`,
tooltip: t`Complete build output`,
color: 'green',
hidden: !production,
icon: <InvenTreeIcon icon='success' />,
onClick: () => {
setSelectedOutputs([record]);
@ -568,7 +569,7 @@ export default function BuildOutputTable({
}
];
},
[buildStatus, user, partId, hasTrackedItems]
[buildStatus, build.status, user, partId, hasTrackedItems]
);
const tableColumns: TableColumn[] = useMemo(() => {

View File

@ -26,7 +26,8 @@ export function ApiTokenTable({
const [opened, { open, close }] = useDisclosure(false);
const generateToken = useCreateApiFormModal({
url: ApiEndpoints.user_tokens,
url: ApiEndpoints.user_token,
method: 'GET',
title: t`Generate Token`,
fields: { name: {} },
successMessage: t`Token generated`,
@ -178,6 +179,7 @@ export function ApiTokenTable({
onClose={close}
title={<StylishText size='xl'>{t`Token`}</StylishText>}
centered
data-testid='generated-api-token'
>
<Text c='dimmed'>
<Trans>

View File

@ -0,0 +1,33 @@
import test, { expect } from '@playwright/test';
import { navigate } from '../helpers';
import { doLogin } from '../login';
/**
* Tests for user interface customization functionality.
*
* Note: The correct environment variables must be set for these tests to work correctly. See "playwright.config.ts" for details.
* These tests are designed to run in CI environments where specific environment variables are set to enable custom logos and splash screens. The tests verify that these customizations are correctly applied in the user interface.
*/
test('Customization - Splash', async ({ page }) => {
await navigate(page, '/');
await page.waitForLoadState('networkidle');
// Check for the custom splash screen
await expect(
page.locator('[style*="playwright_custom_splash.png"]')
).toBeVisible();
});
test('Customization - Logo', async ({ page }) => {
await doLogin(page, {
username: 'noaccess',
password: 'youshallnotpass'
});
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2500);
return;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -543,3 +543,27 @@ async function testColorPicker(page, ref: string) {
await page.mouse.click(box.x + box.width / 2, box.y + box.height + 25);
await page.getByText('Color Mode').click();
}
test('Settings - Auth - Tokens', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits',
url: 'settings/user/'
});
await page.getByRole('tab', { name: 'Security' }).click();
await page.getByRole('button', { name: 'Access Tokens' }).click();
await page
.getByRole('button', { name: 'action-button-generate-token' })
.click();
await page
.getByRole('textbox', { name: 'text-field-name' })
.fill('testtoken');
await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByText('Tokens are only shown once').waitFor();
await page
.getByTestId('generated-api-token')
.locator('.mantine-CloseButton-root')
.click();
await page.getByRole('cell', { name: 'testtoken' }).waitFor();
});