Merge commit '2c8ef258941d1da2e5d8e0310c4f465c5fa72fe9' into notify-context

This commit is contained in:
Oliver Walters 2026-03-10 11:03:39 +00:00
commit c258390de3
112 changed files with 2134 additions and 792 deletions

View File

@ -206,11 +206,11 @@ jobs:
images: |
inventree/inventree
ghcr.io/${{ github.repository }}
- uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # pin@v1
- uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # pin@v1
- name: Push Docker Images
id: push-docker
if: github.event_name != 'pull_request'
uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # pin@v1
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # pin@v1
with:
project: jczzbjkk68
context: .

View File

@ -23,8 +23,6 @@ env:
INVENTREE_SITE_URL: http://localhost:8000
INVENTREE_DEBUG: true
use_performance: false
permissions:
contents: read
@ -42,6 +40,8 @@ jobs:
cicd: ${{ steps.filter.outputs.cicd }}
requirements: ${{ steps.filter.outputs.requirements }}
runner-perf: ${{ steps.runner-perf.outputs.runner }}
performance: ${{ steps.performance.outputs.force-performance }}
submit-performance: ${{ steps.runner-perf.outputs.submit-performance }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
@ -77,10 +77,29 @@ jobs:
if: |
contains(github.event.pull_request.labels.*.name, 'dependency') ||
contains(github.event.pull_request.labels.*.name, 'full-run')
- name: Is performance testing being forced?
run: echo "force-performance=true" >> $GITHUB_OUTPUT
id: performance
if: |
contains(github.event.pull_request.labels.*.name, 'performance-run')
- name: Which runner to use?
env:
GITHUB_REF: ${{ github.ref }}
PERFORMANCE: ${{ steps.performance.outputs.force-performance }}
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' && '${{ env.use_performance }}' == 'true' ]] && echo 'codspeed-macro' || echo 'ubuntu-24.04')" >> $GITHUB_OUTPUT
run: |
is_main_push=false
if [[ '${{ github.event_name }}' == 'push' && "$GITHUB_REF" == 'refs/heads/master' ]]; then
is_main_push=true
fi
if [[ '${{ github.repository }}' == 'inventree/InvenTree' && ( "$is_main_push" == 'true' || "$PERFORMANCE" == 'true' ) ]]; then
echo "runner=codspeed-macro" >> "$GITHUB_OUTPUT"
echo "submit-performance=true" >> "$GITHUB_OUTPUT"
else
echo "runner=ubuntu-24.04" >> "$GITHUB_OUTPUT"
echo "submit-performance=false" >> "$GITHUB_OUTPUT"
fi
pre-commit:
name: Style [pre-commit]
@ -183,7 +202,7 @@ jobs:
- name: Export API Documentation
run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
- name: Upload schema
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: schema.yml
path: src/backend/InvenTree/schema.yml
@ -232,17 +251,17 @@ jobs:
- name: Extract settings / tags
run: invoke int.export-definitions --basedir docs
- name: Upload settings
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: inventree_settings.json
path: docs/generated/inventree_settings.json
- name: Upload tags
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: inventree_tags.yml
path: docs/generated/inventree_tags.yml
- name: Upload filters
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: inventree_filters.yml
path: docs/generated/inventree_filters.yml
@ -265,7 +284,7 @@ jobs:
- name: Create artifact directory
run: mkdir -p artifact
- name: Download schema artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # pin@v7.0.0
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0
with:
path: artifact
merge-multiple: true
@ -292,7 +311,7 @@ jobs:
runs-on: ${{ needs.paths-filter.outputs.runner-perf }}
needs: ["pre-commit", "paths-filter"]
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true' || needs.paths-filter.outputs.performance == 'true'
permissions:
contents: read
id-token: write
@ -341,10 +360,11 @@ jobs:
pip uninstall pytest-django -y
cd ${WRAPPER_NAME}
pip install .
if: needs.paths-filter.outputs.submit-performance == 'true'
- name: Performance Reporting
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # pin@v4
# check if we are in inventree/inventree - reporting only works in that OIDC context
if: github.repository == 'inventree/InvenTree'
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
with:
mode: walltime
run: pytest ./src/performance --codspeed
@ -387,7 +407,7 @@ jobs:
- name: Coverage Tests
run: invoke dev.test --check --coverage --translations
- name: Upload raw coverage to artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: coverage
path: .coverage
@ -406,7 +426,7 @@ jobs:
needs: ["pre-commit", "paths-filter"]
# check if we are in inventree/inventree - reporting only works in that OIDC context
if: (needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true') && github.repository == 'inventree/InvenTree'
if: (needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true') && github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
permissions:
contents: read
id-token: write
@ -701,7 +721,7 @@ jobs:
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
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with:
name: playwright-report
@ -746,7 +766,7 @@ jobs:
run: |
cd src/backend/InvenTree/web/static
zip -r frontend-build.zip web/ web/.vite
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.0.0
with:
name: frontend-build
path: src/backend/InvenTree/web/static/web

View File

@ -55,7 +55,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@0b82b0b1a22399a1c542d4d656f70cd903571b5c # pin@v0
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
@ -73,12 +73,12 @@ jobs:
zip -r ../frontend-build.zip * .vite
- name: Attest Build Provenance
id: attest
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # pin@v1
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # pin@v1
with:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
- name: Upload frontend
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/backend/InvenTree/web/static/frontend-build.zip
@ -86,12 +86,12 @@ jobs:
tag: ${{ github.ref }}
overwrite: true
- name: Upload frontend to artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # pin@v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # pin@v7.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
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: frontend-build.intoto.jsonl
@ -134,7 +134,7 @@ jobs:
cd docs/site
zip -r docs-html.zip *
- name: Publish documentation
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: docs/site/docs-html.zip
@ -163,7 +163,7 @@ jobs:
persist-credentials: false
- name: Get frontend artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # pin@v7.0.0
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0
with:
name: frontend-build
- name: Setup
@ -244,7 +244,7 @@ jobs:
channel: ${{ env.pkg_channel }}
file: ${{ steps.package.outputs.package_path }}
- name: Publish to artifact
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # pin@2.11.3
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.package.outputs.package_path }}

View File

@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: SARIF file
path: results.sarif

View File

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

View File

@ -56,7 +56,7 @@ jobs:
echo "Resetting to HEAD~"
git reset HEAD~ || true
- name: crowdin action
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # pin@v2
uses: crowdin/github-action@8818ff65bfc4322384f983ea37e3926948c11745 # pin@v2
with:
upload_sources: true
upload_translations: false

View File

@ -57,7 +57,7 @@ repos:
files: docs/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, -c, src/backend/requirements.txt]
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.14, -c, src/backend/requirements.txt]
files: contrib/container/requirements\.(in|txt)$
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.36.4

View File

@ -13,11 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
[#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs.
[#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
[#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
- [#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default.
- [#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs.
- [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality.
- [#11374](https://github.com/inventree/InvenTree/pull/11374) adds `updated_at` field on purchase, sales and return orders.
### Changed

View File

@ -42,7 +42,6 @@ InvenTree/
├─ CONTRIBUTING.md # Contribution guidelines and overview
├─ Procfile # Process definition for Debian/Ubuntu packages
├─ README.md # General project information and overview
├─ runtime.txt # Python runtime settings for Debian/Ubuntu packages build
├─ SECURITY.md # Project security policy
├─ tasks.py # Action definitions for development, testing and deployment
```

View File

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

View File

@ -1,14 +1,14 @@
# This file was autogenerated by uv via the following command:
# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.11 -c src/backend/requirements.txt
# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.14 -c src/backend/requirements.txt
asgiref==3.11.1 \
--hash=sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce \
--hash=sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133
# via
# -c src/backend/requirements.txt
# django
django==5.2.11 \
--hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \
--hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0
django==5.2.12 \
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
# via
# -c src/backend/requirements.txt
# -r contrib/container/requirements.in
@ -236,7 +236,6 @@ typing-extensions==4.15.0 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# -c src/backend/requirements.txt
# psycopg
# psycopg-pool
uv==0.9.22 \
--hash=sha256:012bdc5285a9cdb091ac514b7eb8a707e3b649af5355fe4afb4920bfe1958c00 \

View File

@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt -c src/backend/requirements.txt
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# -c src/backend/requirements.txt
# requests

View File

@ -291,3 +291,11 @@ Users may opt to disable the spotlight search functionality if they do not find
Many aspects of the user interface are controlled by user permissions, which determine what actions and features are available to each user based on their assigned roles and permissions within the system. This allows for a highly customizable user experience, where different users can have access to different features and functionality based on their specific needs and responsibilities within the organization.
If a user does not have permission to access a particular feature or section of the system, that feature will be hidden from their view in the user interface. This helps to ensure that users only see the features and information that are relevant to their role, reducing clutter and improving usability.
## Language Support
The InvenTree user interface supports multiple languages, allowing users to interact with the system in their preferred language.
The default system language can be configured by the system administrator in the [server configuration options](../start/config.md#basic-options).
Additionally, users can select their preferred language in their [user settings](../settings/user.md), allowing them to override the system default language with their own choice. This provides a personalized experience for each user, ensuring that they can interact with the system in the language they are most comfortable with.

View File

@ -62,6 +62,7 @@ InvenTree roughly follow the [GitLab flow](https://about.gitlab.com/topics/versi
There are nominally 5 active branches:
- `master` - The main development branch
- `stable` - The latest stable release
- `next-breaking` - The next breaking release (e.g. 2.0, 3.0) with all deprecated features removed
- `l10n` - Translation branch: Source to Crowdin
- `l10_crowdin` - Translation branch: Source from Crowdin
- `y.y.x` - Release branch for the currently supported version (e.g. `0.5.x`)
@ -115,6 +116,23 @@ The translation process is as follows:
4. Translations made in Crowdin are automatically pushed back to the `l10_crowdin` branch by Crowdin once they are approved
5. The `l10_crowdin` branch is merged back into `master` by a maintainer periodically
### `next-breaking` Branch
Used for easier testing of plugins and integrations against the next major release. It is branched from master when a major release is cut and updated on minor release. The branch is not build into docker images or packages and not meant to be run in production.
All deprecated features (REST or python API endpoints mostly) are removed from this branch after each minor release. This allows plugin developers to test their plugins against the next major release early and identify any extensive changes before the major release is cut.
Only breaking changes are added to this branch. No new features should be added at any point to this branch, only breaking removals / changes.
Before a major release is cut (1.12.5 > 2.0.0), this branch is merged back into `master`.
During the life-time of a major release line (1.0.1, 1.1.x, 1.2.x, 1.3.x, ..., 1.12.5) all deprecation removals are collected in this branch.
On every minor release (1.11.8 > 1.12.0) the `master` is rebased onto the `next-breaking` branch.
Every time a change with depreations is merged into `master`, a follow up PR that removes the newly-introduced deprecation is created targeting the `next-breaking` branch. After the next minor is released and `master` was rebased into `next-breaking` all the PRs from the previous minor release line can be merged into the `next-breaking` branch. Deprecation removals for the - possibly - long running major release line can be collected this way without having a large number of deprecation removals PRs open.
## API versioning
The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed.

View File

@ -84,23 +84,27 @@ This is a security measure to prevent plugins from changing the core functionali
An error occurred when discovering or initializing a machine type from a plugin. This likely indicates a faulty or incompatible plugin.
#### INVE-E13
**Error reading InvenTree configuration file**
An error occurred while reading the InvenTree configuration file. This might be caused by a syntax error or invalid value in the configuration file.
#### INVE-E14
**Could not import Django**
Django is not installed in the current Python environment. This means that the InvenTree backend is not running within the correct [python virtual environment](../start/index.md#virtual-environment) or that the required Python packages were not installed correctly.
#### INVE-E15
**Python version not supported**
This error occurs attempting to run InvenTree on a version of Python which is older than the minimum required version. We [require Python {{ config.extra.min_python_version }} or newer](../start/index.md#python-requirements)
#### INVE-E16
**Restore was stopped due to critical issues with backup environment - Backend**
A potentially critical mismatch between the backup environment and the current restore environment was detected during the restore process. The restore was stopped to prevent potential data loss or corruption. Check the logs for more information about the detected issues.
While using [invoke](../start/invoke.md), this can be overridden with the `--restore-allow-newer-version` flag.
### INVE-W (InvenTree Warning)
Warnings - These are non-critical errors which should be addressed when possible.
@ -199,6 +203,14 @@ A user attempted to sign up but registration is currently disabled via the syste
To enable registration, adjust the relevant settings (for regular or SSO registration) to allow user signups.
#### INVE-W13
**Current environment inconsistent with backup environment - Backend**
The environment in which the backup was taken does not match the current environment. This might lead to issues with restoring the backup, as the backup might contain data that is not compatible with the current environment. Plugins for example might be missing or are present in a different version - this can lead to issues with restoring the backup.
This warning will not prevent you from restoring the backup but it is recommended to ensure the mentioned issues are resolved before restoring the backup to prevent issues with the restored instance.
### INVE-I (InvenTree Information)
Information — These are not errors but information messages. They might point out potential issues or just provide information.
@ -215,5 +227,9 @@ An issue was detected with the application of a filtering serializer or decorato
This warning should only be raised during development and not in production, if you recently installed a plugin you might want to contact the plugin author.
#### INVE-I3
**Backup and restore information - Backend**
Information about the metadata of a backup being restored or the environment in which a backup is being restored to. This information can be helpful to understand the backup and restore process and to identify potential issues with the backup or the restore process.
### INVE-M (InvenTree Miscellaneous)
Miscellaneous — These are information messages that might be used to mark debug information or other messages helpful for the InvenTree team to understand behaviour.

View File

@ -170,6 +170,7 @@ Configuration of label printing:
{{ globalsetting("PART_SALABLE") }}
{{ globalsetting("PART_VIRTUAL") }}
{{ globalsetting("PART_COPY_BOM") }}
{{ globalsetting("PART_BOM_ALLOW_ZERO_QUANTITY") }}
{{ globalsetting("PART_COPY_PARAMETERS") }}
{{ globalsetting("PART_COPY_TESTS") }}
{{ globalsetting("PART_CATEGORY_PARAMETERS") }}

View File

@ -29,6 +29,7 @@ The following configuration options are available for backup:
| INVENTREE_BACKUP_DATE_FORMAT | backup_date_format | Date format string used to format timestamps in backup filenames. | `%Y-%m-%d-%H%M%S` |
| INVENTREE_BACKUP_DATABASE_FILENAME_TEMPLATE | backup_database_filename_template | Template string used to generate database backup filenames. | `InvenTree-db-{datetime}.{extension}` |
| INVENTREE_BACKUP_MEDIA_FILENAME_TEMPLATE | backup_media_filename_template | Template string used to generate media backup filenames. | `InvenTree-media-{datetime}.{extension}` |
| INVENTREE_BACKUP_RESTORE_ALLOW_NEWER_VERSION | backup_restore_allow_newer_version | If True, allows restoring a backup created with a newer version of InvenTree. This is dangerous as it can lead to hard-to-debug data loss. | False |
### Storage Backend

View File

@ -28,9 +28,9 @@ bracex==2.6 \
--hash=sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952 \
--hash=sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7
# via wcmatch
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# -c src/backend/requirements.txt
# httpcore
@ -391,9 +391,9 @@ mkdocs-macros-plugin==1.5.0 \
--hash=sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f \
--hash=sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f
# via -r docs/requirements.in
mkdocs-material==9.7.1 \
--hash=sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c \
--hash=sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8
mkdocs-material==9.7.3 \
--hash=sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d \
--hash=sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
@ -562,9 +562,9 @@ requests==2.32.5 \
# mkdocs-macros-plugin
# mkdocs-material
# mkdocs-mermaid2-plugin
rich==14.3.2 \
--hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \
--hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8
rich==14.3.3 \
--hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \
--hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b
# via neoteroi-mkdocs
setuptools==82.0.0 \
--hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \

View File

@ -1,11 +1,22 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 457
INVENTREE_API_VERSION = 460
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v460 -> 2026-02-25 : https://github.com/inventree/InvenTree/pull/11374
- Adds "updated_at" field to PurchaseOrder, SalesOrder and ReturnOrder API endpoints
- Adds "updated_before" and "updated_after" date filters to all three order list endpoints
- Adds "updated_at" ordering option to all three order list endpoints
v459 -> 2026-02-23 : https://github.com/inventree/InvenTree/pull/11411
- Changed PurchaseOrderLine "auto_pricing" default value from true to false
v458 -> 2026-02-22 : https://github.com/inventree/InvenTree/pull/11401
- Switches token refresh endpoint to use POST instead of GET (upstream allauth change)
v457 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/10887
- Extend the "auto allocate" wizard API to include tracked items

View File

@ -65,10 +65,12 @@ class InvenTreeConfig(AppConfig):
self.start_background_tasks()
if not InvenTree.ready.isInTestMode(): # pragma: no cover
# Update exchange rates
InvenTree.tasks.offload_task(InvenTree.tasks.update_exchange_rates)
# Let the background worker check for migrations
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
# Update exchange rates
InvenTree.tasks.offload_task(
InvenTree.tasks.update_exchange_rates, force_async=True
)
self.update_site_url()
self.load_unit_registry()

View File

@ -5,7 +5,16 @@ We use the django-dbbackup library to handle backup and restore operations.
Ref: https://archmonger.github.io/django-dbbackup/latest/configuration/
"""
from datetime import datetime, timedelta
from django.conf import settings
import structlog
import InvenTree.config
import InvenTree.version
logger = structlog.get_logger('inventree')
def get_backup_connector_options() -> dict:
@ -131,3 +140,131 @@ def backup_media_filename_template() -> str:
default_value='InvenTree-media-{datetime}.{extension}',
typecast=str,
)
# schema for backup metadata
InvenTreeBackupMetadata = dict[str, str | int | bool | None]
def _gather_environment_metadata() -> InvenTreeBackupMetadata:
"""Gather metadata about the current environment to be stored with the backup."""
import plugin.installer
new_data: InvenTreeBackupMetadata = {}
new_data['ivt_1_debug'] = settings.DEBUG
new_data['ivt_1_version'] = InvenTree.version.inventreeVersion()
new_data['ivt_1_version_api'] = InvenTree.version.inventreeApiVersion()
new_data['ivt_1_plugins_enabled'] = settings.PLUGINS_ENABLED
new_data['ivt_1_plugins_file_hash'] = plugin.installer.plugins_file_hash()
new_data['ivt_1_installer'] = InvenTree.config.inventreeInstaller()
new_data['ivt_1_backup_time'] = datetime.now().isoformat()
return new_data
def _parse_environment_metadata(metadata: InvenTreeBackupMetadata) -> dict[str, str]:
"""Parse backup metadata to extract environment information."""
data = {}
data['debug'] = metadata.get('ivt_1_debug', False)
data['version'] = metadata.get('ivt_1_version', 'unknown')
data['version_api'] = metadata.get('ivt_1_version_api', 'unknown')
data['plugins_enabled'] = metadata.get('ivt_1_plugins_enabled', False)
data['plugins_file_hash'] = metadata.get('ivt_1_plugins_file_hash', 'unknown')
data['installer'] = metadata.get('ivt_1_installer', 'unknown')
data['backup_time'] = metadata.get('ivt_1_backup_time', 'unknown')
return data
def metadata_set(metadata) -> InvenTreeBackupMetadata:
"""Set backup metadata for the current backup operation."""
return _gather_environment_metadata()
def validate_restore(metadata: InvenTreeBackupMetadata) -> bool | None:
"""Validate whether a backup restore operation should proceed, based on the provided metadata."""
if metadata.get('ivt_1_version') is None:
logger.warning(
'INVE-W13: Backup metadata does not contain version information',
error_code='INVE-W13',
)
return True
current_environment = _parse_environment_metadata(_gather_environment_metadata())
backup_environment = _parse_environment_metadata(metadata)
# Version mismatch
if backup_environment['version'] != current_environment['version']:
logger.warning(
f'INVE-W13: Backup being restored was created with InvenTree version {backup_environment["version"]}, but current version is {current_environment["version"]}',
error_code='INVE-W13',
)
# Backup is from newer version - fail
try:
if int(backup_environment['version_api']) > int(
str(current_environment['version_api'])
):
logger.error(
'INVE-E16: Backup being restored was created with a newer version of InvenTree - restore cannot proceed. If you are using the invoke task for your restore this warning might be overridden once with `--restore-allow-newer-version`',
error_code='INVE-E16',
)
# Check for pass flag to allow restore
if not settings.BACKUP_RESTORE_ALLOW_NEWER_VERSION: # defaults to False
return False
else:
logger.warning(
'INVE-W13: Backup restore is allowing a restore from a newer version of InvenTree - this can lead to data loss or corruption',
error_code='INVE-W13',
)
except ValueError: # pragma: no cover
logger.warning(
'INVE-W13: Could not parse API version from backup metadata - cannot determine if backup is from newer version',
error_code='INVE-W13',
)
# Plugins enabled on backup but not restore environment - warn
if (
backup_environment['plugins_enabled']
and not current_environment['plugins_enabled']
):
logger.warning(
'INVE-W13: Backup being restored was created with plugins enabled, but current environment has plugins disabled - this can lead to data loss',
error_code='INVE-W13',
)
# Plugins file hash mismatch - warn
if pg_hash := backup_environment['plugins_file_hash']:
if pg_hash != current_environment['plugins_file_hash']:
logger.warning(
'INVE-W13: Backup being restored has a different plugins file hash to the current environment - this can lead to data loss or corruption',
error_code='INVE-W13',
)
# Installer mismatch - warn
if installer := backup_environment['installer']:
if installer != current_environment['installer']:
logger.warning(
f"INVE-W13: Backup being restored was created with installer '{installer}', but current environment has installer '{current_environment['installer']}'",
error_code='INVE-W13',
)
# Age of backup
last_backup_time = backup_environment.get('backup_time')
if datetime.now() - datetime.fromisoformat(last_backup_time) > timedelta(days=120):
logger.warning(
f'INVE-W13: Backup being restored is over 120 days old (last backup time: {last_backup_time})',
error_code='INVE-W13',
)
if settings.DEBUG: # pragma: no cover
logger.info(
f'INVE-I3: Backup environment: {backup_environment}', error_code='INVE-I3'
)
logger.info(
f'INVE-I3: Current environment: {current_environment}', error_code='INVE-I3'
)
return True

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

@ -27,7 +27,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_oidc_private_key, get_setting
from InvenTree.ready import isInMainThread
from InvenTree.ready import isInMainThread, isRunningBackup
from InvenTree.sentry import default_sentry_dsn, init_sentry
from InvenTree.version import checkMinPythonVersion, inventreeCommitHash
from users.oauth2_scopes import oauth2_scopes
@ -258,12 +258,22 @@ DBBACKUP_EMAIL_SUBJECT_PREFIX = InvenTree.backup.backup_email_prefix()
DBBACKUP_CONNECTORS = {'default': InvenTree.backup.get_backup_connector_options()}
DBBACKUP_BACKUP_METADATA_SETTER = InvenTree.backup.metadata_set
DBBACKUP_RESTORE_METADATA_VALIDATOR = InvenTree.backup.validate_restore
# Data storage options
DBBACKUP_STORAGE_CONFIG = {
'BACKEND': InvenTree.backup.get_backup_storage_backend(),
'OPTIONS': InvenTree.backup.get_backup_storage_options(),
}
# This can also be overridden with a command line flag --restore-allow-newer-version when running the restore command
BACKUP_RESTORE_ALLOW_NEWER_VERSION = get_boolean_setting(
'INVENTREE_BACKUP_RESTORE_ALLOW_NEWER_VERSION',
'backup_restore_allow_newer_version',
False,
)
# Enable django admin interface?
INVENTREE_ADMIN_ENABLED = get_boolean_setting(
'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True
@ -862,7 +872,7 @@ TRACING_ENABLED = get_boolean_setting(
)
TRACING_DETAILS: Optional[dict] = None
if TRACING_ENABLED: # pragma: no cover
if TRACING_ENABLED and not isRunningBackup(): # pragma: no cover
from InvenTree.tracing import setup_instruments, setup_tracing
_t_endpoint = get_setting('INVENTREE_TRACING_ENDPOINT', 'tracing.endpoint', None)

View File

@ -587,7 +587,10 @@ 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
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):

View File

@ -2,10 +2,15 @@
from pathlib import Path
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management import call_command
from django.test import TestCase
from opentelemetry.instrumentation.sqlite3 import SQLite3Instrumentor
from InvenTree.config import get_testfolder_dir
class CommandTestCase(TestCase):
"""Test case for custom management commands."""
@ -66,3 +71,144 @@ class CommandTestCase(TestCase):
output = call_command('remove_mfa', username=my_admin3.username, verbosity=0)
self.assertEqual(output, 'done')
self.assertEqual(my_admin3.authenticator_set.all().count(), 0)
def test_backup_metadata(self):
"""Test the backup metadata functions."""
from InvenTree.backup import (
_gather_environment_metadata,
_parse_environment_metadata,
)
metadata = _gather_environment_metadata()
self.assertIn('ivt_1_version', metadata)
self.assertIn('ivt_1_plugins_enabled', metadata)
parsed = _parse_environment_metadata(metadata)
self.assertIn('version', parsed)
self.assertIn('plugins_enabled', parsed)
def test_restore_validation(self):
"""Test the restore validation functions."""
from InvenTree.backup import _gather_environment_metadata, validate_restore
metadata = _gather_environment_metadata()
self.assertTrue(validate_restore(metadata))
# Version
with self.assertLogs() as cm:
self.assertTrue(validate_restore({}))
self.assertIn(
'INVE-W13: Backup metadata does not contain version information', str(cm[1])
)
with self.assertLogs() as cm:
self.assertTrue(validate_restore({**metadata, 'ivt_1_version': '123xx'}))
self.assertIn(
'INVE-W13: Backup being restored was created with InvenTree version',
str(cm[1]),
)
with self.assertLogs() as cm:
self.assertFalse(
validate_restore({
**metadata,
'ivt_1_version': '9999',
'ivt_1_version_api': '9999',
})
)
self.assertIn(
'INVE-E16: Backup being restored was created with a newer version',
str(cm[1]),
)
# not with allow flag
with self.settings(BACKUP_RESTORE_ALLOW_NEWER_VERSION=True):
with self.assertLogs() as cm:
self.assertTrue(
validate_restore({
**metadata,
'ivt_1_version': '9999',
'ivt_1_version_api': '9999',
})
)
self.assertIn(
'INVE-W13: Backup restore is allowing a restore from a newer version of InvenTree',
str(cm[1]),
)
# Plugins enabled
with self.settings(PLUGINS_ENABLED=False):
with self.assertLogs() as cm:
self.assertTrue(validate_restore(metadata))
self.assertIn(
'INVE-W13: Backup being restored was created with plugins enabled',
str(cm[1]),
)
# Plugin hash
with self.assertLogs() as cm:
self.assertTrue(
validate_restore({**metadata, 'ivt_1_plugins_file_hash': '123xx'})
)
self.assertIn(
'INVE-W13: Backup being restored has a different plugins file hash',
str(cm[1]),
)
# installer
with self.assertLogs() as cm:
self.assertTrue(validate_restore({**metadata, 'ivt_1_installer': '123xx'}))
self.assertIn(
'INVE-W13: Backup being restored was created with installer', str(cm[1])
)
# Age
with self.assertLogs() as cm:
self.assertTrue(
validate_restore({
**metadata,
'ivt_1_backup_time': '2020-02-02T00:00:00',
})
)
self.assertIn(
'INVE-W13: Backup being restored is over 120 days old', str(cm[1])
)
def test_backup_command_e2e(self):
"""Test the backup command."""
# we only test on sqlite, the environment in which we also run coverage
if settings.DB_ENGINE != 'django.db.backends.sqlite3':
self.skipTest('Backup command test only runs on sqlite database')
# disable tracing for now
if settings.TRACING_ENABLED: # pragma: no cover
print('Disabling tracing for backup command test')
SQLite3Instrumentor().uninstrument()
output_path = get_testfolder_dir().joinpath('backup.zip').resolve()
# Backup
with self.assertLogs() as cm:
output = call_command(
'dbbackup', noinput=True, verbosity=2, output_path=str(output_path)
)
self.assertIsNone(output)
self.assertIn(f'Writing metadata file to {output_path}', str(cm[1]))
# Restore
with self.assertLogs() as cm:
output = call_command(
'dbrestore',
noinput=True,
interactive=False,
verbosity=2,
input_path=str(output_path),
)
self.assertIsNone(output)
self.assertIn('Using connector from metadata', str(cm[1]))
# Cleanup the generated backup file and metadata file
output_path.unlink()
Path(str(output_path) + '.metadata').unlink()
if settings.TRACING_ENABLED: # pragma: no cover
print('Re-enabling tracing for backup command test')
SQLite3Instrumentor().instrument()

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,
)
@ -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

@ -49,11 +49,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 (
@ -1079,7 +1075,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

@ -637,6 +637,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
},
'PART_BOM_ALLOW_ZERO_QUANTITY': {
'name': _('Allow BOM Zero Quantity'),
'description': _(
'Accept a zero quantity for BOM item for part. Enables using setup quantity to define a quantity required per build, independent of build quantity'
),
'default': False,
'validator': bool,
},
'LABEL_ENABLE': {
'name': _('Enable label printing'),
'description': _('Enable label printing from the web interface'),

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']
@ -360,7 +360,7 @@ class SupplierPartList(
"""
filterset_class = SupplierPartFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SupplierPartOutputOptions
ordering_fields = [
@ -475,7 +475,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

@ -552,6 +552,12 @@ class DataImportRow(models.Model):
self.valid = self.validate()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Update the session progress when a row is deleted."""
session = self.session
super().delete(*args, **kwargs)
session.check_complete()
session = models.ForeignKey(
DataImportSession,
on_delete=models.CASCADE,

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 (
@ -228,6 +224,14 @@ class OrderFilter(FilterSet):
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
)
updated_before = InvenTreeDateFilter(
label=_('Updated Before'), field_name='updated_at', lookup_expr='lt'
)
updated_after = InvenTreeDateFilter(
label=_('Updated After'), field_name='updated_at', lookup_expr='gt'
)
min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
def filter_min_date(self, queryset, name, value):
@ -391,7 +395,7 @@ class PurchaseOrderList(
"""
filterset_class = PurchaseOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = PurchaseOrderOutputOptions
ordering_field_aliases = {
@ -420,6 +424,7 @@ class PurchaseOrderList(
'responsible',
'total_price',
'project_code',
'updated_at',
]
ordering = '-reference'
@ -700,7 +705,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 +864,7 @@ class SalesOrderList(
"""
filterset_class = SalesOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SalesOrderOutputOptions
ordering_field_aliases = {
@ -882,6 +887,7 @@ class SalesOrderList(
'shipment_date',
'total_price',
'project_code',
'updated_at',
]
search_fields = [
@ -1043,7 +1049,7 @@ class SalesOrderLineItemList(
filterset_class = SalesOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = SalesOrderLineItemOutputOptions
@ -1289,7 +1295,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 +1405,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 +1534,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
@ -1549,6 +1555,7 @@ class ReturnOrderList(
'target_date',
'complete_date',
'project_code',
'updated_at',
]
search_fields = [
@ -1674,7 +1681,7 @@ class ReturnOrderLineItemList(
filterset_class = ReturnOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
filter_backends = SEARCH_ORDER_FILTER
output_options = ReturnOrderLineItemOutputOptions

View File

@ -0,0 +1,43 @@
# Generated by Django 5.2.11 on 2026-02-19 22:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0114_purchaseorderextraline_project_code_and_more"),
]
operations = [
migrations.AddField(
model_name="purchaseorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
migrations.AddField(
model_name="returnorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
migrations.AddField(
model_name="salesorder",
name="updated_at",
field=models.DateTimeField(
blank=True,
null=True,
help_text="Timestamp of last update",
verbose_name="Updated At",
),
),
]

View File

@ -9,7 +9,7 @@ from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import F, Q, QuerySet, Sum
from django.db.models.functions import Coalesce
from django.db.models.signals import post_save
from django.db.models.signals import post_delete, post_save
from django.dispatch.dispatcher import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@ -331,6 +331,8 @@ class Order(
if not self.creation_date:
self.creation_date = InvenTree.helpers.current_date()
self.updated_at = InvenTree.helpers.current_time()
super().save(*args, **kwargs)
def check_locked(self, db: bool = False) -> bool:
@ -498,6 +500,13 @@ class Order(
help_text=_('Date order was issued'),
)
updated_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_('Updated At'),
help_text=_('Timestamp of last update'),
)
responsible = models.ForeignKey(
UserModels.Owner,
on_delete=models.SET_NULL,
@ -3073,3 +3082,43 @@ class ReturnOrderExtraLine(OrderExtraLine):
verbose_name=_('Order'),
help_text=_('Return Order'),
)
def _touch_order_updated_at(instance):
"""Bump updated_at on the parent order without triggering a full save."""
if not InvenTree.ready.canAppAccessDatabase(allow_test=True):
return
instance.order.__class__.objects.filter(pk=instance.order_id).update(
updated_at=InvenTree.helpers.current_time()
)
@receiver(post_save, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_save')
@receiver(
post_delete, sender=PurchaseOrderLineItem, dispatch_uid='po_lineitem_post_delete'
)
@receiver(
post_save, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_save'
)
@receiver(
post_delete, sender=PurchaseOrderExtraLine, dispatch_uid='po_extraline_post_delete'
)
@receiver(post_save, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_save')
@receiver(
post_delete, sender=SalesOrderLineItem, dispatch_uid='so_lineitem_post_delete'
)
@receiver(post_save, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_save')
@receiver(
post_delete, sender=SalesOrderExtraLine, dispatch_uid='so_extraline_post_delete'
)
@receiver(post_save, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_save')
@receiver(
post_delete, sender=ReturnOrderLineItem, dispatch_uid='ro_lineitem_post_delete'
)
@receiver(post_save, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_save')
@receiver(
post_delete, sender=ReturnOrderExtraLine, dispatch_uid='ro_extraline_post_delete'
)
def update_order_on_lineitem_change(sender, instance, **kwargs):
"""Update parent order updated_at when any line item is saved or deleted."""
_touch_order_updated_at(instance)

View File

@ -373,8 +373,14 @@ class PurchaseOrderSerializer(
'total_price',
'order_currency',
'destination',
'updated_at',
])
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
read_only_fields = [
'issue_date',
'complete_date',
'creation_date',
'updated_at',
]
extra_kwargs = {
'supplier': {'required': True},
'order_currency': {'required': False},
@ -639,7 +645,7 @@ class PurchaseOrderLineItemSerializer(
help_text=_(
'Automatically calculate purchase price based on supplier part data'
),
default=True,
default=False,
)
destination_detail = enable_filter(
@ -1026,8 +1032,9 @@ class SalesOrderSerializer(
'shipments_count',
'completed_shipments_count',
'allocated_lines',
'updated_at',
])
read_only_fields = ['status', 'creation_date', 'shipment_date']
read_only_fields = ['status', 'creation_date', 'shipment_date', 'updated_at']
extra_kwargs = {'order_currency': {'required': False}}
def skip_create_fields(self):
@ -1918,8 +1925,9 @@ class ReturnOrderSerializer(
'customer_reference',
'order_currency',
'total_price',
'updated_at',
])
read_only_fields = ['creation_date']
read_only_fields = ['creation_date', 'updated_at']
def skip_create_fields(self):
"""Skip these fields when instantiating a new object."""

View File

@ -25,7 +25,17 @@ from part.models import Part
from stock.models import StockItem, StockLocation
from users.models import Owner
from .models import PurchaseOrder, PurchaseOrderExtraLine, PurchaseOrderLineItem
from .models import (
PurchaseOrder,
PurchaseOrderExtraLine,
PurchaseOrderLineItem,
ReturnOrder,
ReturnOrderExtraLine,
ReturnOrderLineItem,
SalesOrder,
SalesOrderExtraLine,
SalesOrderLineItem,
)
class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
@ -369,7 +379,8 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
order=po,
part=sp_1,
quantity=3,
purchase_price=Money(1000, 'USD'), # "Unit price" should be $100USD
# "Unit price" should be $100USD
purchase_price=Money(1000, 'USD'),
)
# 13 x 0.1 = 1.3
@ -569,3 +580,151 @@ class OrderTest(ExchangeRateMixin, PluginRegistryMixin, TestCase):
p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4)
class OrderUpdatedAtTest(TestCase):
"""Tests to verify that the updated_at field is correctly maintained on all order types."""
def setUp(self):
"""Set up objects for all three order types."""
self.supplier = Company.objects.filter(is_supplier=True).first()
self.customer = Company.objects.filter(is_customer=True).first()
self.po = PurchaseOrder.objects.create(
reference='PO-TEST-001', supplier=self.supplier
)
self.so = SalesOrder.objects.create(
reference='SO-TEST-001', customer=self.customer
)
self.ro = ReturnOrder.objects.create(
reference='RO-TEST-001', customer=self.customer
)
self.part = Part.objects.create(name='Test Part', description='Test Part')
self.stock_item = StockItem.objects.create(part=self.part, quantity=10)
def _refresh(self, instance):
"""Return a fresh copy of the instance from the database."""
return instance.__class__.objects.get(pk=instance.pk)
def test_updated_at_set_on_save(self):
"""updated_at should be populated after the order is saved."""
for instance in [self.po, self.so, self.ro]:
self.assertIsNotNone(self._refresh(instance).updated_at)
def test_updated_at_changes_on_save(self):
"""updated_at should advance when the order is saved again."""
for instance in [self.po, self.so, self.ro]:
original = self._refresh(instance).updated_at
instance.description = 'Updated description'
instance.save()
refreshed = self._refresh(instance)
self.assertGreaterEqual(refreshed.updated_at, original)
def test_updated_at_on_extra_line_add(self):
"""updated_at should advance on the parent order when an extra line is added."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
before = self._refresh(instance).updated_at
ExtraLine.objects.create(order=instance, quantity=1)
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_extra_line_update(self):
"""updated_at should advance on the parent order when an extra line is updated."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
line = ExtraLine.objects.create(order=instance, quantity=1)
before = self._refresh(instance).updated_at
line.quantity = 5
line.save()
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_extra_line_delete(self):
"""updated_at should advance on the parent order when an extra line is deleted."""
for instance, ExtraLine in [
(self.po, PurchaseOrderExtraLine),
(self.so, SalesOrderExtraLine),
(self.ro, ReturnOrderExtraLine),
]:
line = ExtraLine.objects.create(order=instance, quantity=1)
before = self._refresh(instance).updated_at
line.delete()
after = self._refresh(instance).updated_at
self.assertGreaterEqual(after, before)
def test_updated_at_on_line_item_add(self):
"""updated_at should advance on the parent order when a regular line item is added."""
before_po = self._refresh(self.po).updated_at
PurchaseOrderLineItem.objects.create(order=self.po, part=None, quantity=1)
self.assertGreaterEqual(self._refresh(self.po).updated_at, before_po)
before_so = self._refresh(self.so).updated_at
SalesOrderLineItem.objects.create(order=self.so, part=None, quantity=1)
self.assertGreaterEqual(self._refresh(self.so).updated_at, before_so)
before_ro = self._refresh(self.ro).updated_at
ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
self.assertGreaterEqual(self._refresh(self.ro).updated_at, before_ro)
def test_updated_at_on_line_item_update(self):
"""updated_at should advance on the parent order when a regular line item is updated."""
po_line = PurchaseOrderLineItem.objects.create(
order=self.po, part=None, quantity=1
)
so_line = SalesOrderLineItem.objects.create(
order=self.so, part=None, quantity=1
)
ro_line = ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
for instance, line in [
(self.po, po_line),
(self.so, so_line),
(self.ro, ro_line),
]:
before = self._refresh(instance).updated_at
line.quantity = 5
line.save()
self.assertGreaterEqual(self._refresh(instance).updated_at, before)
def test_updated_at_on_line_item_delete(self):
"""updated_at should advance on the parent order when a regular line item is deleted."""
po_line = PurchaseOrderLineItem.objects.create(
order=self.po, part=None, quantity=1
)
so_line = SalesOrderLineItem.objects.create(
order=self.so, part=None, quantity=1
)
ro_line = ReturnOrderLineItem.objects.create(
order=self.ro, item=self.stock_item, quantity=1
)
for instance, line in [
(self.po, po_line),
(self.so, so_line),
(self.ro, ro_line),
]:
before = self._refresh(instance).updated_at
line.delete()
self.assertGreaterEqual(self._refresh(instance).updated_at, before)

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

@ -22,6 +22,7 @@ from sql_util.utils import SubqueryCount
import common.currency
import common.filters
import common.models
import common.serializers
import company.models
import InvenTree.helpers
@ -1656,8 +1657,20 @@ class BomItemSerializer(
def validate_quantity(self, quantity):
"""Perform validation for the BomItem quantity field."""
if quantity <= 0:
raise serializers.ValidationError(_('Quantity must be greater than zero'))
allow_zero_qty = common.models.InvenTreeSetting.get_setting(
'PART_BOM_ALLOW_ZERO_QUANTITY', False
)
if allow_zero_qty:
if quantity < 0:
raise serializers.ValidationError(
_('Quantity must be greater than or equal to zero')
)
else:
if quantity <= 0:
raise serializers.ValidationError(
_('Quantity must be greater than zero')
)
return quantity

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']
@ -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

@ -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

View File

@ -101,16 +101,16 @@ blessed==1.30.0 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
boto3==1.42.54 \
--hash=sha256:71194e855bfc81a21872cbe29c41f52ffdbe67e0a184a52c13346ef00b328939 \
--hash=sha256:fe3d8ec586c39a0c96327fd317c77ca601ec5f991e9ba7211cacae8db4c07a73
boto3==1.42.58 \
--hash=sha256:1bc5ff0b7a1a3f42b115481e269e1aada1d68bbfa80a989ac2882d51072907a3 \
--hash=sha256:3a21b5bbc8bf8d6472a7ae7bdc77819b1f86f35d127f428f4603bed1b98122c0
# via
# -c src/backend/requirements.txt
# django-anymail
# django-storages
botocore==1.42.54 \
--hash=sha256:853a0822de66d060aeebafa07ca13a03799f7958313d1b29f8dc7e2e1be8f527 \
--hash=sha256:ab203d4e57d22913c8386a695d048e003b7508a8a4a7a46c9ddf4ebd67a20b69
botocore==1.42.58 \
--hash=sha256:3098178f4404cf85c8997ebb7948b3f267cff1dd191b08fc4ebb614ac1013a20 \
--hash=sha256:55224d6a91afae0997e8bee62d1ef1ae2dcbc6c210516939b32a774b0b35bec5
# via
# -c src/backend/requirements.txt
# boto3
@ -219,9 +219,9 @@ brotli==1.2.0 \
# via
# -c src/backend/requirements.txt
# fonttools
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# -c src/backend/requirements.txt
# requests
@ -504,9 +504,9 @@ defusedxml==0.7.1 \
# via
# -c src/backend/requirements.txt
# python3-openid
django==5.2.11 \
--hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \
--hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0
django==5.2.12 \
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
@ -538,9 +538,9 @@ django==5.2.11 \
# djangorestframework
# djangorestframework-simplejwt
# drf-spectacular
django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \
--hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \
--hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15
django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \
--hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \
--hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
@ -877,68 +877,68 @@ googleapis-common-protos==1.72.0 \
# -c src/backend/requirements.txt
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
grpcio==1.78.1 \
--hash=sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85 \
--hash=sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca \
--hash=sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862 \
--hash=sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b \
--hash=sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f \
--hash=sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97 \
--hash=sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0 \
--hash=sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd \
--hash=sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338 \
--hash=sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72 \
--hash=sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907 \
--hash=sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba \
--hash=sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd \
--hash=sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966 \
--hash=sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22 \
--hash=sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11 \
--hash=sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f \
--hash=sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b \
--hash=sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce \
--hash=sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2 \
--hash=sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b \
--hash=sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70 \
--hash=sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529 \
--hash=sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9 \
--hash=sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb \
--hash=sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7 \
--hash=sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512 \
--hash=sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460 \
--hash=sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0 \
--hash=sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22 \
--hash=sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74 \
--hash=sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb \
--hash=sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494 \
--hash=sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04 \
--hash=sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f \
--hash=sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42 \
--hash=sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1 \
--hash=sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79 \
--hash=sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541 \
--hash=sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67 \
--hash=sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba \
--hash=sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465 \
--hash=sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f \
--hash=sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d \
--hash=sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496 \
--hash=sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e \
--hash=sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89 \
--hash=sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0 \
--hash=sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068 \
--hash=sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd \
--hash=sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f \
--hash=sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4 \
--hash=sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a \
--hash=sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1 \
--hash=sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d \
--hash=sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747 \
--hash=sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668 \
--hash=sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb \
--hash=sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20 \
--hash=sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1 \
--hash=sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28
grpcio==1.78.0 \
--hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \
--hash=sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d \
--hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \
--hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \
--hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \
--hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \
--hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \
--hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \
--hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \
--hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \
--hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \
--hash=sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec \
--hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \
--hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \
--hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \
--hash=sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb \
--hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \
--hash=sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df \
--hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \
--hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \
--hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \
--hash=sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf \
--hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \
--hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \
--hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \
--hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \
--hash=sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5 \
--hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \
--hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \
--hash=sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1 \
--hash=sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5 \
--hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \
--hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \
--hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \
--hash=sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf \
--hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \
--hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \
--hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \
--hash=sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c \
--hash=sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597 \
--hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \
--hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \
--hash=sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813 \
--hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \
--hash=sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2 \
--hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \
--hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \
--hash=sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a \
--hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \
--hash=sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5 \
--hash=sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a \
--hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \
--hash=sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c \
--hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \
--hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \
--hash=sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525 \
--hash=sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de \
--hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \
--hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \
--hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce \
--hash=sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
@ -949,9 +949,9 @@ gunicorn==25.1.0 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
icalendar==7.0.1 \
--hash=sha256:1f577af3e4d022a701aa98f9366dd524f0f1134b90c73c28493f06d5371901d5 \
--hash=sha256:68a6295f31ed907885ae79f10336b53ba067e0a66075b6a956abbe5e89ac7b39
icalendar==7.0.2 \
--hash=sha256:ad31a5825b39522a30b073c6ced3ffcdf6c02cbb7dab69ba2e4de32ddbf77cc9 \
--hash=sha256:de844ff5cde32f539bea7644e36d8494032a926b933bedb92621f2f239760806
# via
# -c src/backend/requirements.txt
# django-ical
@ -1669,9 +1669,9 @@ pynacl==1.6.2 \
# via
# -c src/backend/requirements.txt
# paramiko
pypdf==6.7.2 \
--hash=sha256:331b63cd66f63138f152a700565b3e0cebdf4ec8bec3b7594b2522418782f1f3 \
--hash=sha256:82a1a48de500ceea59a52a7d979f5095927ef802e4e4fac25ab862a73468acbb
pypdf==6.7.5 \
--hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \
--hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
@ -1899,9 +1899,9 @@ rapidfuzz==3.14.3 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
redis==7.2.0 \
--hash=sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497 \
--hash=sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26
redis==7.2.1 \
--hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \
--hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5
# via
# -c src/backend/requirements.txt
# django-redis
@ -2164,9 +2164,9 @@ webencodings==0.5.1 \
# cssselect2
# tinycss2
# tinyhtml5
whitenoise==6.11.0 \
--hash=sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f \
--hash=sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258
whitenoise==6.12.0 \
--hash=sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad \
--hash=sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in

View File

@ -11,9 +11,9 @@ build==1.4.0 \
--hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \
--hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936
# via pip-tools
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements.txt
@ -401,9 +401,9 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
django==5.2.11 \
--hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \
--hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0
django==5.2.12 \
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements.txt
@ -437,10 +437,12 @@ django-types==0.23.0 \
--hash=sha256:0727b13ae810c4b1f14eeac9872834ac928c99dc76584ea7c23afc4461e049dd \
--hash=sha256:f97fb746166fb15a5f40e470a1fd7a58226349aac9e0a9cb8ae81deb14d94fd0
# via -r src/backend/requirements-dev.in
filelock==3.21.2 \
--hash=sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708 \
--hash=sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560
# via virtualenv
filelock==3.24.3 \
--hash=sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa \
--hash=sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d
# via
# python-discovery
# virtualenv
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
@ -460,9 +462,9 @@ iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
# via pytest
isort==7.0.0 \
--hash=sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 \
--hash=sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187
isort==8.0.0 \
--hash=sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9 \
--hash=sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c
# via -r src/backend/requirements-dev.in
markdown-it-py==4.0.0 \
--hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \
@ -503,6 +505,7 @@ platformdirs==4.9.2 \
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements.txt
# python-discovery
# virtualenv
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
@ -555,10 +558,14 @@ pytest-codspeed==4.3.0 \
--hash=sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5 \
--hash=sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4
# via -r src/backend/requirements-dev.in
pytest-django==4.11.1 \
--hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \
--hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991
pytest-django==4.12.0 \
--hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \
--hash=sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758
# via -r src/backend/requirements-dev.in
python-discovery==1.1.0 \
--hash=sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268 \
--hash=sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b
# via virtualenv
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
@ -648,9 +655,9 @@ requests-mock==1.12.1 \
--hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \
--hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401
# via -r src/backend/requirements-dev.in
rich==14.3.2 \
--hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \
--hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8
rich==14.3.3 \
--hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \
--hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b
# via pytest-codspeed
setuptools==82.0.0 \
--hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \
@ -688,9 +695,9 @@ ty==0.0.1a21 \
--hash=sha256:e941e9a9d1e54b03eeaf9c3197c26a19cf76009fd5e41e16e5657c1c827bd6d3 \
--hash=sha256:ecf41706b803827b0de8717f32a434dad1e67be9f4b8caf403e12013179ea06a
# via -r src/backend/requirements-dev.in
types-psycopg2==2.9.21.20251012 \
--hash=sha256:4cdafd38927da0cfde49804f39ab85afd9c6e9c492800e42f1f0c1a1b0312935 \
--hash=sha256:712bad5c423fe979e357edbf40a07ca40ef775d74043de72bd4544ca328cc57e
types-psycopg2==2.9.21.20260223 \
--hash=sha256:78ed70de2e56bc6b5c26c8c1da8e9af54e49fdc3c94d1504609f3519e2b84f02 \
--hash=sha256:c6228ade72d813b0624f4c03feeb89471950ac27cd0506b5debed6f053086bc8
# via django-types
types-pyyaml==6.0.12.20250915 \
--hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \
@ -712,9 +719,9 @@ urllib3==2.6.3 \
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements.txt
# requests
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==21.1.0 \
--hash=sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07 \
--hash=sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44
# via pre-commit
wheel==0.46.3 \
--hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \

View File

@ -10,9 +10,9 @@ build==1.4.0 \
--hash=sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 \
--hash=sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936
# via pip-tools
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# -c src/backend/requirements.txt
# requests
@ -231,99 +231,113 @@ click==8.3.1 \
--hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \
--hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6
# via pip-tools
coverage[toml]==7.13.3 \
--hash=sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1 \
--hash=sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67 \
--hash=sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395 \
--hash=sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d \
--hash=sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14 \
--hash=sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8 \
--hash=sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f \
--hash=sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0 \
--hash=sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c \
--hash=sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b \
--hash=sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051 \
--hash=sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8 \
--hash=sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea \
--hash=sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f \
--hash=sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba \
--hash=sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3 \
--hash=sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627 \
--hash=sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10 \
--hash=sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508 \
--hash=sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75 \
--hash=sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56 \
--hash=sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3 \
--hash=sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a \
--hash=sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4 \
--hash=sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab \
--hash=sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8 \
--hash=sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4 \
--hash=sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f \
--hash=sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be \
--hash=sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024 \
--hash=sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73 \
--hash=sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43 \
--hash=sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c \
--hash=sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c \
--hash=sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107 \
--hash=sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e \
--hash=sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0 \
--hash=sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895 \
--hash=sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43 \
--hash=sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba \
--hash=sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e \
--hash=sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2 \
--hash=sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f \
--hash=sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4 \
--hash=sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25 \
--hash=sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6 \
--hash=sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23 \
--hash=sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7 \
--hash=sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6 \
--hash=sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910 \
--hash=sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155 \
--hash=sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b \
--hash=sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9 \
--hash=sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef \
--hash=sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39 \
--hash=sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0 \
--hash=sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587 \
--hash=sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00 \
--hash=sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b \
--hash=sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3 \
--hash=sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e \
--hash=sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a \
--hash=sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34 \
--hash=sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a \
--hash=sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b \
--hash=sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8 \
--hash=sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222 \
--hash=sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b \
--hash=sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3 \
--hash=sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9 \
--hash=sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0 \
--hash=sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301 \
--hash=sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e \
--hash=sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f \
--hash=sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd \
--hash=sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8 \
--hash=sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96 \
--hash=sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a \
--hash=sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f \
--hash=sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc \
--hash=sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca \
--hash=sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595 \
--hash=sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba \
--hash=sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e \
--hash=sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a \
--hash=sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1 \
--hash=sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac \
--hash=sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86 \
--hash=sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad \
--hash=sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4 \
--hash=sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0 \
--hash=sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb
coverage[toml]==7.13.4 \
--hash=sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246 \
--hash=sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459 \
--hash=sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129 \
--hash=sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6 \
--hash=sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415 \
--hash=sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf \
--hash=sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80 \
--hash=sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11 \
--hash=sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0 \
--hash=sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b \
--hash=sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9 \
--hash=sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b \
--hash=sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f \
--hash=sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505 \
--hash=sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47 \
--hash=sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55 \
--hash=sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def \
--hash=sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689 \
--hash=sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012 \
--hash=sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5 \
--hash=sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3 \
--hash=sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95 \
--hash=sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9 \
--hash=sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601 \
--hash=sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997 \
--hash=sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c \
--hash=sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac \
--hash=sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c \
--hash=sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa \
--hash=sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750 \
--hash=sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3 \
--hash=sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d \
--hash=sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12 \
--hash=sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a \
--hash=sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932 \
--hash=sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356 \
--hash=sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92 \
--hash=sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148 \
--hash=sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39 \
--hash=sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634 \
--hash=sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6 \
--hash=sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72 \
--hash=sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98 \
--hash=sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef \
--hash=sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3 \
--hash=sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9 \
--hash=sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0 \
--hash=sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a \
--hash=sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9 \
--hash=sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552 \
--hash=sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc \
--hash=sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f \
--hash=sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525 \
--hash=sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940 \
--hash=sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a \
--hash=sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23 \
--hash=sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f \
--hash=sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc \
--hash=sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b \
--hash=sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056 \
--hash=sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7 \
--hash=sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb \
--hash=sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a \
--hash=sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd \
--hash=sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea \
--hash=sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126 \
--hash=sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299 \
--hash=sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9 \
--hash=sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b \
--hash=sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00 \
--hash=sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf \
--hash=sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda \
--hash=sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2 \
--hash=sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5 \
--hash=sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d \
--hash=sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9 \
--hash=sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9 \
--hash=sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b \
--hash=sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa \
--hash=sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092 \
--hash=sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58 \
--hash=sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea \
--hash=sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26 \
--hash=sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea \
--hash=sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9 \
--hash=sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053 \
--hash=sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f \
--hash=sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0 \
--hash=sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3 \
--hash=sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256 \
--hash=sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a \
--hash=sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903 \
--hash=sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91 \
--hash=sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd \
--hash=sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505 \
--hash=sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7 \
--hash=sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0 \
--hash=sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2 \
--hash=sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a \
--hash=sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71 \
--hash=sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985 \
--hash=sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242 \
--hash=sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d \
--hash=sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af \
--hash=sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c \
--hash=sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0
# via -r src/backend/requirements-dev.in
cryptography==46.0.5 \
--hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \
@ -382,9 +396,9 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
django==5.2.11 \
--hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \
--hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0
django==5.2.12 \
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
# via
# -c src/backend/requirements.txt
# django-silk
@ -417,9 +431,9 @@ django-types==0.23.0 \
--hash=sha256:0727b13ae810c4b1f14eeac9872834ac928c99dc76584ea7c23afc4461e049dd \
--hash=sha256:f97fb746166fb15a5f40e470a1fd7a58226349aac9e0a9cb8ae81deb14d94fd0
# via -r src/backend/requirements-dev.in
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
filelock==3.24.3 \
--hash=sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa \
--hash=sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d
# via virtualenv
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
@ -439,9 +453,9 @@ iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
# via pytest
isort==7.0.0 \
--hash=sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 \
--hash=sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187
isort==8.0.0 \
--hash=sha256:184916a933041c7cf718787f7e52064f3c06272aff69a5cb4dc46497bd8911d9 \
--hash=sha256:fddea59202f231e170e52e71e3510b99c373b6e571b55d9c7b31b679c0fed47c
# via -r src/backend/requirements-dev.in
markdown-it-py==4.0.0 \
--hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \
@ -471,9 +485,9 @@ pip==26.0.1 \
--hash=sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b \
--hash=sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8
# via pip-tools
pip-tools==7.5.2 \
--hash=sha256:2d64d72da6a044da1110257d333960563d7a4743637e8617dd2610ae7b82d60f \
--hash=sha256:2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce
pip-tools==7.5.3 \
--hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \
--hash=sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41
# via -r src/backend/requirements-dev.in
platformdirs==4.9.2 \
--hash=sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd \
@ -513,27 +527,27 @@ pytest==9.0.2 \
# via
# pytest-codspeed
# pytest-django
pytest-codspeed==4.2.0 \
--hash=sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7 \
--hash=sha256:0881a736285f33b9a8894da8fe8e1775aa1a4310226abe5d1f0329228efb680c \
--hash=sha256:238e17abe8f08d8747fa6c7acff34fefd3c40f17a56a7847ca13dc8d6e8c6009 \
--hash=sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8 \
--hash=sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b \
--hash=sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe \
--hash=sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37 \
--hash=sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b \
--hash=sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8 \
--hash=sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5 \
--hash=sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f \
--hash=sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39 \
--hash=sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3 \
--hash=sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca \
--hash=sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830 \
--hash=sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0
pytest-codspeed==4.3.0 \
--hash=sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a \
--hash=sha256:10d19b48c9dc35b227a7ac863d4b3c58132256c2ba1aae3220aaddbf6f3f5f9a \
--hash=sha256:34f2fd8497456eefbd325673f677ea80d93bb1bc08a578c1fa43a09cec3d1879 \
--hash=sha256:5230d9d65f39063a313ed1820df775166227ec5c20a1122968f85653d5efee48 \
--hash=sha256:527a3a02eaa3e4d4583adc4ba2327eef79628f3e1c682a4b959439551a72588e \
--hash=sha256:619120775e92a3f43fb4ff4c256a251b1554c904d95e2154a382484283f0388a \
--hash=sha256:6ac31ae566bf047e91c79b6d12d9a31efedad556ff9258294d6ecebeacb92fa4 \
--hash=sha256:878aad5e4bb7b401ad8d82f3af5186030cd2bd0d0446782e10dabb9db8827466 \
--hash=sha256:9858c2a6e1f391d5696757e7b6e9484749a7376c46f8b4dd9aebf093479a9667 \
--hash=sha256:a2f5bb6d8898bea7db45e3c8b916ee48e36905b929477bb511b79c5a3ccacda4 \
--hash=sha256:b2acecc4126658abebc683b38121adec405a46e18a619d49d6154c6e60c5deb2 \
--hash=sha256:bec30f4fc9c4973143cd80f0d33fa780e9fa3e01e4dbe8cedf229e72f1212c62 \
--hash=sha256:dbeff1eb2f2e36df088658b556fa993e6937bf64ffb07406de4db16fd2b26874 \
--hash=sha256:df0d1f6ea594f29b745c634d66d5f5f1caa1c3abd2af82fea49d656038e8fc77 \
--hash=sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5 \
--hash=sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4
# via -r src/backend/requirements-dev.in
pytest-django==4.11.1 \
--hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \
--hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991
pytest-django==4.12.0 \
--hash=sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85 \
--hash=sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758
# via -r src/backend/requirements-dev.in
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
@ -622,9 +636,9 @@ requests-mock==1.12.1 \
--hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \
--hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401
# via -r src/backend/requirements-dev.in
rich==14.3.2 \
--hash=sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69 \
--hash=sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8
rich==14.3.3 \
--hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \
--hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b
# via pytest-codspeed
setuptools==82.0.0 \
--hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \
@ -731,9 +745,9 @@ urllib3==2.6.3 \
# via
# -c src/backend/requirements.txt
# requests
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==20.38.0 \
--hash=sha256:94f39b1abaea5185bf7ea5a46702b56f1d0c9aa2f41a6c2b8b0af4ddc74c10a7 \
--hash=sha256:d6e78e5889de3a4742df2d3d44e779366325a90cf356f15621fddace82431794
# via pre-commit
wheel==0.46.3 \
--hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \

View File

@ -95,15 +95,15 @@ blessed==1.30.0 \
--hash=sha256:4061a9f10dd22798716c2548ba36385af6a29d856c897f367c6ccc927e0b3a5a \
--hash=sha256:4d547019d7b40fc5420ea2ba2bc180fdccc31d6715298e2b49ffa7b020d44667
# via -r src/backend/requirements.in
boto3==1.42.54 \
--hash=sha256:71194e855bfc81a21872cbe29c41f52ffdbe67e0a184a52c13346ef00b328939 \
--hash=sha256:fe3d8ec586c39a0c96327fd317c77ca601ec5f991e9ba7211cacae8db4c07a73
boto3==1.42.58 \
--hash=sha256:1bc5ff0b7a1a3f42b115481e269e1aada1d68bbfa80a989ac2882d51072907a3 \
--hash=sha256:3a21b5bbc8bf8d6472a7ae7bdc77819b1f86f35d127f428f4603bed1b98122c0
# via
# django-anymail
# django-storages
botocore==1.42.54 \
--hash=sha256:853a0822de66d060aeebafa07ca13a03799f7958313d1b29f8dc7e2e1be8f527 \
--hash=sha256:ab203d4e57d22913c8386a695d048e003b7508a8a4a7a46c9ddf4ebd67a20b69
botocore==1.42.58 \
--hash=sha256:3098178f4404cf85c8997ebb7948b3f267cff1dd191b08fc4ebb614ac1013a20 \
--hash=sha256:55224d6a91afae0997e8bee62d1ef1ae2dcbc6c210516939b32a774b0b35bec5
# via
# boto3
# s3transfer
@ -209,9 +209,9 @@ brotli==1.2.0 \
--hash=sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361 \
--hash=sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d
# via fonttools
certifi==2026.1.4 \
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
certifi==2026.2.25 \
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
# via
# requests
# sentry-sdk
@ -485,9 +485,9 @@ defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
# via python3-openid
django==5.2.11 \
--hash=sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3 \
--hash=sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0
django==5.2.12 \
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
# via
# -r src/backend/requirements.in
# django-allauth
@ -518,9 +518,9 @@ django==5.2.11 \
# djangorestframework
# djangorestframework-simplejwt
# drf-spectacular
django-allauth[headless, mfa, openid, saml, socialaccount]==65.13.1 \
--hash=sha256:2887294beedfd108b4b52ebd182e0ed373deaeb927fc5a22f77bbde3174704a6 \
--hash=sha256:2af0d07812f8c1a8e3732feaabe6a9db5ecf3fad6b45b6a0f7fd825f656c5a15
django-allauth[headless, mfa, openid, saml, socialaccount]==65.14.3 \
--hash=sha256:1d8e1127bdffceb8001bdd9bafbf97661f81e92f4b7bd4f6e799167b0311286d \
--hash=sha256:548eef76ab85f6e48f46f98437abf22acf0e834f73e9915fb6cc3f31a0dcdf4d
# via -r src/backend/requirements.in
django-anymail[amazon-ses, postal]==14.0 \
--hash=sha256:98e5575bcff231952a92117b7d0a4dd37fab582333a343de302d415a31dc72b1 \
@ -773,68 +773,68 @@ googleapis-common-protos==1.72.0 \
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
grpcio==1.78.1 \
--hash=sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85 \
--hash=sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca \
--hash=sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862 \
--hash=sha256:0fa9943d4c7f4a14a9a876153a4e8ee2bb20a410b65c09f31510b2a42271f41b \
--hash=sha256:13937b28986f45fee342806b07c6344db785ad74a549ebcb00c659142973556f \
--hash=sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97 \
--hash=sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0 \
--hash=sha256:263307118791bc350f4642749a9c8c2d13fec496228ab11070973e568c256bfd \
--hash=sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338 \
--hash=sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72 \
--hash=sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907 \
--hash=sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba \
--hash=sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd \
--hash=sha256:36aeff5ba8aaf70ceb2cbf6cbba9ad6beef715ad744841f3e0cd977ec02e5966 \
--hash=sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22 \
--hash=sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11 \
--hash=sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f \
--hash=sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b \
--hash=sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce \
--hash=sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2 \
--hash=sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b \
--hash=sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70 \
--hash=sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529 \
--hash=sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9 \
--hash=sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb \
--hash=sha256:5572c5dd1e43dbb452b466be9794f77e3502bdb6aa6a1a7feca72c98c5085ca7 \
--hash=sha256:559f58b6823e1abc38f82e157800aff649146f8906f7998c356cd48ae274d512 \
--hash=sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460 \
--hash=sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0 \
--hash=sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22 \
--hash=sha256:75fa92c47d048d696f12b81a775316fca68385ffc6e6cb1ed1d76c8562579f74 \
--hash=sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb \
--hash=sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494 \
--hash=sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04 \
--hash=sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f \
--hash=sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42 \
--hash=sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1 \
--hash=sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79 \
--hash=sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541 \
--hash=sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67 \
--hash=sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba \
--hash=sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465 \
--hash=sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f \
--hash=sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d \
--hash=sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496 \
--hash=sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e \
--hash=sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89 \
--hash=sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0 \
--hash=sha256:ca6aebae928383e971d5eace4f1a217fd7aadaf18d5ddd3163d80354105e9068 \
--hash=sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd \
--hash=sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f \
--hash=sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4 \
--hash=sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a \
--hash=sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1 \
--hash=sha256:e49e720cd6b092504ec7bb2f60eb459aaaf4ce0e5fe20521c201b179e93b5d5d \
--hash=sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747 \
--hash=sha256:ebeec1383aed86530a5f39646984e92d6596c050629982ac54eeb4e2f6ead668 \
--hash=sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb \
--hash=sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20 \
--hash=sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1 \
--hash=sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28
grpcio==1.78.0 \
--hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \
--hash=sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d \
--hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \
--hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \
--hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \
--hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \
--hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \
--hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \
--hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \
--hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \
--hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \
--hash=sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec \
--hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \
--hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \
--hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \
--hash=sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb \
--hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \
--hash=sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df \
--hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \
--hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \
--hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \
--hash=sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf \
--hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \
--hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \
--hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \
--hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \
--hash=sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5 \
--hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \
--hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \
--hash=sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1 \
--hash=sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5 \
--hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \
--hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \
--hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \
--hash=sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf \
--hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \
--hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \
--hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \
--hash=sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c \
--hash=sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597 \
--hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \
--hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \
--hash=sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813 \
--hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \
--hash=sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2 \
--hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \
--hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \
--hash=sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a \
--hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \
--hash=sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5 \
--hash=sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a \
--hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \
--hash=sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c \
--hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \
--hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \
--hash=sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525 \
--hash=sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de \
--hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \
--hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \
--hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce \
--hash=sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7
# via
# -r src/backend/requirements.in
# opentelemetry-exporter-otlp-proto-grpc
@ -842,9 +842,9 @@ gunicorn==25.1.0 \
--hash=sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616 \
--hash=sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b
# via -r src/backend/requirements.in
icalendar==7.0.1 \
--hash=sha256:1f577af3e4d022a701aa98f9366dd524f0f1134b90c73c28493f06d5371901d5 \
--hash=sha256:68a6295f31ed907885ae79f10336b53ba067e0a66075b6a956abbe5e89ac7b39
icalendar==7.0.2 \
--hash=sha256:ad31a5825b39522a30b073c6ced3ffcdf6c02cbb7dab69ba2e4de32ddbf77cc9 \
--hash=sha256:de844ff5cde32f539bea7644e36d8494032a926b933bedb92621f2f239760806
# via django-ical
idna==3.11 \
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
@ -1477,9 +1477,9 @@ pynacl==1.6.2 \
--hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \
--hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9
# via paramiko
pypdf==6.7.2 \
--hash=sha256:331b63cd66f63138f152a700565b3e0cebdf4ec8bec3b7594b2522418782f1f3 \
--hash=sha256:82a1a48de500ceea59a52a7d979f5095927ef802e4e4fac25ab862a73468acbb
pypdf==6.7.5 \
--hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \
--hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d
# via -r src/backend/requirements.in
pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
@ -1686,9 +1686,9 @@ rapidfuzz==3.14.3 \
--hash=sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb \
--hash=sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9
# via -r src/backend/requirements.in
redis==7.2.0 \
--hash=sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497 \
--hash=sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26
redis==7.2.1 \
--hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \
--hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5
# via django-redis
referencing==0.37.0 \
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
@ -1922,9 +1922,9 @@ webencodings==0.5.1 \
# cssselect2
# tinycss2
# tinyhtml5
whitenoise==6.11.0 \
--hash=sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f \
--hash=sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258
whitenoise==6.12.0 \
--hash=sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad \
--hash=sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2
# via -r src/backend/requirements.in
wrapt==1.17.3 \
--hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \

View File

@ -2,6 +2,10 @@
This file contains historical changelog information for the InvenTree UI components library.
### 0.8.0 - March 2026
Exposes the `monitorDataOutput` hook, which allows plugins to monitor the output of a long-running task and display notifications when the task is complete. This is useful for plugins that need to perform long-running tasks and want to provide feedback to the user when the task is complete.
### 0.7.0 - October 2025
Exposes stock adjustment forms to plugins, allowing plugins to adjust stock adjustments using the common InvenTree UI form components.

View File

@ -0,0 +1,122 @@
import { t } from '@lingui/core/macro';
import { useDocumentVisibility } from '@mantine/hooks';
import { notifications, showNotification } from '@mantine/notifications';
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import type { AxiosInstance } from 'axios';
import { useEffect, useState } from 'react';
import { ProgressBar } from '../components/ProgressBar';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../functions/Api';
/**
* Hook for monitoring a data output process running on the server
*/
export default function monitorDataOutput({
api,
title,
hostname,
id
}: {
api: AxiosInstance;
title: string;
hostname?: string;
id?: number;
}) {
const visibility = useDocumentVisibility();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (!!id) {
setLoading(true);
showNotification({
id: `data-output-${id}`,
title: title,
loading: true,
autoClose: false,
withCloseButton: false,
message: <ProgressBar size='lg' value={0} progressLabel />
});
} else setLoading(false);
}, [id, title]);
useQuery({
enabled: !!id && loading && visibility === 'visible',
refetchInterval: 500,
queryKey: ['data-output', id, title],
queryFn: () =>
api
.get(apiUrl(ApiEndpoints.data_output, id))
.then((response) => {
const data = response?.data ?? {};
if (!!data.errors || !!data.error) {
setLoading(false);
const error: string =
data?.error ?? data?.errors?.error ?? t`Process failed`;
notifications.update({
id: `data-output-${id}`,
loading: false,
icon: <IconExclamationCircle />,
autoClose: 2500,
title: title,
message: error,
color: 'red'
});
} else if (data.complete) {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
loading: false,
autoClose: 2500,
title: title,
message: t`Process completed successfully`,
color: 'green',
icon: <IconCircleCheck />
});
if (data.output) {
const url = data.output;
const base = hostname ?? window.location.hostname;
const downloadUrl = new URL(url, base);
window.open(downloadUrl.toString(), '_blank');
}
} else {
notifications.update({
id: `data-output-${id}`,
loading: true,
autoClose: false,
withCloseButton: false,
message: (
<ProgressBar
size='lg'
maximum={data.total}
value={data.progress}
progressLabel={data.total > 0}
animated
/>
)
});
}
return data;
})
.catch(() => {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
loading: false,
autoClose: 2500,
title: title,
message: t`Process failed`,
color: 'red'
});
return {};
})
});
}

View File

@ -71,3 +71,6 @@ export {
RowCancelAction,
RowActions
} from './components/RowActions';
// Shared hooks
export { default as monitorDataOutput } from './hooks/MonitorDataOutput';

View File

@ -39,7 +39,7 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api';
*/
export type TableFilter = {
name: string;
label: string;
label?: string;
description?: string;
type?: TableFilterType;
choices?: TableFilterChoice[];

View File

@ -99,6 +99,8 @@ export type TableState = {
* @param cellsStyle - The style of the cells in the column
* @param extra - Extra data to pass to the render function
* @param noContext - Disable context menu for this column
* @param copyable - Enable copy button on hover (uses accessor to get value, or custom function)
* @param copyAccessor - Custom accessor path for copy value (defaults to column accessor)
*/
export type TableColumnProps<T = any> = {
accessor?: string;
@ -123,6 +125,8 @@ export type TableColumnProps<T = any> = {
extra?: any;
noContext?: boolean;
style?: MantineStyleProp;
copyable?: boolean | ((record: T) => string);
copyAccessor?: string;
};
/**

View File

@ -1,7 +1,7 @@
{
"name": "@inventreedb/ui",
"description": "UI components for the InvenTree project",
"version": "0.7.0",
"version": "0.8.0",
"private": false,
"type": "module",
"license": "MIT",

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/core/macro';
import {
ActionIcon,
type ActionIconVariant,
Button,
type DefaultMantineColor,
type FloatingPosition,
@ -22,7 +23,8 @@ export function CopyButton({
tooltipPosition,
content,
size,
color = 'gray'
color = 'gray',
variant = 'transparent'
}: Readonly<{
value: any;
label?: string;
@ -32,9 +34,15 @@ export function CopyButton({
content?: JSX.Element;
size?: MantineSize;
color?: DefaultMantineColor;
variant?: ActionIconVariant;
}>) {
const ButtonComponent = label ? Button : ActionIcon;
// Disable the copy button if we are not in a secure context, as the Clipboard API is not available
if (!window.isSecureContext) {
return null;
}
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
@ -46,8 +54,12 @@ export function CopyButton({
<ButtonComponent
disabled={disabled}
color={copied ? 'teal' : color}
onClick={copy}
variant='transparent'
onClick={(e: any) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
variant={copied ? 'transparent' : (variant ?? 'transparent')}
size={size ?? 'sm'}
>
{copied ? (

View File

@ -29,6 +29,10 @@ import {
} from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
defaultLocale,
getPriorityLocale
} from '../../contexts/LanguageContext';
import type { CalendarState } from '../../hooks/UseCalendar';
import { useLocalState } from '../../states/LocalState';
import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer';
@ -60,7 +64,19 @@ export default function Calendar({
const [locale] = useLocalState(useShallow((s) => [s.language]));
// Ensure underscore is replaced with dash
const calendarLocale = useMemo(() => locale.replace('_', '-'), [locale]);
const calendarLocale = useMemo(() => {
let _locale: string | null = locale;
if (!_locale) {
_locale = getPriorityLocale();
}
_locale = _locale || defaultLocale;
_locale = _locale.replace('_', '-');
return _locale;
}, [locale]);
const selectMonth = useCallback(
(date: DateValue) => {

View File

@ -54,10 +54,16 @@ export type DetailsField = {
type BadgeType = 'owner' | 'user' | 'group';
type ValueFormatterReturn = string | number | null | React.ReactNode;
type StringDetailField = {
type: 'string' | 'text' | 'date';
unit?: boolean;
};
type StringDetailField =
| {
type: 'string' | 'text';
unit?: boolean;
}
| {
type: 'date';
unit?: boolean;
showTime?: boolean;
};
type NumberDetailField = {
type: 'number';
@ -260,7 +266,13 @@ function NameBadge({
}
function DateValue(props: Readonly<FieldProps>) {
return <Text size='sm'>{formatDate(props.field_value?.toString())}</Text>;
return (
<Text size='sm'>
{formatDate(props.field_value?.toString(), {
showTime: props.field_data?.showTime
})}
</Text>
);
}
// Return a formatted "number" value, with optional unit

View File

@ -1,8 +1,12 @@
import { Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { t } from '@lingui/core/macro';
import { useShallow } from 'zustand/react/shallow';
import { getSupportedLanguages } from '../../contexts/LanguageContext';
import {
activateLocale,
getSupportedLanguages
} from '../../contexts/LanguageContext';
import { useLocalState } from '../../states/LocalState';
export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
@ -28,13 +32,21 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
}));
setLangOptions(newLangOptions);
setValue(locale);
activateLocale(locale); // Ensure the locale is activated on component load
}, [locale]);
return (
<Select
w={width}
data={langOptions}
data={[
{
value: '',
label: t`Default Language`
},
...langOptions
]}
value={value}
defaultValue={''}
onChange={setValue}
searchable
aria-label='Select language'

View File

@ -18,6 +18,7 @@ import {
type InvenTreePluginContext
} from '@lib/types/Plugins';
import { i18n } from '@lingui/core';
import { defaultLocale } from '../../contexts/LanguageContext';
import {
useAddStockItem,
useAssignStockItem,
@ -35,10 +36,12 @@ import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useServerApiState } from '../../states/ServerApiState';
import { RenderInstance } from '../render/Instance';
export const useInvenTreeContext = () => {
const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host]));
const [server] = useServerApiState(useShallow((s) => [s.server]));
const navigate = useNavigate();
const user = useUserState();
const { colorScheme } = useMantineColorScheme();
@ -57,7 +60,7 @@ export const useInvenTreeContext = () => {
user: user,
host: host,
i18n: i18n,
locale: locale,
locale: locale || server.default_locale || defaultLocale,
api: api,
queryClient: queryClient,
navigate: navigate,

View File

@ -65,9 +65,29 @@ export function LanguageContext({
const [language] = useLocalState(useShallow((state) => [state.language]));
const [server] = useServerApiState(useShallow((state) => [state.server]));
const [activeLocale, setActiveLocale] = useState<string | null>(null);
useEffect(() => {
activateLocale(defaultLocale);
}, []);
// Update the locale based on prioritization:
// 1. Locally selected locale
// 2. Server default locale
// 3. English (fallback)
let locale: string | null = activeLocale;
if (!!language) {
locale = language;
} else if (!!server.default_locale) {
locale = server.default_locale;
} else {
locale = defaultLocale;
}
if (locale != activeLocale) {
setActiveLocale(locale);
activateLocale(locale);
}
}, [activeLocale, language, server.default_locale, defaultLocale]);
const [loadedState, setLoadedState] = useState<
'loading' | 'loaded' | 'error'
@ -77,7 +97,7 @@ export function LanguageContext({
useEffect(() => {
isMounted.current = true;
let lang = language;
let lang: string = language || defaultLocale;
// Ensure that the selected language is supported
if (!Object.keys(getSupportedLanguages()).includes(lang)) {
@ -96,7 +116,7 @@ export function LanguageContext({
*/
const locales: (string | undefined)[] = [];
if (lang != 'pseudo-LOCALE') {
if (!!lang && lang != 'pseudo-LOCALE') {
locales.push(lang);
}
@ -156,8 +176,26 @@ export function LanguageContext({
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
export async function activateLocale(locale: string) {
const { messages } = await import(`../locales/${locale}/messages.ts`);
i18n.load(locale, messages);
i18n.activate(locale);
// This function is used to determine the locale to activate based on the prioritization rules.
export function getPriorityLocale(): string {
const serverDefault = useServerApiState.getState().server.default_locale;
const userDefault = useLocalState.getState().language;
return userDefault || serverDefault || defaultLocale;
}
export async function activateLocale(locale: string | null) {
if (!locale) {
locale = getPriorityLocale();
}
const localeDir = locale.split('-')[0]; // Extract the base locale (e.g., 'en' from 'en-US')
try {
const { messages } = await import(`../locales/${localeDir}/messages.ts`);
i18n.load(locale, messages);
i18n.activate(locale);
} catch (err) {
console.error(`Failed to load locale ${locale}:`, err);
}
}

View File

@ -1,6 +1,10 @@
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react';
import { MantineProvider, createTheme } from '@mantine/core';
import {
MantineProvider,
type MantineThemeOverride,
createTheme
} from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import { ContextMenuProvider } from 'mantine-contextmenu';
@ -20,23 +24,31 @@ export function ThemeContext({
}: Readonly<{ children: JSX.Element }>) {
const [userTheme] = useLocalState(useShallow((state) => [state.userTheme]));
let customUserTheme: MantineThemeOverride | undefined = undefined;
// Theme
const myTheme = createTheme({
primaryColor: userTheme.primaryColor,
white: userTheme.whiteColor,
black: userTheme.blackColor,
defaultRadius: userTheme.radius,
breakpoints: {
xs: '30em',
sm: '48em',
md: '64em',
lg: '74em',
xl: '90em'
}
});
try {
customUserTheme = createTheme({
primaryColor: userTheme.primaryColor,
white: userTheme.whiteColor,
black: userTheme.blackColor,
defaultRadius: userTheme.radius,
breakpoints: {
xs: '30em',
sm: '48em',
md: '64em',
lg: '74em',
xl: '90em'
}
});
} catch (error) {
console.error('Error creating theme with user settings:', error);
// Fallback to default theme if there's an error
customUserTheme = undefined;
}
return (
<MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
<MantineProvider theme={customUserTheme} colorSchemeManager={colorSchema}>
<ContextMenuProvider>
<LanguageContext>
<ModalsProvider

View File

@ -39,6 +39,7 @@ export function useSupplierPartFields({
},
manufacturer_part: {
value: manufacturerPartId,
autoFill: true,
filters: {
manufacturer: manufacturerId,
part_detail: true,

View File

@ -113,16 +113,6 @@ export function usePurchaseOrderLineItemFields({
}
}, [create, part, quantity, priceBreaks]);
useEffect(() => {
if (autoPricing) {
setPurchasePrice('');
}
}, [autoPricing]);
useEffect(() => {
setAutoPricing(purchasePrice === '');
}, [purchasePrice]);
const fields = useMemo(() => {
const fields: ApiFormFieldSet = {
order: {
@ -159,6 +149,7 @@ export function usePurchaseOrderLineItemFields({
purchase_price: {
icon: <IconCurrencyDollar />,
value: purchasePrice,
disabled: autoPricing,
placeholder: suggestedPurchasePrice,
placeholderAutofill: true,
onValueChange: setPurchasePrice
@ -169,6 +160,7 @@ export function usePurchaseOrderLineItemFields({
onValueChange: setPurchasePriceCurrency
},
auto_pricing: {
default: create !== false,
value: autoPricing,
onValueChange: setAutoPricing
},

View File

@ -1,14 +1,6 @@
import { ProgressBar } from '@lib/components/ProgressBar';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro';
import { useDocumentVisibility } from '@mantine/hooks';
import { notifications, showNotification } from '@mantine/notifications';
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import monitorDataOutput from '@lib/hooks/MonitorDataOutput';
import { useApi } from '../contexts/ApiContext';
import { generateUrl } from '../functions/urls';
import { useLocalState } from '../states/LocalState';
/**
* Hook for monitoring a data output process running on the server
@ -21,97 +13,12 @@ export default function useDataOutput({
id?: number;
}) {
const api = useApi();
const { getHost } = useLocalState.getState();
const visibility = useDocumentVisibility();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (!!id) {
setLoading(true);
showNotification({
id: `data-output-${id}`,
title: title,
loading: true,
autoClose: false,
withCloseButton: false,
message: <ProgressBar size='lg' value={0} progressLabel />
});
} else setLoading(false);
}, [id, title]);
useQuery({
enabled: !!id && loading && visibility === 'visible',
refetchInterval: 500,
queryKey: ['data-output', id, title],
queryFn: () =>
api
.get(apiUrl(ApiEndpoints.data_output, id))
.then((response) => {
const data = response?.data ?? {};
if (!!data.errors || !!data.error) {
setLoading(false);
const error: string =
data?.error ?? data?.errors?.error ?? t`Process failed`;
notifications.update({
id: `data-output-${id}`,
loading: false,
icon: <IconExclamationCircle />,
autoClose: 2500,
title: title,
message: error,
color: 'red'
});
} else if (data.complete) {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
loading: false,
autoClose: 2500,
title: title,
message: t`Process completed successfully`,
color: 'green',
icon: <IconCircleCheck />
});
if (data.output) {
const url = generateUrl(data.output);
window.open(url.toString(), '_blank');
}
} else {
notifications.update({
id: `data-output-${id}`,
loading: true,
autoClose: false,
withCloseButton: false,
message: (
<ProgressBar
size='lg'
maximum={data.total}
value={data.progress}
progressLabel={data.total > 0}
animated
/>
)
});
}
return data;
})
.catch(() => {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
loading: false,
autoClose: 2500,
title: title,
message: t`Process failed`,
color: 'red'
});
return {};
})
return monitorDataOutput({
api: api,
title: title,
id: id,
hostname: getHost()
});
}

View File

@ -1,21 +1,43 @@
import type { FilterSetState, TableFilter } from '@lib/types/Filters';
import { useLocalStorage } from '@mantine/hooks';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
export function useFilterSet(filterKey: string): FilterSetState {
export function useFilterSet(
filterKey: string,
initialFilters?: TableFilter[]
): FilterSetState {
// Array of active filters (saved to local storage)
const [activeFilters, setActiveFilters] = useLocalStorage<TableFilter[]>({
const [storedFilters, setStoredFilters] = useLocalStorage<
TableFilter[] | null
>({
key: `inventree-filterset-${filterKey}`,
defaultValue: [],
defaultValue: null,
sync: false,
getInitialValueInEffect: false
});
const activeFilters: TableFilter[] = useMemo(() => {
if (storedFilters == null) {
// If there are no stored filters, set initial values
const filters = initialFilters || [];
setStoredFilters(filters);
return filters;
}
return storedFilters || [];
}, [storedFilters]);
// Callback to clear all active filters from the table
const clearActiveFilters = useCallback(() => {
setActiveFilters([]);
setStoredFilters([]);
}, []);
const setActiveFilters = useCallback(
(filters: TableFilter[]) => {
setStoredFilters(filters);
},
[setStoredFilters]
);
return {
filterKey,
activeFilters,

View File

@ -2,17 +2,28 @@ import { randomId } from '@mantine/hooks';
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { FilterSetState } from '@lib/types/Filters';
import type { FilterSetState, TableFilter } from '@lib/types/Filters';
import type { TableState } from '@lib/types/Tables';
import { useFilterSet } from './UseFilterSet';
export type TableStateExtraProps = {
idAccessor?: string;
initialFilters?: TableFilter[];
};
/**
* A custom hook for managing the state of an <InvenTreeTable> component.
*
* Refer to the TableState type definition for more information.
*/
export function useTable(tableName: string, idAccessor = 'pk'): TableState {
export function useTable(
tableName: string,
tableProps: TableStateExtraProps = {
idAccessor: 'pk',
initialFilters: []
}
): TableState {
// Function to generate a new ID (to refresh the table)
function generateTableName() {
return `${tableName.replaceAll('-', '')}-${randomId()}`;
@ -38,7 +49,10 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
[generateTableName]
);
const filterSet: FilterSetState = useFilterSet(`table-${tableName}`);
const filterSet: FilterSetState = useFilterSet(
`table-${tableName}`,
tableProps.initialFilters
);
// Array of expanded records
const [expandedRecords, setExpandedRecords] = useState<any[]>([]);
@ -59,7 +73,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
// Array of selected primary key values
const selectedIds = useMemo(
() => selectedRecords.map((r) => r[idAccessor || 'pk']),
() => selectedRecords.map((r) => r[tableProps.idAccessor || 'pk']),
[selectedRecords]
);
@ -89,7 +103,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
// Find the matching record in the table
const index = _records.findIndex(
(r) => r[idAccessor || 'pk'] === record.pk
(r) => r[tableProps.idAccessor || 'pk'] === record.pk
);
if (index >= 0) {
@ -106,6 +120,11 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
[records]
);
const idAccessor = useMemo(
() => tableProps.idAccessor || 'pk',
[tableProps.idAccessor]
);
const [isLoading, setIsLoading] = useState<boolean>(false);
return {

View File

@ -20,7 +20,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
export function CurrencyTable({
setInfo
}: Readonly<{ setInfo: (info: any) => void }>) {
const table = useTable('currency', 'currency');
const table = useTable('currency', { idAccessor: 'currency' });
const columns = useMemo(() => {
return [
{

View File

@ -11,7 +11,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable';
function AllUnitTable() {
const table = useTable('all-units', 'name');
const table = useTable('all-units', { idAccessor: 'name' });
const columns = useMemo(() => {
return [
{

View File

@ -217,6 +217,7 @@ export default function SystemSettings() {
'PART_SALABLE',
'PART_VIRTUAL',
'PART_COPY_BOM',
'PART_BOM_ALLOW_ZERO_QUANTITY',
'PART_COPY_PARAMETERS',
'PART_COPY_TESTS',
'PART_CATEGORY_PARAMETERS',

View File

@ -469,6 +469,7 @@ export default function BuildDetail() {
tableName='build-consumed'
showLocation={false}
allowReturn
defaultInStock={null}
params={{
consumed_by: id
}}

View File

@ -248,6 +248,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
tableName='assigned-stock'
showLocation={false}
allowReturn
defaultInStock={null}
params={{ customer: company.pk }}
/>
) : (

View File

@ -304,6 +304,15 @@ export default function PurchaseOrderDetail() {
label: t`Completion Date`,
copy: true,
hidden: !order.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@ -108,6 +108,7 @@ export default function PurchasingIndex() {
icon: <IconTable />,
content: (
<CompanyTable
companyType='supplier'
path='purchasing/supplier'
params={{ is_supplier: true }}
/>
@ -157,6 +158,7 @@ export default function PurchasingIndex() {
icon: <IconTable />,
content: (
<CompanyTable
companyType='manufacturer'
path='purchasing/manufacturer'
params={{ is_manufacturer: true }}
/>

View File

@ -282,6 +282,15 @@ export default function ReturnOrderDetail() {
label: t`Completion Date`,
copy: true,
hidden: !order.complete_date
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@ -141,6 +141,7 @@ export default function SalesIndex() {
icon: <IconTable />,
content: (
<CompanyTable
companyType='customer'
path='sales/customer'
params={{ is_customer: true }}
/>

View File

@ -273,6 +273,15 @@ export default function SalesOrderDetail() {
label: t`Completion Date`,
hidden: !order.shipment_date,
copy: true
},
{
type: 'date',
name: 'updated_at',
label: t`Last Updated`,
icon: 'calendar',
copy: true,
showTime: true,
hidden: !order.updated_at
}
];

View File

@ -17,8 +17,8 @@ interface LocalStateProps {
hostKey: string;
hostList: HostList;
setHostList: (newHostList: HostList) => void;
language: string;
setLanguage: (newLanguage: string, noPatch?: boolean) => void;
language: string | null;
setLanguage: (newLanguage: string | null, noPatch?: boolean) => void;
userTheme: UserTheme;
setTheme: (
newValues: {
@ -78,7 +78,7 @@ export const useLocalState = create<LocalStateProps>()(
hostKey: '',
hostList: {},
setHostList: (newHostList) => set({ hostList: newHostList }),
language: 'en',
language: null,
setLanguage: (newLanguage, noPatch = false) => {
set({ language: newLanguage });
if (!noPatch) patchUser('language', newLanguage);

View File

@ -107,6 +107,18 @@ export function PartColumn(props: PartColumnProps): TableColumn {
};
}
export function IPNColumn(props: TableColumnProps): TableColumn {
return {
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
switchable: true,
title: t`IPN`,
copyable: true,
...props
};
}
export type StockColumnProps = TableColumnProps & {
nullMessage?: string | ReactNode;
};
@ -448,6 +460,7 @@ export function DescriptionColumn(props: TableColumnProps): TableColumn {
sortable: false,
switchable: true,
minWidth: '200px',
copyable: true,
...props
};
}
@ -457,6 +470,8 @@ export function LinkColumn(props: TableColumnProps): TableColumn {
accessor: 'link',
sortable: false,
defaultVisible: false,
copyable: true,
copyAccessor: props.accessor ?? 'link',
render: (record: any) => {
const url = resolveItem(record, props.accessor ?? 'link');
@ -490,6 +505,7 @@ export function ReferenceColumn(props: TableColumnProps): TableColumn {
title: t`Reference`,
sortable: true,
switchable: true,
copyable: true,
...props
};
}
@ -664,6 +680,7 @@ export function DateColumn(props: TableColumnProps): TableColumn {
formatDate(resolveItem(record, props.accessor ?? 'date'), {
showTime: props.extra?.showTime
}),
copyable: true,
...props
};
}
@ -708,6 +725,16 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
});
}
export function UpdatedAtColumn(props: TableColumnProps): TableColumn {
return DateColumn({
accessor: 'updated_at',
title: t`Updated`,
defaultVisible: false,
extra: { showTime: true },
...props
});
}
export function CurrencyColumn({
accessor,
title,

View File

@ -0,0 +1,51 @@
import { Group } from '@mantine/core';
import { useState } from 'react';
import { CopyButton } from '../components/buttons/CopyButton';
/**
* A wrapper component that adds a copy button to cell content on hover
* This component is used to make table cells copyable without adding visual clutter
*
* @param children - The cell content to render
* @param value - The value to copy when the copy button is clicked
*/
export function CopyableCell({
children,
value
}: Readonly<{
children: React.ReactNode;
value: string;
}>) {
const [isHovered, setIsHovered] = useState(false);
return (
<Group
gap={0}
p={0}
wrap='nowrap'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
justify='space-between'
align='center'
>
{children}
{window.isSecureContext && isHovered && value != null && (
<span
style={{ position: 'relative' }}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div
style={{
position: 'absolute',
right: 0,
transform: 'translateY(-50%)'
}}
>
<CopyButton value={value} variant={'default'} />
</div>
</span>
)}
</Group>
);
}

View File

@ -3,6 +3,7 @@ import { t } from '@lingui/core/macro';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { isTrue } from '@lib/functions/Conversion';
import type { TableFilter, TableFilterChoice } from '@lib/types/Filters';
import type {
StatusCodeInterface,
@ -14,6 +15,47 @@ import {
} from '../states/GlobalStatusState';
import { useGlobalSettingsState } from '../states/SettingsStates';
// Determine the appropriate display label for a given filter, based on its name and the list of available filters
export function filterDisplayLabel(
name: string,
filters?: TableFilter[]
): string {
const filter = filters?.find((f) => f.name === name);
return filter?.label ?? name;
}
// Determine the appropriate display value for a filter, based on its type and value
// This is useful for recreating a display value if we only have a name:value pair
export function filterDisplayValue(
name: string,
value: any,
filters?: TableFilter[]
) {
const filterDef = filters?.find((f) => f.name === name);
if (!filterDef) {
return value;
}
if (!filterDef.type || filterDef.type == 'boolean') {
return isTrue(value) ? t`Yes` : t`No`;
}
if (filterDef.type === 'choice' && filterDef.choices) {
const choice = filterDef.choices.find((c) => c.value === value);
return choice ? choice.label : value;
}
if (filterDef.type === 'choice' && filterDef.choiceFunction) {
const choices = filterDef.choiceFunction();
const choice = choices.find((c) => c.value === value);
return choice ? choice.label : value;
}
// No obvious match - return the raw value
return value;
}
/**
* Return list of available filter options for a given filter
* @param filter - TableFilter object
@ -244,6 +286,24 @@ export function CompletedAfterFilter(): TableFilter {
};
}
export function UpdatedAfterFilter(): TableFilter {
return {
name: 'updated_after',
label: t`Updated After`,
description: t`Show orders updated after this date`,
type: 'date'
};
}
export function UpdatedBeforeFilter(): TableFilter {
return {
name: 'updated_before',
label: t`Updated Before`,
description: t`Show orders updated before this date`,
type: 'date'
};
}
export function HasProjectCodeFilter(): TableFilter {
const globalSettings = useGlobalSettingsState.getState();
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);

View File

@ -28,17 +28,46 @@ import type {
import { IconCheck } from '@tabler/icons-react';
import { StandaloneField } from '../components/forms/StandaloneField';
import { StylishText } from '../components/items/StylishText';
import { getTableFilterOptions } from './Filter';
import {
filterDisplayLabel,
filterDisplayValue,
getTableFilterOptions
} from './Filter';
/*
* Render a preview of a single filter
*/
export function FilterPreview({
filter,
filters
}: Readonly<{
filter: TableFilter;
filters?: TableFilter[];
}>) {
return (
<Group key={filter.name} justify='space-between' gap='xl' wrap='nowrap'>
<Text size='sm'>
{filter.label ?? filterDisplayLabel(filter.name, filters)}
</Text>
<Text size='xs'>
{filter.displayValue ??
filterDisplayValue(filter.name, filter.value, filters)}
</Text>
</Group>
);
}
/*
* Render a single table filter item
*/
function FilterItem({
flt,
filterSet
filterSet,
availableFilters
}: Readonly<{
flt: TableFilter;
filterSet: FilterSetState;
availableFilters?: TableFilter[];
}>) {
const removeFilter = useCallback(() => {
const newFilters = filterSet.activeFilters.filter(
@ -47,17 +76,29 @@ function FilterItem({
filterSet.setActiveFilters(newFilters);
}, [flt]);
// Find the matching filter definition
const filterProps: TableFilter | undefined = useMemo(() => {
return availableFilters?.find((f) => f.name === flt.name);
}, [availableFilters, flt]);
return (
<Paper p='sm' shadow='sm' radius='xs'>
<Group justify='space-between' key={flt.name} wrap='nowrap'>
<Stack gap='xs'>
<Text size='sm'>{flt.label}</Text>
<Text size='xs'>{flt.description}</Text>
<Text size='sm'>{flt.label ?? filterProps?.label ?? flt.name}</Text>
<Text size='xs'>{flt.description ?? filterProps?.description}</Text>
</Stack>
<Group justify='right'>
<Badge>{flt.displayValue ?? flt.value}</Badge>
<Tooltip label={t`Remove filter`} withinPortal={true}>
<CloseButton size='md' color='red' onClick={removeFilter} />
<Badge>
{flt.displayValue ??
filterDisplayValue(flt.name, flt.value, availableFilters)}
</Badge>
<Tooltip
label={t`Remove filter`}
withinPortal={true}
position='top-end'
>
<CloseButton size='md' c='red' onClick={removeFilter} />
</Tooltip>
</Group>
</Group>
@ -174,10 +215,10 @@ function FilterAddGroup({
return (
availableFilters
?.filter((flt) => !activeFilterNames.includes(flt.name))
?.sort((a, b) => a.label.localeCompare(b.label))
?.sort((a, b) => (a.label ?? a.name).localeCompare(b.label ?? b.name))
?.map((flt) => ({
value: flt.name,
label: flt.label,
label: flt.label ?? flt.name,
description: flt.description
})) ?? []
);
@ -317,7 +358,12 @@ export function FilterSelectDrawer({
<Stack gap='xs'>
{hasFilters &&
filterSet.activeFilters?.map((f) => (
<FilterItem key={f.name} flt={f} filterSet={filterSet} />
<FilterItem
key={f.name}
flt={f}
filterSet={filterSet}
availableFilters={availableFilters}
/>
))}
{addFilter && (
<Stack gap='xs'>

View File

@ -33,6 +33,7 @@ import { hashString } from '../functions/tables';
import { useLocalState } from '../states/LocalState';
import { useUserSettingsState } from '../states/SettingsStates';
import { useStoredTableState } from '../states/StoredTableState';
import { CopyableCell } from './CopyableCell';
import InvenTreeTableHeader from './InvenTreeTableHeader';
const ACTIONS_COLUMN_ACCESSOR: string = '--actions--';
@ -264,11 +265,40 @@ export function InvenTreeTable<T extends Record<string, any>>({
? false
: (tableState.hiddenColumns?.includes(col.accessor) ?? false);
// Wrap the render function with CopyableCell if copyable is enabled
const originalRender = col.render;
let wrappedRender = originalRender;
if (col.copyable) {
wrappedRender = (record: any, index?: number) => {
const content =
originalRender?.(record, index) ??
resolveItem(record, col.accessor);
// Determine the value to copy, ensuring it is always a string
let rawCopyValue: unknown;
if (typeof col.copyable === 'function') {
rawCopyValue = col.copyable(record);
} else {
const accessor = col.copyAccessor ?? col.accessor;
rawCopyValue = resolveItem(record, accessor);
}
const copyValue = rawCopyValue == null ? '' : String(rawCopyValue);
if (window.isSecureContext && !!copyValue) {
return <CopyableCell value={copyValue}>{content}</CopyableCell>;
} else {
return content;
}
};
}
return {
...col,
hidden: hidden,
resizable: col.resizable ?? true,
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`,
render: wrappedRender,
cellsStyle: (record: any, index: number) => {
const width = (col as any).minWidth ?? 100;
return {

View File

@ -9,7 +9,6 @@ import {
Paper,
Space,
Stack,
Text,
Tooltip
} from '@mantine/core';
import {
@ -37,7 +36,7 @@ import { StylishText } from '../components/items/StylishText';
import useDataExport from '../hooks/UseDataExport';
import { useDeleteApiFormModal } from '../hooks/UseForm';
import { TableColumnSelect } from './ColumnSelect';
import { FilterSelectDrawer } from './FilterSelectDrawer';
import { FilterPreview, FilterSelectDrawer } from './FilterSelectDrawer';
/**
* Render a composite header for an InvenTree table
@ -272,15 +271,10 @@ export default function InvenTreeTableHeader({
<StylishText size='md'>{t`Active Filters`}</StylishText>
<Divider />
{tableState.filterSet.activeFilters?.map((filter) => (
<Group
key={filter.name}
justify='space-between'
gap='xl'
wrap='nowrap'
>
<Text size='sm'>{filter.label}</Text>
<Text size='xs'>{filter.displayValue}</Text>
</Group>
<FilterPreview
filter={filter}
filters={tableProps.tableFilters}
/>
))}
</Stack>
</Paper>

View File

@ -44,6 +44,7 @@ import {
BooleanColumn,
CategoryColumn,
DescriptionColumn,
IPNColumn,
NoteColumn,
ReferenceColumn
} from '../ColumnRenderers';
@ -129,12 +130,9 @@ export function BomTable({
);
}
},
{
accessor: 'sub_part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN'
},
IPNColumn({
accessor: 'sub_part_detail.IPN'
}),
CategoryColumn({
accessor: 'category_detail',
defaultVisible: false,

View File

@ -15,6 +15,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
PartColumn,
ReferenceColumn
} from '../ColumnRenderers';
@ -40,11 +41,9 @@ export function UsedInTable({
title: t`Assembly`,
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
sortable: false,
title: t`IPN`
},
IPNColumn({
sortable: false
}),
{
accessor: 'part_detail.revision',
title: t`Revision`,

View File

@ -22,6 +22,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DecimalColumn,
IPNColumn,
LocationColumn,
PartColumn,
ReferenceColumn,
@ -99,14 +100,9 @@ export default function BuildAllocatedStockTable({
hidden: !showPartInfo,
switchable: false
}),
{
accessor: 'part_detail.IPN',
ordering: 'IPN',
hidden: !showPartInfo,
title: t`IPN`,
sortable: true,
switchable: true
},
IPNColumn({
hidden: !showPartInfo
}),
{
hidden: !showPartInfo,
accessor: 'bom_reference',
@ -119,7 +115,9 @@ export default function BuildAllocatedStockTable({
title: t`Batch Code`,
sortable: false,
switchable: true,
render: (record: any) => record?.stock_item_detail?.batch
render: (record: any) => record?.stock_item_detail?.batch,
copyable: true,
copyAccessor: 'stock_item_detail.batch'
},
DecimalColumn({
accessor: 'stock_item_detail.quantity',

View File

@ -44,6 +44,7 @@ import {
CategoryColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LocationColumn,
PartColumn,
RenderPartColumn
@ -91,7 +92,9 @@ export function BuildLineSubTable({
},
{
accessor: 'stock_item_detail.batch',
title: t`Batch`
title: t`Batch`,
copyable: true,
copyAccessor: 'stock_item_detail.batch'
},
LocationColumn({
accessor: 'location_detail'
@ -332,12 +335,7 @@ export default function BuildLineTable({
);
}
}),
{
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
title: t`IPN`
},
IPNColumn({}),
CategoryColumn({
accessor: 'category_detail',
defaultVisible: false,

View File

@ -18,6 +18,8 @@ import {
CreationDateColumn,
DateColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
@ -64,7 +66,14 @@ export function BuildOrderTable({
salesOrderId?: number;
}>) {
const globalSettings = useGlobalSettingsState();
const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index');
const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index', {
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
});
const tableColumns = useMemo(() => {
return [
@ -72,13 +81,7 @@ export function BuildOrderTable({
PartColumn({
switchable: false
}),
{
accessor: 'part_detail.IPN',
sortable: true,
ordering: 'IPN',
switchable: true,
title: t`IPN`
},
IPNColumn({}),
{
accessor: 'part_detail.revision',
title: t`Revision`,
@ -143,7 +146,8 @@ export function BuildOrderTable({
ordering: 'issued_by',
title: t`Issued By`
}),
ResponsibleColumn({})
ResponsibleColumn({}),
LinkColumn({})
];
}, [parentBuildId, globalSettings]);

View File

@ -29,13 +29,22 @@ import { InvenTreeTable } from '../InvenTreeTable';
* based on the provided filter parameters
*/
export function CompanyTable({
companyType,
params,
path
}: Readonly<{
companyType?: string;
params?: any;
path?: string;
}>) {
const table = useTable('company');
const table = useTable(`company-${companyType ?? 'index'}`, {
initialFilters: [
{
name: 'active',
value: 'true'
}
]
});
const navigate = useNavigate();
const user = useUserState();

View File

@ -26,7 +26,7 @@ export default function BarcodeScanTable({
const navigate = useNavigate();
const user = useUserState();
const table = useTable('barcode-scan-results', 'id');
const table = useTable('barcode-scan-results', { idAccessor: 'id' });
const tableColumns: TableColumn[] = useMemo(() => {
return [

View File

@ -16,6 +16,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
PartColumn,
ProjectCodeColumn,
StatusColumn
@ -56,10 +57,7 @@ export default function PartSalesAllocationsTable({
PartColumn({
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`
},
IPNColumn({}),
ProjectCodeColumn({
accessor: 'order_detail.project_code_detail'
}),

View File

@ -41,6 +41,7 @@ import {
CategoryColumn,
DefaultLocationColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn
} from '../ColumnRenderers';
@ -56,17 +57,17 @@ function partTableColumns(): TableColumn[] {
part: '',
accessor: 'name'
}),
{
accessor: 'IPN',
sortable: true
},
IPNColumn({
accessor: 'IPN'
}),
{
accessor: 'revision',
sortable: true
},
{
accessor: 'units',
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
CategoryColumn({
@ -338,17 +339,26 @@ export function PartListTable({
enableImport = true,
basePartInstance,
props,
tableName = 'part-list',
defaultPartData
}: Readonly<{
enableImport?: boolean;
props?: InvenTreeTableProps;
basePartInstance?: any;
tableName?: string;
defaultPartData?: any;
}>) {
const tableColumns = useMemo(() => partTableColumns(), []);
const tableFilters = useMemo(() => partTableFilters(), []);
const table = useTable('part-list');
const table = useTable(tableName ?? 'part-list', {
initialFilters: [
{
name: 'active',
value: 'true'
}
]
});
const user = useUserState();
const globalSettings = useGlobalSettingsState();

View File

@ -288,7 +288,8 @@ export default function PartTestResultTable({
accessor: 'batch',
title: t`Batch Code`,
sortable: true,
switchable: true
switchable: true,
copyable: true
},
LocationColumn({
accessor: 'location_detail'

View File

@ -43,6 +43,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
ancestor: part.pk
}
}}
tableName='part-variants'
basePartInstance={part}
defaultPartData={{
...part,

View File

@ -19,7 +19,9 @@ export interface PluginRegistryErrorI {
* Table displaying list of plugin registry errors
*/
export default function PluginErrorTable() {
const table = useTable('registryErrors', 'id');
const table = useTable('registryErrors', {
idAccessor: 'id'
});
const registryErrorTableColumns: TableColumn<PluginRegistryErrorI>[] =
useMemo(

View File

@ -3,7 +3,7 @@ import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { type ReactNode, useMemo } from 'react';
import { CompanyColumn, PartColumn } from '../ColumnRenderers';
import { CompanyColumn, IPNColumn, PartColumn } from '../ColumnRenderers';
import ParametricDataTable from '../general/ParametricDataTable';
export default function ManufacturerPartParametricTable({
@ -16,12 +16,9 @@ export default function ManufacturerPartParametricTable({
PartColumn({
switchable: false
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: false,
switchable: true
},
IPNColumn({
sortable: false
}),
{
accessor: 'manufacturer',
sortable: true,
@ -32,7 +29,8 @@ export default function ManufacturerPartParametricTable({
{
accessor: 'MPN',
title: t`MPN`,
sortable: true
sortable: true,
copyable: true
}
];
}, []);

View File

@ -25,6 +25,7 @@ import { useUserState } from '../../states/UserState';
import {
CompanyColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
PartColumn
} from '../ColumnRenderers';
@ -54,7 +55,29 @@ export function ManufacturerPartTable({
return tId;
}, [manufacturerId, partId]);
const table = useTable(tableId);
const initialFilters = useMemo(() => {
const filters: TableFilter[] = [];
if (!manufacturerId) {
filters.push({
name: 'manufacturer_active',
value: 'true'
});
}
if (!partId) {
filters.push({
name: 'part_active',
value: 'true'
});
}
return filters;
}, [manufacturerId, partId]);
const table = useTable(tableId, {
initialFilters: initialFilters
});
const user = useUserState();
@ -64,13 +87,7 @@ export function ManufacturerPartTable({
PartColumn({
switchable: !!partId
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
{
accessor: 'manufacturer',
sortable: true,
@ -81,7 +98,8 @@ export function ManufacturerPartTable({
{
accessor: 'MPN',
title: t`MPN`,
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
LinkColumn({})

View File

@ -237,7 +237,8 @@ export function PurchaseOrderLineItemTable({
title: t`Supplier Code`,
switchable: false,
sortable: true,
ordering: 'SKU'
ordering: 'SKU',
copyable: true
},
LinkColumn({
accessor: 'supplier_part_detail.link',
@ -250,7 +251,8 @@ export function PurchaseOrderLineItemTable({
ordering: 'MPN',
title: t`Manufacturer Code`,
sortable: true,
defaultVisible: false
defaultVisible: false,
copyable: true
},
CurrencyColumn({
accessor: 'purchase_price',

View File

@ -19,12 +19,14 @@ import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
StartDateColumn,
StatusColumn,
TargetDateColumn
TargetDateColumn,
UpdatedAtColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
@ -44,7 +46,9 @@ import {
StartDateAfterFilter,
StartDateBeforeFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
TargetDateBeforeFilter,
UpdatedAfterFilter,
UpdatedBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -60,7 +64,14 @@ export function PurchaseOrderTable({
supplierPartId?: number;
externalBuildId?: number;
}>) {
const table = useTable('purchase-order');
const table = useTable('purchase-order', {
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
});
const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => {
@ -91,6 +102,8 @@ export function PurchaseOrderTable({
},
CompletedBeforeFilter(),
CompletedAfterFilter(),
UpdatedBeforeFilter(),
UpdatedAfterFilter(),
ProjectCodeFilter(),
HasProjectCodeFilter(),
ResponsibleFilter(),
@ -113,7 +126,8 @@ export function PurchaseOrderTable({
)
},
{
accessor: 'supplier_reference'
accessor: 'supplier_reference',
copyable: true
},
LineItemsProgressColumn({}),
StatusColumn({ model: ModelType.purchaseorder }),
@ -133,6 +147,9 @@ export function PurchaseOrderTable({
CompletionDateColumn({
accessor: 'complete_date'
}),
UpdatedAtColumn({
defaultVisible: false
}),
{
accessor: 'total_price',
title: t`Total Price`,
@ -143,7 +160,8 @@ export function PurchaseOrderTable({
});
}
},
ResponsibleColumn({})
ResponsibleColumn({}),
LinkColumn({})
];
}, []);

View File

@ -27,7 +27,8 @@ export default function SupplierPartParametricTable({
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
sortable: true,
copyable: true
}
];
}, []);

View File

@ -32,6 +32,7 @@ import {
CompanyColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
NoteColumn,
PartColumn
@ -54,7 +55,34 @@ export function SupplierPartTable({
partId?: number;
supplierId?: number;
}>): ReactNode {
const table = useTable('supplierparts');
const initialFilters = useMemo(() => {
const filters: TableFilter[] = [
{
name: 'active',
value: 'true'
}
];
if (!supplierId) {
filters.push({
name: 'supplier_active',
value: 'true'
});
}
if (!partId) {
filters.push({
name: 'part_active',
value: 'true'
});
}
return filters;
}, [supplierId, partId]);
const table = useTable('supplierparts', {
initialFilters: initialFilters
});
const user = useUserState();
@ -65,13 +93,7 @@ export function SupplierPartTable({
switchable: !!partId,
part: 'part_detail'
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
{
accessor: 'supplier',
sortable: true,
@ -82,7 +104,8 @@ export function SupplierPartTable({
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
sortable: true,
copyable: true
},
DescriptionColumn({}),
{
@ -97,7 +120,9 @@ export function SupplierPartTable({
accessor: 'MPN',
sortable: true,
title: t`MPN`,
render: (record: any) => record?.manufacturer_part_detail?.MPN
render: (record: any) => record?.manufacturer_part_detail?.MPN,
copyable: true,
copyAccessor: 'manufacturer_part_detail.MPN'
},
BooleanColumn({
accessor: 'primary',

View File

@ -19,12 +19,14 @@ import {
CreationDateColumn,
DescriptionColumn,
LineItemsProgressColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
StartDateColumn,
StatusColumn,
TargetDateColumn
TargetDateColumn,
UpdatedAtColumn
} from '../ColumnRenderers';
import {
AssignedToMeFilter,
@ -45,7 +47,9 @@ import {
StartDateAfterFilter,
StartDateBeforeFilter,
TargetDateAfterFilter,
TargetDateBeforeFilter
TargetDateBeforeFilter,
UpdatedAfterFilter,
UpdatedBeforeFilter
} from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -56,7 +60,18 @@ export function ReturnOrderTable({
partId?: number;
customerId?: number;
}>) {
const table = useTable(!!partId ? 'returnorders-part' : 'returnorders-index');
const table = useTable(
!!partId ? 'returnorders-part' : 'returnorders-index',
{
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
}
);
const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => {
@ -87,6 +102,8 @@ export function ReturnOrderTable({
},
CompletedBeforeFilter(),
CompletedAfterFilter(),
UpdatedBeforeFilter(),
UpdatedAfterFilter(),
HasProjectCodeFilter(),
ProjectCodeFilter(),
ResponsibleFilter(),
@ -112,7 +129,8 @@ export function ReturnOrderTable({
)
},
{
accessor: 'customer_reference'
accessor: 'customer_reference',
copyable: true
},
DescriptionColumn({}),
LineItemsProgressColumn({}),
@ -133,6 +151,9 @@ export function ReturnOrderTable({
CompletionDateColumn({
accessor: 'complete_date'
}),
UpdatedAtColumn({
defaultVisible: false
}),
ResponsibleColumn({}),
{
accessor: 'total_price',
@ -143,7 +164,8 @@ export function ReturnOrderTable({
currency: record.order_currency || record.customer_detail?.currency
});
}
}
},
LinkColumn({})
];
}, []);

View File

@ -29,6 +29,7 @@ import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import {
DescriptionColumn,
IPNColumn,
LocationColumn,
PartColumn,
ReferenceColumn,
@ -130,13 +131,9 @@ export default function SalesOrderAllocationTable({
accessor: 'part_detail.description',
hidden: showPartInfo != true
}),
{
accessor: 'part_detail.IPN',
title: t`IPN`,
hidden: showPartInfo != true,
sortable: true,
ordering: 'IPN'
},
IPNColumn({
hidden: showPartInfo != true
}),
{
accessor: 'serial',
title: t`Serial Number`,
@ -149,7 +146,9 @@ export default function SalesOrderAllocationTable({
title: t`Batch Code`,
sortable: true,
switchable: true,
render: (record: any) => record?.item_detail?.batch
render: (record: any) => record?.item_detail?.batch,
copyable: true,
copyAccessor: 'item_detail.batch'
},
{
accessor: 'available',

View File

@ -47,8 +47,10 @@ import {
DateColumn,
DecimalColumn,
DescriptionColumn,
IPNColumn,
LinkColumn,
ProjectCodeColumn,
ReferenceColumn,
RenderPartColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
@ -94,21 +96,11 @@ export default function SalesOrderLineItemTable({
);
}
},
{
accessor: 'part_detail.IPN',
title: t`IPN`,
sortable: true,
ordering: 'IPN',
switchable: true
},
IPNColumn({}),
DescriptionColumn({
accessor: 'part_detail.description'
}),
{
accessor: 'reference',
sortable: false,
switchable: true
},
ReferenceColumn({}),
ProjectCodeColumn({}),
DecimalColumn({
accessor: 'quantity',

View File

@ -142,7 +142,8 @@ export default function SalesOrderShipmentTable({
accessor: 'order_detail.reference',
title: t`Sales Order`,
hidden: !showOrderInfo,
sortable: false
sortable: false,
copyable: true
},
StatusColumn({
switchable: true,
@ -155,7 +156,8 @@ export default function SalesOrderShipmentTable({
accessor: 'reference',
title: t`Shipment Reference`,
switchable: false,
sortable: true
sortable: true,
copyable: true
},
{
accessor: 'allocated_items',
@ -193,14 +195,14 @@ export default function SalesOrderShipmentTable({
title: t`Delivery Date`
}),
{
accessor: 'tracking_number'
accessor: 'tracking_number',
copyable: true
},
{
accessor: 'invoice_number'
accessor: 'invoice_number',
copyable: true
},
LinkColumn({
accessor: 'link'
})
LinkColumn({})
];
}, [showOrderInfo]);

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