Merge remote-tracking branch 'origin/master' into report_merge

This commit is contained in:
Tristan Le 2025-04-18 16:30:30 +00:00
commit bd57809f13
485 changed files with 77940 additions and 67293 deletions

View File

@ -34,7 +34,7 @@ RUN apt install -y \
yarn nodejs npm
# Update to the latest stable node version
RUN npm install -g n --ignore-scripts && n lts
RUN npm install -g n@10.1.0 --ignore-scripts && n lts
RUN yarn config set network-timeout 600000 -g

View File

@ -27,7 +27,7 @@ python3 -m pip install --upgrade pip
pip3 install --ignore-installed --upgrade invoke Pillow
# install base level packages
pip3 install -Ur --require-hashes contrib/container/requirements.txt
pip3 install -Ur contrib/container/requirements.txt --require-hashes
# Run initial InvenTree server setup
invoke update -s

View File

@ -64,7 +64,9 @@ runs:
- name: Install Specific Python Dependencies
if: ${{ inputs.pip-dependency }}
shell: bash
run: uv pip install ${{ inputs.pip-dependency }}
run: uv pip install ${PIP_DEPS}
env:
PIP_DEPS: ${{ inputs.pip-dependency }}
# NPM installs
- name: Install node.js ${{ env.node_version }}
@ -78,8 +80,10 @@ runs:
shell: bash
run: |
sudo apt-get update
sudo apt-get install ${{ inputs.apt-dependency }}
sudo apt-get install ${{ inputs.apt-dependency }}
sudo apt-get install ${APT_DEPS}
sudo apt-get install ${APT_DEPS}
env:
APT_DEPS: ${{ inputs.apt-dependency }}
# Invoke commands
- name: Install dev requirements

View File

@ -52,7 +52,6 @@ updates:
- package-ecosystem: npm
directories:
- /src/frontend
- /src/backend
schedule:
interval: weekly
groups:

View File

@ -120,6 +120,7 @@ jobs:
test -f data/config.yaml
test -f data/plugins.txt
test -f data/secret_key.txt
test -f data/oidc.pem
- name: Run Unit Tests
run: |
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env

View File

@ -193,7 +193,6 @@ jobs:
diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
- name: Check schema - including warnings
run: invoke dev.schema
continue-on-error: true
- name: Extract version for publishing
id: version
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
@ -344,7 +343,7 @@ jobs:
- name: Coverage Tests
run: invoke dev.test --coverage --translations
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -487,7 +486,7 @@ jobs:
- name: Run Tests
run: invoke dev.test --migrations --report --coverage --translations
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -619,7 +618,7 @@ jobs:
if: github.event_name != 'pull_request'
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: github.event_name != 'pull_request'
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -682,7 +681,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # pin@v3
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # pin@v3
with:
sarif_file: results.sarif
category: zizmor

View File

@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
with:
sarif_file: results.sarif

3
.gitignore vendored
View File

@ -7,6 +7,7 @@ __pycache__/
.Python
env/
inventree-env/
.venv/
./build/
.cache/
develop-eggs/
@ -18,7 +19,6 @@ share/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@ -57,6 +57,7 @@ data.json
# Key file
secret_key.txt
oidc.pem
# IDE / development files
.idea/

23
.vscode/launch.json vendored
View File

@ -40,6 +40,29 @@
"django": true,
"justMyCode": false
},
{
"name": "InvenTree invoke schema",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/.venv/lib/python3.9/site-packages/invoke/__main__.py",
"cwd": "${workspaceFolder}",
"args": [
"dev.schema","--ignore-warnings"
],
"justMyCode": false
},
{
"name": "schema generation",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": [
"schema",
"--file","src/frontend/schema.yml"
],
"django": true,
"justMyCode": false
},
{
"name": "InvenTree Frontend - Vite",
"type": "chrome",

View File

@ -37,6 +37,7 @@ ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}/src/backend"
# InvenTree configuration files
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
ENV INVENTREE_OIDC_PRIVATE_KEY_FILE="${INVENTREE_DATA_DIR}/oidc.pem"
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt"
# Worker configuration (can be altered by user)
@ -108,7 +109,7 @@ RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
FROM prebuild AS frontend
RUN apk add --no-cache --update nodejs npm yarn bash
RUN npm install -g --ignore-scripts n
RUN npm install -g --ignore-scripts n@10.1.0
RUN bash -c "n lts"
RUN yarn config set network-timeout 600000 -g
COPY src ${INVENTREE_HOME}/src
@ -152,7 +153,7 @@ RUN pip install --require-hashes -r base_requirements.txt --no-cache
# Install nodejs / npm / yarn
RUN apk add --no-cache --update nodejs npm yarn bash
RUN npm install -g --ignore-scripts n
RUN npm install -g --ignore-scripts n@10.1.0
RUN bash -c "n lts"
RUN yarn config set network-timeout 600000 -g

View File

@ -181,6 +181,7 @@ function detect_envs() {
export INVENTREE_PLUGINS_ENABLED=true
export INVENTREE_PLUGIN_FILE=${CONF_DIR}/plugins.txt
export INVENTREE_SECRET_KEY_FILE=${CONF_DIR}/secret_key.txt
export INVENTREE_OIDC_PRIVATE_KEY_FILE=${CONF_DIR}/oidc.pem
export INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE:-sqlite3}
export INVENTREE_DB_NAME=${INVENTREE_DB_NAME:-${DATA_DIR}/database.sqlite3}
@ -339,6 +340,8 @@ function set_env() {
sed -i s=#plugin_file:\ \'/path/to/plugins.txt\'=plugin_file:\ \'${INVENTREE_PLUGIN_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# Secret key file
sed -i s=#secret_key_file:\ \'/etc/inventree/secret_key.txt\'=secret_key_file:\ \'${INVENTREE_SECRET_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# OIDC private key file
sed -i s=#oidc_private_key_file:\ \'/etc/inventree/oidc.pem\'=oidc_private_key_file:\ \'${INVENTREE_OIDC_PRIVATE_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# Debug mode
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE}

View File

@ -33,7 +33,7 @@ invoke dev.schema -help
Users must be authenticated to gain access to the InvenTree API. The API accepts either basic username:password authentication, or token authentication. Token authentication is recommended as it provides much faster API access.
!!! warning "Permissions"
API access is restricted based on the permissions assigned to the user.
API access is restricted based on the permissions assigned to the user or scope of the application.
### Basic Auth
@ -44,9 +44,9 @@ Users can authenticate against the API using basic authentication - specifically
Each user is assigned an authentication token which can be used to access the API. This token is persistent for that user (unless invalidated by an administrator) and can be used across multiple sessions.
!!! info "Token Administration"
User tokens can be created and/or invalidated via the Admin interface.
User tokens can be created and/or invalidated via the user settings, admin center or admin interface.
### Requesting a Token
#### Requesting a Token
If a user does not know their access token, it can be requested via the API interface itself, using a basic authentication request.
@ -66,7 +66,7 @@ HTTP_200_OK
}
```
### Using a Token
#### Using a Token
After reception of a valid authentication token, it can be subsequently used to perform token-based authentication.
@ -95,6 +95,46 @@ headers = {
response = request.get('http://localhost:8080/api/part/', data=data, headers=headers)
```
### oAuth2 / OIDC
!!! warning "Experimental"
This is an experimental feature that needs to be specifically enabled. See [Experimental features](../settings/experimental.md) for more information.
InvenTree has built-in support for using [oAuth2](https://oauth.net/2/) and OpenID Connect (OIDC) for authentication to the API. This enables using the instance as a very limited identity provider.
A default application using a public client with PKCE enabled ships with each instance. Intended to be used with the python api and configured with very wide scopes this can also be used for quick tests - the cliend_id is `zDFnsiRheJIOKNx6aCQ0quBxECg1QBHtVFDPloJ6`.
#### Managing applications
Superusers can register new applications and manage existing ones using a small application under the subpath `/o/applications/`.
It is recommended to:
- read the spec (RFC 6749 / 6750) and/or best practices (RFC 9700) before choosing client types
- chose scopes as narrow as possible
- configure redirection URIs as exact as possible
#### Scopes
InvenTree's oAuth scopes are strongly related to the [user roles](#user-roles).
Names consist of 1. type, 2. kind and 3. (opt) role, separated by colons.
There are 3 types:
- a: administrative scopes - used for administrating the server - these can be staff or superuser scopes
- g: general scopes - give wide access to the basic building blocks of InvenTree
- r: role scopes - map to specific actions (2) and roles (3)
Examples:
```bash
a:superuser
g:read
r:change:part
r:delete:stock
```
!!! info "Read the API docs"
The API [documentation](#documentation) and [schema](./schema.md) list the required scopes for every API endpoint / interaction in the security sections.
## Authorization
### User Roles

View File

@ -4,7 +4,7 @@ title: Pricing Support
## Pricing
Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being proscriptive.
Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being prescriptive.
InvenTree provides support for multiple currencies, allowing pricing information to be stored with base currency rates.

View File

@ -18,7 +18,8 @@ If you add a lot of code (over ~1000 LOC) maybe split it into multiple plugins t
Great. Now please read the [plugin documentation](./plugins.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings.
### Pick your building blocks
Consider the usecase for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
Consider the use-case for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
- Is it just a simple REST-endpoint that runs a function ([ActionMixin](./plugins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./plugins/barcode.md))?
- How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./plugins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./plugins/navigation.md)).
@ -30,7 +31,7 @@ Consider the usecase for your plugin and define the exact function of the plugin
- Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./plugins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance.
### Define the metadata
Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a weblink so users can file issues / reach you.
Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a web link so users can file issues / reach you.
### Development guidelines
If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case.
@ -139,9 +140,7 @@ from plugin.mixins import ActionMixin
class SampleActionPlugin(ActionMixin, InvenTreePlugin):
"""
Use docstrings for everything... pls
"""
"""Use docstrings for everything."""
NAME = "SampleActionPlugin"
ACTION_NAME = "sample"

View File

@ -4,40 +4,8 @@ title: Third Party Integrations
## Third Party Integrations
A list of known third-party InvenTree extensions is provided below. If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/).
A list of known third-party InvenTree extensions is provided [on our website](https://inventree.org/extend/integrate/) If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/).
### Ki-nTree
## Available Plugins
[Ki-nTree](https://github.com/sparkmicro/Ki-nTree/) is a fantastic tool for automated creation of [KiCad](https://www.kicad.org/) library parts, with direct integration with InvenTree.
### PK2InvenTree
[PK2InvenTree](https://github.com/rgilham/PK2InvenTree) is an open-source tool for migrating an existing [PartKeepr](https://github.com/partkeepr/PartKeepr) database to InvenTree.
### Digikey-Inventree-Integration
[Digikey-Inventree-Integration](https://github.com/EUdds/Digikey-Inventree-Integration) is a simple project that takes a digikey part number to creates a part in InvenTree.
### F360-InvenTree
[F360-InvenTree](https://github.com/matmair/F360-InvenTree/) is a tool for creating links between Autodesk Fusion 360 components and InvenTree parts.
Still under heavy development.
### DigitalOcean droplet
[InvenTree droplet](https://inventree.org/digitalocean) is a 1-click solution to deploy InvenTree in the cloud with DigitalOcean. You still have to administer and update your instance.
The source code for this droplet can be found in [inventree_droplet](https://github.com/invenhost/inventree_droplet).
### InvenTree zebra plugin
[InvenTree zebra plugin](https://github.com/SergeoLacruz/inventree-zebra-plugin) is a plugin to print labels with zebra printers.
Currently only the GK420T printer is supported.
### InvenTree Apprise
[InvenTree Apprise](https://github.com/matmair/inventree-apprise) is a plugin to send notifications via Apprise. This enables a wide variety of targets.
## First party plugins
### InvenTree brother plugin
[InvenTree brother plugin](https://github.com/inventree/inventree-brother-plugin) is a plugin to print labels with brother Q series printers.
Refer to the [InvenTree website](https://inventree.org/plugins.html) for a (non exhaustive) list of plugins that are available for InvenTree. This includes both official and third-party plugins.

View File

@ -4,7 +4,7 @@ title: Plugins
## InvenTree Plugin Architecture
The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviours which are decoupled from core InvenTree code.
The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviors which are decoupled from core InvenTree code.
Plugins can be added from multiple sources:
@ -35,8 +35,8 @@ create-inventree-plugin
Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
!!! warning "Namechange"
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. While the old name is still available till `0.8.0` we strongly suggest upgrading your plugins. Deprecation warnings are raised if the old name is used.
!!! warning "Name Change"
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`.
### Imports
@ -66,7 +66,7 @@ Mixins are split up internally to keep the source tree clean and enable better t
#### Models and other internal InvenTree APIs
!!! warning "Danger Zone"
The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `ÌnvenTree` org if you are not sure you are using something the intended way.
The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `InvenTree` org if you are not sure you are using something the intended way.
We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases.
If you need to react to state changes please use the [EventMixin](./plugins/event.md).
@ -121,6 +121,7 @@ Supported mixin classes are:
| [ReportMixin](./plugins/report.md) | Add custom context data to reports |
| [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks |
| [SettingsMixin](./plugins/settings.md) | Integrate user configurable settings |
| [UserInterfaceMixin](./plugins/ui.md) | Add custom user interface features |
| [UrlsMixin](./plugins/urls.md) | Respond to custom URL endpoints |
| [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models |

View File

@ -7,4 +7,4 @@ title: App Mixin
If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration.
!!! warning "Danger Zone"
Only use this mixin if you have an understanding of djangos [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of django to make sure your code does not cause unwanted behaviour in InvenTree before releasing.
Only use this mixin if you have an understanding of Django's [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of Django to make sure your code does not cause unwanted behaviour in InvenTree before releasing.

View File

@ -5,7 +5,7 @@ title: Navigation Mixin
## NavigationMixin
Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header.
The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon (InvenTree ships with fontawesome 4 by default).
The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon.
``` python
class MyNavigationPlugin(NavigationMixin, InvenTreePlugin):

View File

@ -217,3 +217,47 @@ We are working to develop and distribute a library of custom InvenTree component
### Examples
Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration.
## Building a User Interface Plugin
The technology stack which allows custom plugins to hook into the InvenTree user interface utilizes the following components:
- [React](https://react.dev)
- [Mantine](https://mantine.dev)
- [TypeScript](https://www.typescriptlang.org/)
- [Vite](https://vitejs.dev/)
While you don't need to be an expert in all of these technologies, it is recommended that you have a basic understanding of how they work together to build the InvenTree user interface. To get started, you should familiarize yourself with the frontend code (at `./src/frontend/`) as well as the vite configuration for the [InvenTree plugin creator](httsps://github.com/inventree/plugin-creator).
### Bundled with InvenTree
If a plugin is bundled with a separate copy of React libraries, issues may arise either due to version mismatches or because the React context is not shared between the InvenTree core and the plugin. This can lead to issues with rendering components, as the React context may not be shared between the two libraries.
To avoid issues, the InvenTree UI provides globally accessible components, which can be used as external modules by the plugin. This allows the plugin to use the same React context as the InvenTree core, and ensures that the plugin is compatible with the InvenTree user interface.
The following modules are provided as global objects at runtime:
- `React`
- `ReactDOM`
- `ReactDOMClient`
Additionally, for the Mantine library, the following modules are provided as global objects at runtime:
- `@mantine/core`
- `@mantine/hooks`
- `@mantine/notifications`
To use these modules in your plugin, they must be correctly *externalized* in the Vite configuration. Getting this right is crucial to ensure that the plugin is compatible with the InvenTree user interface. The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides a good starting point for this configuration, and can be used to generate a new plugin with the correct configuration.
!!! info "Bundled Version"
Keep in mind that the version of React and Mantine used in the InvenTree core may differ from the version used in your plugin. It is recommended to use the same version as the InvenTree core to avoid compatibility issues.
### Plugin Creator
The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides an out-of-the-box setup for creating InvenTree plugins which integrate into the user interface. This includes a pre-configured Vite setup, which allows you to quickly get started with building your own custom UI plugins.
Using the plugin creator tool is the recommended way to get started with building custom UI plugins for InvenTree, as it provides a solid foundation to build upon. It is also the only method which is officially supported by the InvenTree development team!
### DIY
Of course, you can also build your own custom UI plugins from scratch. This is a more advanced option, and requires a good understanding of the InvenTree codebase, as well as the technologies used to build the user interface. You are free to use other web technologies, however if you choose to do this, don't expect any support from the InvenTree development team. We will only provide support for plugins which are built using the recommended stack, and which follow the guidelines outlined in this documentation.

View File

@ -69,7 +69,7 @@ from plugin.mixins import ValidationMixin
import part.models
class MyValidationMixin(Validationixin, InvenTreePlugin):
class MyValidationMixin(ValidationMixin, InvenTreePlugin):
"""Custom validation plugin."""
def validate_model_instance(self, instance, deltas=None):

View File

@ -0,0 +1,21 @@
---
title: Resources InvenTree receives
---
The InvenTree project is grateful to receive resources from various vendors free of charge.
Individuals and companies can also support via [GitHub sponsors](https://github.com/sponsors/inventree).
## Current supporters
- [DigitalOcean](https://inventree.org/digitalocean) - Cloud hosting provider, used to host the InvenTree demo instance
- [Crowdin](https://crowdin.com/) - Translation platform, used to manage the [InvenTree translations](../develop/contributing.md#translations) across backend, frontend and app
- [SonarQube Cloud](https://sonarcloud.io/) - Code quality and security analysis, used to track the code quality of the various components
- [Packager.io](https://packager.io/) - Linux package builder/hosting service, used to host the InvenTree debian packages
- [Codecov](https://codecov.io) - Code coverage as a service, used to track the code coverage of the various components
- [Netlify](https://www.netlify.com/) - Static site hosting provider, used to test deploy the frontend and website
- [Depot](https://depot.dev/?utm_source=inventree) - Docker build accelerator, used to build the multi-arch images for the InvenTree docker image
## Past supporters
Non cromprehensive list of past supporters. The project stops consuming resources for various reasons, this does not mean they are not good resources.
- [Coveralls](https://coveralls.io/) - Code coverage as a service
- [Deepsource](https://deepsource.io/) - Code quality and security analysis

View File

@ -32,6 +32,18 @@ As the `django.db.models.QuerySet` is not a generic class, we would loose type i
Models that implement the `InvenTreeReportMixin` must have an explicit return type annotation for the `report_context` function.
#### INVE-E5
**Rulesets have issues - Backend**
The rulesets used for managing user/group/oAuth permissions have an issue.
This might be caused by an addition or removal of models to the code base. Running the test suit should surface more logs with the error code indicating the exact infractions.
#### INVE-E6
**Scopes have issues - Backend**
The scopes used for oAuth permissions have an issue and do not match the rulesets.
This might be caused by an addition or removal of models to the code base or changes to the rulesets. Running the test suit should surface more logs with the error code indicating the exact infractions.
### INVE-W (InvenTree Warning)
Warnings - These are non-critical errors which should be addressed when possible.
@ -52,6 +64,40 @@ See [INVE-W1](#inve-w1)
See [INVE-W1](#inve-w1)
#### INVE-W4
**Server is running in debug mode - Backend**
InvenTree is running in debug mode. This is **not** recommended for production use, as it exposes sensitive information and makes the server more vulnerable to attacks. Debug mode is not intended for production/exposed instances, **even for short duration**.
It is recommended to run InvenTree in production mode for better security and performance. See [Debug Mode Information](../start/intro.md#debug-mode).
#### INVE-W5
**Background worker process not running - Backend**
The background worker seems to not be running. This is detected by a heartbeat that runs all 5 minutes - this error triggers after not being run in the last 10 minutes.
Check if the process for background workers is running and reaching the database. Steps vary between deployment methods.
See [Background Worker Information](../start/processes.md#background-worker).
#### INVE-W6
**Server restart required - Backend**
The server needs a restart due to changes in settings. Steps very between deployment methods.
#### INVE-W7
**Email settings not configured - Backend**
Not all required settings for sending emails are configured. Not having an email provider configured might lead to degraded processes as password reset, update notifications and user notifications can not work. Setting up email is recommended.
See [Email information](../start/config.md#email-settings).
#### INVE-W8
**Database Migrations required - Backend**
There are database migrations waiting to be applied. This might lead to integrity and availability issues. Applying migrations as soon as possible is recommended.
Some deployment methods support [auto applying of updates](../start/config.md#auto-update). See also [Perform Database Migrations](../start/install.md#perform-database-migrations).
Steps very between deployment methods.
### INVE-I (InvenTree Information)
Information — These are not errors but information messages. They might point out potential issues or just provide information.

View File

@ -0,0 +1,17 @@
---
title: Experimental Features
---
## Feature Flags
InvenTree ships with django-flags and enables path (parameter), user, session, date or settings based feature flags. This allows admins to slowly test and roll out new features on their instance without running parallel instances.
Additional flags can be provided via the the `INVENTREE_FLAGS` environment key (see [configuration](../start/config.md#environment-variables)).
Superusers can configure run-time conditions [as per django-flags](https://cfpb.github.io/django-flags/conditions/) docs under `/admin/flags/flagstate/`.
## Current Experimental Features
| Feature | Key | Description |
| ------- | ----------- |
| oAuth provider / api | OIDC | Use oAuth and OIDC to authenticate users with the API - [read more](../api/api.md#oauth2--oidc). |

View File

@ -38,6 +38,7 @@ Customize settings for search results:
| ---- | ----------- | ------- | ----- |
{{ usersetting("SEARCH_WHOLE") }}
{{ usersetting("SEARCH_REGEX") }}
{{ usersetting("SEARCH_NOTES") }}
{{ usersetting("SEARCH_PREVIEW_RESULTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }}
{{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }}

View File

@ -218,31 +218,35 @@ You can either specify the password directly using `INVENTREE_ADMIN_PASSWORD`, o
!!! info "Administrator Account"
Providing `INVENTREE_ADMIN` credentials will result in the provided account being created with *superuser* permissions when InvenTree is started.
## Secret Key
## Secret Key material
InvenTree requires a secret key for providing cryptographic signing - this should be a secret (and unpredictable) value.
InvenTree requires secret keys for providing cryptographic signing and oidc private keys- this should be a secret (and unpredictable) value.
!!! info "Auto-Generated Key"
If none of the following options are specified, InvenTree will automatically generate a secret key file (stored in `secret_key.txt`) on first run.
!!! info "Auto-Generated material"
If none of the following options are specified, InvenTree will automatically generate a secret key file (stored in `secret_key.txt`) and a oidc key file (stored in `oidc.pem`) on first run.
The secret key can be provided in multiple ways, with the following (descending) priorities:
The secret key material can be provided in multiple ways, with the following (descending) priorities:
**Pass Secret Key via Environment Variable**
**Pass Secret Key Material via Environment Variable**
A secret key string can be passed directly using the environment variable `INVENTREE_SECRET_KEY`
A oidc private key can be passed directly using the environment variable `INVENTREE_OIDC_PRIVATE_KEY`
**Pass Secret Key File via Environment Variable**
**Pass Secret Key Material File via Environment Variable**
A file containing the secret key can be passed via the environment variable `INVENTREE_SECRET_KEY_FILE`
A PEM-encoded file containing the oidc private key can be passed via the environment variable `INVENTREE_OIDC_PRIVATE_KEY_FILE`
**Fallback to Default Secret Key File**
**Fallback to Default Secret Key Material**
If not specified via environment variables, the fallback secret_key file (automatically generated as part of InvenTree installation) will be used.
If not specified via environment variables, the fallback files (automatically generated as part of InvenTree installation) will be used.
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_SECRET_KEY | secret_key | Raw secret key value | *Not specified* |
| INVENTREE_SECRET_KEY_FILE | secret_key_file | File containing secret key value | *Not specified* |
| INVENTREE_OIDC_PRIVATE_KEY | oidc_private_key | Raw private key value | *Not specified* |
| INVENTREE_OIDC_PRIVATE_KEY_FILE | oidc_private_key_file | File containing private key value in PEM format | *Not specified* |
## Database Options

View File

@ -125,9 +125,9 @@ Running in DEBUG mode provides many handy development features, however it is st
So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
### Potential Issues
### Turning Debug Mode off
Turning off DEBUG mode creates further work for the system administrator. In particular, when running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the InvenTree server can no longer run as a monolithic process.
When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the proxy setup has to be configured to handle this.
!!! info "Read More"
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details

View File

@ -126,17 +126,18 @@ def define_env(env):
"""Define custom environment variables for the documentation build process."""
@env.macro
def sourcedir(dirname, branch=None):
def sourcedir(dirname: str, branch=None):
"""Return a link to a directory within the source code repository.
Arguments:
- dirname: The name of the directory to link to (relative to the top-level directory)
dirname: The name of the directory to link to (relative to the top-level directory)
branch: The branch of the repository to link to (defaults to the current build environment)
Returns:
- A fully qualified URL to the source code directory on GitHub
A fully qualified URL to the source code directory on GitHub
Raises:
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid
FileNotFoundError: If the directory does not exist, or the generated URL is invalid
"""
if branch == None:
branch = get_build_environment()
@ -169,13 +170,15 @@ def define_env(env):
"""Return a link to a file within the source code repository.
Arguments:
- filename: The name of the file to link to (relative to the top-level directory)
filename: The name of the file to link to (relative to the top-level directory)
branch: The branch of the repository to link to (defaults to the current build environment)
raw: If True, return the raw URL to the file (defaults to False)
Returns:
- A fully qualified URL to the source code file on GitHub
A fully qualified URL to the source code file on GitHub
Raises:
- FileNotFoundError: If the file does not exist, or the generated URL is invalid
FileNotFoundError: If the file does not exist, or the generated URL is invalid
"""
if branch == None:
branch = get_build_environment()
@ -247,9 +250,9 @@ def define_env(env):
"""Include a file in the documentation, in a 'collapse' block.
Arguments:
- filename: The name of the file to include (relative to the top-level directory)
- title:
- fmt:
filename: The name of the file to include (relative to the top-level directory)
title: The title of the collapse block in the documentation
fmt: The format of the included file (e.g., 'python', 'html', etc.)
"""
here = os.path.dirname(__file__)
path = os.path.join(here, '..', filename)
@ -299,7 +302,7 @@ def define_env(env):
"""Extract information on a particular global setting.
Arguments:
- key: The name of the global setting to extract information for.
key: The name of the global setting to extract information for.
"""
global GLOBAL_SETTINGS
setting = GLOBAL_SETTINGS[key]
@ -311,7 +314,7 @@ def define_env(env):
"""Extract information on a particular user setting.
Arguments:
- key: The name of the user setting to extract information for.
key: The name of the user setting to extract information for.
"""
global USER_SETTINGS
setting = USER_SETTINGS[key]

View File

@ -89,6 +89,7 @@ nav:
- Project:
- Governance: project/governance.md
- Project Security: project/security.md
- Resources: project/resources.md
- Security: security.md
- Install:
- Introduction: start/intro.md
@ -160,6 +161,7 @@ nav:
- Single Sign on: settings/SSO.md
- Multi Factor Authentication: settings/MFA.md
- Email: settings/email.md
- Experimental Features: settings/experimental.md
- Export Data: settings/export.md
- Import Data: settings/import.md
- Operations:

View File

@ -5,4 +5,4 @@ mkdocs-git-revision-date-localized-plugin>=1.1,<2.0
mkdocs-simple-hooks>=0.1,<1.0
mkdocs-include-markdown-plugin
neoteroi-mkdocs
mkdocstrings[python]>=0.25.0,<=0.29.0
mkdocstrings[python]>=0.25.0,<=0.29.1

View File

@ -331,9 +331,9 @@ mkdocs-macros-plugin==1.3.7 \
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
# via -r docs/requirements.in
mkdocs-material==9.6.9 \
--hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \
--hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c
mkdocs-material==9.6.11 \
--hash=sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff \
--hash=sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
@ -343,9 +343,9 @@ mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in
mkdocstrings[python]==0.29.0 \
--hash=sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e \
--hash=sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d
mkdocstrings[python]==0.29.1 \
--hash=sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6 \
--hash=sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42
# via
# -r docs/requirements.in
# mkdocstrings-python
@ -497,7 +497,6 @@ typing-extensions==4.12.2 \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# anyio
# mkdocstrings
# mkdocstrings-python
# rich
urllib3==2.3.0 \

View File

@ -18,11 +18,16 @@ src = ["src/backend/InvenTree"]
"__init__.py" = ["D104"]
[tool.ruff.lint]
select = ["A", "B", "C", "C4", "D", "F", "I", "N", "SIM", "PIE", "PLE", "PLW", "RUF", "UP", "W"]
select = ["A", "B", "C", "C4", "D", "F", "I", "N", "SIM", "PIE", "PLE", "PLW", "RUF", "UP", "W",
#"DOC201", "DOC202", # enforce return docs
"DOC402","DOC403", # enforce yield docs
#"DOC501","DOC502", # enforce raise
]
# Things that should be enabled in the future:
# - LOG
# - DJ # for Django stuff
# - S # for security stuff (bandit)
# - D401 - Imperative docstrings
ignore = [
"PLE1205",
@ -51,8 +56,6 @@ ignore = [
"N806",
# - N812 - lowercase imported as non-lowercase
"N812",
# - D417 Missing argument descriptions in the docstring
"D417",
# - RUF032 - decimal-from-float-literal
"RUF032",
"RUF045",
@ -69,6 +72,7 @@ ignore = [
[tool.ruff.lint.pydocstyle]
convention = "google"
ignore-var-parameters = true
[tool.ruff.lint.isort]
split-on-trailing-comma = false

View File

@ -1,7 +1,6 @@
"""Main JSON interface views."""
import json
import sys
from pathlib import Path
from django.conf import settings
@ -11,24 +10,22 @@ from django.utils.translation import gettext_lazy as _
import structlog
from django_q.models import OrmQ
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions, serializers
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import serializers
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
import common.models
import InvenTree.version
import users.models
from common.settings import get_global_setting
from InvenTree import helpers, ready
from InvenTree import helpers
from InvenTree.auth_overrides import registration_enabled
from InvenTree.mixins import ListCreateAPI
from InvenTree.sso import sso_registration_enabled
from part.models import Part
from plugin.serializers import MetadataSerializer
from users.models import ApiToken
from users.permissions import check_user_permission
from .helpers import plugins_info
from .helpers_email import is_email_configured
@ -81,8 +78,8 @@ def read_license_file(path: Path) -> list:
class LicenseViewSerializer(serializers.Serializer):
"""Serializer for license information."""
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.CharField(
backend = serializers.ListField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.ListField(
help_text='Frontend licenses texts', read_only=True
)
@ -90,7 +87,7 @@ class LicenseViewSerializer(serializers.Serializer):
class LicenseView(APIView):
"""Simple JSON endpoint for InvenTree license information."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
def get(self, request, *args, **kwargs):
@ -115,7 +112,7 @@ class VersionViewSerializer(serializers.Serializer):
api = serializers.IntegerField()
commit_hash = serializers.CharField()
commit_date = serializers.CharField()
commit_branch = serializers.CharField()
commit_branch = serializers.CharField(allow_null=True)
python = serializers.CharField()
django = serializers.CharField()
@ -124,7 +121,6 @@ class VersionViewSerializer(serializers.Serializer):
doc = serializers.URLField()
code = serializers.URLField()
credit = serializers.URLField()
app = serializers.URLField()
bug = serializers.URLField()
@ -137,7 +133,7 @@ class VersionViewSerializer(serializers.Serializer):
class VersionView(APIView):
"""Simple JSON endpoint for InvenTree version information."""
permission_classes = [permissions.IsAdminUser]
permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
@extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)})
def get(self, request, *args, **kwargs):
@ -157,7 +153,6 @@ class VersionView(APIView):
'links': {
'doc': InvenTree.version.inventreeDocUrl(),
'code': InvenTree.version.inventreeGithubUrl(),
'credit': InvenTree.version.inventreeCreditsUrl(),
'app': InvenTree.version.inventreeAppUrl(),
'bug': f'{InvenTree.version.inventreeGithubUrl()}issues',
},
@ -168,9 +163,9 @@ class VersionInformationSerializer(serializers.Serializer):
"""Serializer for a single version."""
version = serializers.CharField()
date = serializers.CharField()
gh = serializers.CharField()
text = serializers.CharField()
date = serializers.DateField()
gh = serializers.CharField(allow_null=True)
text = serializers.ListField(child=serializers.CharField())
latest = serializers.BooleanField()
class Meta:
@ -179,23 +174,44 @@ class VersionInformationSerializer(serializers.Serializer):
fields = '__all__'
class VersionApiSerializer(serializers.Serializer):
"""Serializer for the version api endpoint."""
VersionInformationSerializer(many=True)
@extend_schema(
parameters=[
OpenApiParameter(
name='versions',
type=int,
description='Number of versions to return.',
default=10,
),
OpenApiParameter(
name='start_version',
type=int,
description='First version to report. Defaults to return the latest {versions} versions.',
),
]
)
class VersionTextView(ListAPI):
"""Simple JSON endpoint for InvenTree version text."""
serializer_class = VersionInformationSerializer
permission_classes = [permissions.IsAdminUser]
permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
# Specifically disable pagination for this view
pagination_class = None
@extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)})
def list(self, request, *args, **kwargs):
"""Return information about the InvenTree server."""
return JsonResponse(inventreeApiText())
versions = request.query_params.get('versions')
start_version = request.query_params.get('start_version')
api_kwargs = {}
if versions is not None:
api_kwargs['versions'] = int(versions)
if start_version is not None:
api_kwargs['start_version'] = int(start_version)
version_data = inventreeApiText(**api_kwargs)
return JsonResponse(list(version_data.values()), safe=False)
class InfoApiSerializer(serializers.Serializer):
@ -249,7 +265,7 @@ class InfoView(APIView):
Use to confirm that the server is running, etc.
"""
permission_classes = [permissions.AllowAny]
permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
def worker_pending_tasks(self):
"""Return the current number of outstanding background tasks."""
@ -332,7 +348,7 @@ class InfoView(APIView):
class NotFoundView(APIView):
"""Simple JSON view when accessing an invalid API view."""
permission_classes = [permissions.AllowAny]
permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
def not_found(self, request):
"""Return a 404 error."""
@ -582,6 +598,7 @@ class APISearchViewSerializer(serializers.Serializer):
search = serializers.CharField()
search_regex = serializers.BooleanField(default=False, required=False)
search_whole = serializers.BooleanField(default=False, required=False)
search_notes = serializers.BooleanField(default=False, required=False)
limit = serializers.IntegerField(default=1, required=False)
offset = serializers.IntegerField(default=0, required=False)
@ -595,7 +612,7 @@ class APISearchView(GenericAPIView):
Is much more efficient and simplifies code!
"""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = APISearchViewSerializer
def get_result_types(self):
@ -643,6 +660,7 @@ class APISearchView(GenericAPIView):
'search': '',
'search_regex': False,
'search_whole': False,
'search_notes': False,
'limit': 1,
'offset': 0,
}
@ -681,14 +699,9 @@ class APISearchView(GenericAPIView):
# Check permissions and update results dict with particular query
model = view.serializer_class.Meta.model
app_label = model._meta.app_label
model_name = model._meta.model_name
table = f'{app_label}_{model_name}'
try:
if users.models.RuleSet.check_table_permission(
request.user, table, 'view'
):
if check_user_permission(request.user, model, 'view'):
results[key] = view.list(request, *args, **kwargs).data
else:
results[key] = {
@ -705,37 +718,32 @@ class APISearchView(GenericAPIView):
class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model."""
MODEL_REF = 'model'
def get_model_type(self):
"""Return the model type associated with this API instance."""
model = self.kwargs.get(self.MODEL_REF, None)
if ready.isGeneratingSchema():
model = common.models.ProjectCode
if 'lookup_field' in self.kwargs:
# Set custom lookup field (instead of default 'pk' value) if supplied
self.lookup_field = self.kwargs.pop('lookup_field')
model = None # Placeholder for the model class
@classmethod
def as_view(cls, model, lookup_field=None, **initkwargs):
"""Override to ensure model specific rendering."""
if model is None:
raise ValidationError(
f"MetadataView called without '{self.MODEL_REF}' parameter"
"MetadataView defined without 'model' arg"
) # pragma: no cover
initkwargs['model'] = model
return model
# Set custom lookup field (instead of default 'pk' value) if supplied
if lookup_field:
initkwargs['lookup_field'] = lookup_field
return super().as_view(**initkwargs)
def get_permission_model(self):
"""Return the 'permission' model associated with this view."""
return self.get_model_type()
return self.model
def get_queryset(self):
"""Return the queryset for this endpoint."""
return self.get_model_type().objects.all()
return self.model.objects.all()
def get_serializer(self, *args, **kwargs):
"""Return MetadataSerializer instance."""
# Detect if we are currently generating the OpenAPI schema
if 'spectacular' in sys.argv:
return MetadataSerializer(Part, *args, **kwargs) # pragma: no cover
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
return MetadataSerializer(self.model, *args, **kwargs)

View File

@ -1,581 +1,607 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 331
INVENTREE_API_VERSION = 338
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v331 - 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437
v338 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9333
- Adds oAuth2 support for the API
v337 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9505
- Adds API endpoint with extra serial number information for a given StockItem object
v336 -> 2025-04-10 : https://github.com/inventree/InvenTree/pull/9492
- Fixed query and response serialization for units_all and version_text
- Fixed LicenseView and VersionInformation serialization
v335 -> 2025-04-09 : https://github.com/inventree/InvenTree/pull/9476
- Adds "roles" detail to the Group API endpoint
- Adds "users" detail to the Group API endpoint
- Adds "groups" detail to the User API endpoint
v334 -> 2025-04-08 : https://github.com/inventree/InvenTree/pull/9453
- Fixes various operationId and enum collisions and help texts
v333 -> 2025-04-03 : https://github.com/inventree/InvenTree/pull/9452
- Currency string is no longer restricted to a hardcoded enum
- Customizable status keys are no longer hardcoded enum values
v332 -> 2025-04-02 : https://github.com/inventree/InvenTree/pull/9393
- Adds 'search_notes' parameter to all searchable API endpoints
v331 -> 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437
- Set correct types on various formerly-string PK fields as well permissions
- Include metadata request and response types
v330 - 2025-03-31 : https://github.com/inventree/InvenTree/pull/9420
v330 -> 2025-03-31 : https://github.com/inventree/InvenTree/pull/9420
- Deconflict operation id between single and bulk destroy operations
- Add request body definition for bulk destroy operations
v329 - 2025-03-30 : https://github.com/inventree/InvenTree/pull/9399
v329 -> 2025-03-30 : https://github.com/inventree/InvenTree/pull/9399
- Convert url path regex-specified PKs to int
v228 - 2025-03-29 : https://github.com/inventree/InvenTree/pull/9407
v328 -> 2025-03-29 : https://github.com/inventree/InvenTree/pull/9407
- Updates schema to include paging arguments
v327 - 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339
v327 -> 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339
- Adds "is_mandatory" field to the Plugin API
- Adds ability to filter by "mandatory" status in the Plugin API
v326 - 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
v326 -> 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
- Overhaul the data-export API functionality
- Allow customization of data exporting via plugins
- Consolidate LabelOutput and ReportOutput API endpoints into single DataOutput endpoint
v325 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/9244
v325 -> 2024-03-17 : https://github.com/inventree/InvenTree/pull/9244
- Adds the option for superusers to list all user tokens
- Make list endpoints sortable, filterable and searchable
v324 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320
v324 -> 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320
- Adds BulkUpdate support for the SalesOrderAllocation model
- Adds BulkUpdate support for the PartCategory model
- Adds BulkUpdate support for the StockLocation model
v323 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313
v323 -> 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313
- Adds BulkUpdate support to the Part API endpoint
- Remove legacy API endpoint to set part category for multiple parts
v322 - 2025-03-16 : https://github.com/inventree/InvenTree/pull/8933
v322 -> 2025-03-16 : https://github.com/inventree/InvenTree/pull/8933
- Add min_date and max_date query filters for orders, for use in calendar views
v321 - 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236
v321 -> 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236
- Adds conditionally-returned fields to the schema to match API behavior
- Removes required flag for nullable read-only fields to match API behavior
v320 - 2025-03-05 : https://github.com/inventree/InvenTree/pull/9243
v320 -> 2025-03-05 : https://github.com/inventree/InvenTree/pull/9243
- Link fields are now up to 2000 chars long
v319 - 2025-03-04 : https://github.com/inventree/InvenTree/pull/9199
v319 -> 2025-03-04 : https://github.com/inventree/InvenTree/pull/9199
- Add detail API endpoint for the LabelOutput model
- Add detail API endpoint for the ReportOutput model
v318 - 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116
v318 -> 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116
- Adds user profile API endpoints
v317 - 2025-02-26 : https://github.com/inventree/InvenTree/pull/9143
v317 -> 2025-02-26 : https://github.com/inventree/InvenTree/pull/9143
- Default 'overdue' field to False in Build serializer
- Add allow_null to various fields in Build, Settings, Order, Part, and Stock serializers
- Add type hints to Users model to properly type fields
v316 - 2025-02-26 : https://github.com/inventree/InvenTree/pull/9185
v316 -> 2025-02-26 : https://github.com/inventree/InvenTree/pull/9185
- Allow 'icon' field to be nullified in the PartCategory API
- Allow 'custom_icon' field to be nullified in the StockLocation API
v315 - 2025-02-22 : https://github.com/inventree/InvenTree/pull/9150
v315 -> 2025-02-22 : https://github.com/inventree/InvenTree/pull/9150
- Remove outdated 'url' field from some API endpoints
v314 - 2025-02-17 : https://github.com/inventree/InvenTree/pull/6293
v314 -> 2025-02-17 : https://github.com/inventree/InvenTree/pull/6293
- Removes a considerable amount of old auth endpoints
- Introduces allauth-provided auth endpoints
v313 - 2025-02-17 : https://github.com/inventree/InvenTree/pull/9087
v313 -> 2025-02-17 : https://github.com/inventree/InvenTree/pull/9087
- Adds instance id optionally to the info view endpoint
v312 - 2025-02-15 : https://github.com/inventree/InvenTree/pull/9079
v312 -> 2025-02-15 : https://github.com/inventree/InvenTree/pull/9079
- Remove old API endpoints associated with legacy BOM import functionality
v311 - 2025-02-14 : https://github.com/inventree/InvenTree/pull/9076
v311 -> 2025-02-14 : https://github.com/inventree/InvenTree/pull/9076
- Adds "model_filters" attribute to settings API
v310 - 2025-02-14 : https://github.com/inventree/InvenTree/pull/9077
v310 -> 2025-02-14 : https://github.com/inventree/InvenTree/pull/9077
- Adds 'is_variant' filter to the Part list API
v309 - 2025-02-02 : https://github.com/inventree/InvenTree/pull/9008
v309 -> 2025-02-02 : https://github.com/inventree/InvenTree/pull/9008
- Bug fixes for the "Part" serializer
- Fixes for data import API endpoints
v308 - 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003
v308 -> 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003
- Adds extra detail to the ReportOutput and LabelOutput API endpoints
- Allows ordering of output list endpoints
v307 - 2025-01-29 : https://github.com/inventree/InvenTree/pull/8969
v307 -> 2025-01-29 : https://github.com/inventree/InvenTree/pull/8969
- Extend Info Endpoint to include customizations
v306 - 2025-01-28 : https://github.com/inventree/InvenTree/pull/8966
v306 -> 2025-01-28 : https://github.com/inventree/InvenTree/pull/8966
- Adds "start_date" to PurchasesOrder API
- Adds "start_date" to SalesOrder API
- Adds "start_date" to ReturnOrder API
- Updated API filters
v305 - 2025-01-26 : https://github.com/inventree/InvenTree/pull/8950
v305 -> 2025-01-26 : https://github.com/inventree/InvenTree/pull/8950
- Bug fixes for the SupplierPart API
- Refactoring for data export via API
v304 - 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
v304 -> 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
- Adds "category" filter to build list API
v303 - 2025-01-20 : https://github.com/inventree/InvenTree/pull/8915
v303 -> 2025-01-20 : https://github.com/inventree/InvenTree/pull/8915
- Adds "start_date" field to Build model and API endpoints
- Adds additional API filtering and sorting options for Build list
v302 - 2025-01-18 : https://github.com/inventree/InvenTree/pull/8905
v302 -> 2025-01-18 : https://github.com/inventree/InvenTree/pull/8905
- Fix schema definition on the /label/print endpoint
v301 - 2025-01-14 : https://github.com/inventree/InvenTree/pull/8894
v301 -> 2025-01-14 : https://github.com/inventree/InvenTree/pull/8894
- Remove ui preferences from the API
v300 - 2025-01-13 : https://github.com/inventree/InvenTree/pull/8886
v300 -> 2025-01-13 : https://github.com/inventree/InvenTree/pull/8886
- Allow null value for 'expiry_date' field introduced in #8867
v299 - 2025-01-10 : https://github.com/inventree/InvenTree/pull/8867
v299 -> 2025-01-10 : https://github.com/inventree/InvenTree/pull/8867
- Adds 'expiry_date' field to the PurchaseOrderReceive API endpoint
- Adds 'default_expiry` field to the PartBriefSerializer, affecting API endpoints which use it
v298 - 2025-01-07 : https://github.com/inventree/InvenTree/pull/8848
v298 -> 2025-01-07 : https://github.com/inventree/InvenTree/pull/8848
- Adds 'created_by' field to PurchaseOrder API endpoints
- Adds 'created_by' field to SalesOrder API endpoints
- Adds 'created_by' field to ReturnOrder API endpoints
v297 - 2024-12-29 : https://github.com/inventree/InvenTree/pull/8438
v297 -> 2024-12-29 : https://github.com/inventree/InvenTree/pull/8438
- Adjustments to the CustomUserState API endpoints and serializers
v296 - 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732
v296 -> 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732
- Adjust default "part_detail" behavior for StockItem API endpoints
v295 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8746
v295 -> 2024-12-23 : https://github.com/inventree/InvenTree/pull/8746
- Improve API documentation for build APIs
v294 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738
v294 -> 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738
- Extends registration API documentation
v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658
v293 -> 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658
- Adds new fields to the supplier barcode API endpoints
v292 - 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625
v292 -> 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625
- Add "on_order" and "in_stock" annotations to SupplierPart API
- Enhanced filtering for the SupplierPart API
v291 - 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596
v291 -> 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596
- Allow null / empty values for plugin settings
v290 - 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590
v290 -> 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590
- Adds "quantity" field to ReturnOrderLineItem model and API
v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
v289 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
- Enable status change when transferring stock items
v288 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574
v288 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574
- Adds "consumed" filter to StockItem API
v287 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
v287 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
- Adds ability to set stock status when returning items from a customer
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
v286 -> 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
- Adds "SelectionList" and "SelectionListEntry" API endpoints
v285 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559
v285 -> 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559
- Adds better description for registration endpoints
v284 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
v284 -> 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
- Adds new date filters to the StockItem API
- Adds new date filters to the BuildOrder API
- Adds new date filters to the SalesOrder API
- Adds new date filters to the PurchaseOrder API
- Adds new date filters to the ReturnOrder API
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
v283 -> 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
- Adds "note" field to the PartRelated API endpoint
v282 - 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487
v282 -> 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487
- Remove the "test statistics" API endpoints
- This is now provided via a custom plugin
v281 - 2024-11-15 : https://github.com/inventree/InvenTree/pull/8480
v281 -> 2024-11-15 : https://github.com/inventree/InvenTree/pull/8480
- Fixes StockHistory API data serialization
v280 - 2024-11-10 : https://github.com/inventree/InvenTree/pull/8461
v280 -> 2024-11-10 : https://github.com/inventree/InvenTree/pull/8461
- Makes schema for API information endpoint more informing
- Removes general not found endpoint
v279 - 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458
v279 -> 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458
- Adds "order_outstanding" and "part" filters to the BuildLine API endpoint
- Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint
v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
v278 -> 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
- Updates to the SalesOrder API endpoints
- Add "shipment count" information to the SalesOrder API endpoints
- Allow null value for SalesOrderAllocation.shipment field
- Additional filtering options for allocation endpoints
v277 - 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
v277 -> 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
- Allow build order list to be filtered by "outstanding" (alias for "active")
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
v276 -> 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
- Adds 'destination' field to the PurchaseOrder model and API endpoints
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
v275 -> 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
- Adds SKU and MPN fields to the StockItem serializer
- Additional export options for the StockItem serializer
v274 - 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
v274 -> 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
- Add more detailed information to NotificationEntry API serializer
v273 - 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
v273 -> 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
- Fixes for the BuildLine API endpoint
v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
v272 -> 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
- Adjustments to BuildLine API serializers
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
v271 -> 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
- Fixes for SalesOrderLineItem endpoints
v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
v270 -> 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
- Adds missing date fields from order API endpoint(s)
v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
v269 -> 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
- Adds "include_variants" filter to the BuildOrder API endpoint
- Adds "include_variants" filter to the SalesOrder API endpoint
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
- Adds "include_variants" filter to the ReturnOrder API endpoint
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
v268 -> 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
- Adds "in_stock" attribute to the StockItem serializer
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
v267 -> 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
v266 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
- Tweak SalesOrderShipment API for more efficient data retrieval
265 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
v265 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
- Adds API endpoint for providing custom admin integration details for plugins
264 - 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
v264 -> 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
- Adds Sales Order Shipment attachment model type
263 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
v263 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
- Adds Sales Order Shipment report
262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
v262 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
- Tweak permission requirements for uninstalling plugins via API
261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
v261 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
- Fixes for BuildOrder API serializers
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
v260 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
- Adds facility for server-side context data to be passed to client-side plugins
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
v259 -> 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
- Implements new API endpoint for enabling custom UI features via plugins
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
v258 -> 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
- Enhances the existing PartScheduling API endpoint
- Adds a formal DRF serializer to the endpoint
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
v257 -> 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
- Adds API endpoint for reporting barcode scan history
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
v256 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
- Adjustments for "stocktake" (stock history) API endpoints
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
v255 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
- Enables copying line items when duplicating an order
v254 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
v254 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
- Implements new API endpoints for enabling custom UI functionality via plugins
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
v253 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
- Adjustments for user API endpoints
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
v252 -> 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
- Add endpoint for listing all known units
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
v251 -> 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
- Adds "attach_to_model" field to the ReportTemplate model
v250 - 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069
v250 -> 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069
- Fixes 'revision' field definition in Part serializer
v249 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
v249 -> 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
- Sort status enums
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
v248 -> 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
- Small adjustments to labels for new custom status fields
v247 - 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
v247 -> 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
- Adjust "attachment" field on StockItemTestResult serializer
- Allow null values for attachment
v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
v246 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
- Adds custom status fields to various serializers
- Adds endpoints to admin custom status fields
v245 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
v245 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
- Documented pagination fields (no functional changes)
v244 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941
v244 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941
- Adds "create_child_builds" field to the Build API
- Write-only field to create child builds from the API
- Only available when creating a new build order
v243 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940
v243 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940
- Expose "ancestor" filter to the BuildOrder API
v242 - 2024-08-20 : https://github.com/inventree/InvenTree/pull/7932
v242 -> 2024-08-20 : https://github.com/inventree/InvenTree/pull/7932
- Adds "level" attribute to BuildOrder serializer
- Allow ordering of BuildOrder API by "level" attribute
- Allow "parent" filter for BuildOrder API to have "cascade=True" option
v241 - 2024-08-18 : https://github.com/inventree/InvenTree/pull/7906
v241 -> 2024-08-18 : https://github.com/inventree/InvenTree/pull/7906
- Adjusts required fields for the MeUserDetail endpoint
v240 - 2024-08-16 : https://github.com/inventree/InvenTree/pull/7900
v240 -> 2024-08-16 : https://github.com/inventree/InvenTree/pull/7900
- Adjust "issued_by" filter for the BuildOrder list endpoint
- Adjust "assigned_to" filter for the BuildOrder list endpoint
v239 - 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888
v239 -> 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888
- Adds "testable" field to the Part model
- Adds associated filters to various API endpoints
v238 - 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874
v238 -> 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874
- Add "assembly" filter to BuildLine API endpoint
v237 - 2024-08-13 : https://github.com/inventree/InvenTree/pull/7863
v237 -> 2024-08-13 : https://github.com/inventree/InvenTree/pull/7863
- Reimplement "bulk delete" operation for Attachment model
- Fix permission checks for Attachment API endpoints
v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
v236 -> 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
- Adds "supplier_name" to the PurchaseOrder API serializer
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
v235 -> 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
- Adds "on_order" quantity to SalesOrderLineItem serializer
- Adds "building" quantity to SalesOrderLineItem serializer
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
v234 -> 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
- Fixes bug in the plugin metadata endpoint
v233 - 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
v233 -> 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
- Adds new endpoints for managing state of build orders
- Adds new endpoints for managing state of purchase orders
- Adds new endpoints for managing state of sales orders
- Adds new endpoints for managing state of return orders
v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
v232 -> 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
- Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date'
v231 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794
v231 -> 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794
- Optimize BuildItem and BuildLine serializers to improve API efficiency
v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164
v230 -> 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164
- Adds test statistics endpoint
v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775
v229 -> 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775
- Add extra exportable fields to the BomItem serializer
v228 - 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
v228 -> 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
- Adds "icon" field to the PartCategory.path and StockLocation.path API
- Adds icon packages API endpoint
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
v227 -> 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
- Adds endpoints to list and revoke the tokens issued to the current user
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
v226 -> 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
- Adds barcode generation API endpoint
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
v225 -> 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
- Adds "filters" field to DataImportSession API
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
v224 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
- Add notes field to ManufacturerPart and SupplierPart API endpoints
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
v223 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
- Allow adjustment of "packaging" field when receiving items against a purchase order
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
v222 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
v221 -> 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
- Adds missing fields from StockItemBriefSerializer
- Adds missing fields from PartBriefSerializer
- Adds extra exportable fields to BuildItemSerializer
v220 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
v220 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
- Adds "revision_of" field to Part serializer
- Adds new API filters for "revision" status
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
v219 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
- Adds new fields to the BuildItem API endpoints
- Adds new ordering / filtering options to the BuildItem API endpoints
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
v218 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
- Adds "can_build" field to the BomItem API
v217 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599
v217 -> 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599
- Fixes bug in "project_code" field for order API endpoints
v216 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595
v216 -> 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595
- Moves API endpoint for contenttype lookup by model name
v215 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591
v215 -> 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591
- Adds additional fields to the BuildLine serializer
v214 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587
v214 -> 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587
- Adds "default_location_detail" field to the Part API
v213 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527
v213 -> 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527
- Adds 'locked' field to Part API
v212 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562
v212 -> 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562
- Makes API generation more robust (no functional changes)
v211 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911
v211 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911
- Adds API endpoints for managing data import and export
v210 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518
v210 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518
- Adds translatable text to User API fields
v209 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514
v209 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514
- Add "top_level" filter to PartCategory API endpoint
- Add "top_level" filter to StockLocation API endpoint
v208 - 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479
v208 -> 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479
- Adds documentation for the user roles API endpoint (no functional changes)
v207 - 2024-06-09 : https://github.com/inventree/InvenTree/pull/7420
v207 -> 2024-06-09 : https://github.com/inventree/InvenTree/pull/7420
- Moves all "Attachment" models into a single table
- All "Attachment" operations are now performed at /api/attachment/
- Add permissions information to /api/user/roles/ endpoint
v206 - 2024-06-08 : https://github.com/inventree/InvenTree/pull/7417
v206 -> 2024-06-08 : https://github.com/inventree/InvenTree/pull/7417
- Adds "choices" field to the PartTestTemplate model
v205 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7284
v205 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7284
- Added model_type and model_id fields to the "NotesImage" serializer
v204 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7393
v204 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7393
- Fixes previous API update which resulted in inconsistent ordering of currency codes
v203 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7390
v203 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7390
- Currency codes are now configurable as a run-time setting
v202 - 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343
v202 -> 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343
- Adjust "required" attribute of Part.category field to be optional
v201 - 2024-05-21 : https://github.com/inventree/InvenTree/pull/7074
v201 -> 2024-05-21 : https://github.com/inventree/InvenTree/pull/7074
- Major refactor of the report template / report printing interface
- This is a *breaking change* to the report template API
v200 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7000
v200 -> 2024-05-20 : https://github.com/inventree/InvenTree/pull/7000
- Adds API endpoint for generating custom batch codes
- Adds API endpoint for generating custom serial numbers
v199 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264
v199 -> 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264
- Expose "bom_valid" filter for the Part API
- Expose "starred" filter for the Part API
v198 - 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258
v198 -> 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258
- Fixed lookup field conflicts in the plugins API
v197 - 2024-05-14 : https://github.com/inventree/InvenTree/pull/7224
v197 -> 2024-05-14 : https://github.com/inventree/InvenTree/pull/7224
- Refactor the plugin API endpoints to use the plugin "key" for lookup, rather than the PK value
v196 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160
v196 -> 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160
- Adds "location" field to BuildOutputComplete API endpoint
v195 - 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
v195 -> 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
- Fixes bug in BuildOrderCancel API endpoint
v194 - 2024-05-01 : https://github.com/inventree/InvenTree/pull/7147
v194 -> 2024-05-01 : https://github.com/inventree/InvenTree/pull/7147
- Adds field description to the currency_exchange_retrieve API call
v193 - 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144
v193 -> 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144
- Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints
v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
v192 -> 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
- Adds 'trackable' ordering option to BuildLineLabel API endpoint
v191 - 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079
v191 -> 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079
- Adds API endpoints for Contenttype model
v190 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024
v190 -> 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024
- Adds "active" field to the Company API endpoints
- Allow company list to be filtered by "active" status
v189 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066
v189 -> 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066
- Adds "currency" field to CompanyBriefSerializer class
v188 - 2024-04-16 : https://github.com/inventree/InvenTree/pull/6970
v188 -> 2024-04-16 : https://github.com/inventree/InvenTree/pull/6970
- Adds session authentication support for the API
- Improvements for login / logout endpoints for better support of React web interface
v187 - 2024-04-10 : https://github.com/inventree/InvenTree/pull/6985
v187 -> 2024-04-10 : https://github.com/inventree/InvenTree/pull/6985
- Allow Part list endpoint to be sorted by pricing_min and pricing_max values
- Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
- Adds total pricing values to BomItem serializer
v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855
v186 -> 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855
- Adds license information to the API
v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
v185 -> 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
- Remove /plugin/activate endpoint
- Update docstrings and typing for various API endpoints (no functional changes)
v184 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464
v184 -> 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464
- Add additional fields for tests (start/end datetime, test station)
v183 - 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972
v183 -> 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972
- Adds "category_default_location" annotated field to part serializer
- Adds "part_detail.category_default_location" annotated field to stock item serializer
- Adds "part_detail.category_default_location" annotated field to purchase order line serializer
- Adds "parent_default_location" annotated field to category serializer
v182 - 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714
v182 -> 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714
- Expose ReportSnippet model to the /report/snippet/ API endpoint
- Expose ReportAsset model to the /report/asset/ API endpoint
v181 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6541
v181 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6541
- Adds "width" and "height" fields to the LabelTemplate API endpoint
- Adds "page_size" and "landscape" fields to the ReportTemplate API endpoint
v180 - 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463
v180 -> 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463
- Tweaks to API documentation to allow automatic documentation generation
v179 - 2024-03-01 : https://github.com/inventree/InvenTree/pull/6605
v179 -> 2024-03-01 : https://github.com/inventree/InvenTree/pull/6605
- Adds "subcategories" count to PartCategory serializer
- Adds "sublocations" count to StockLocation serializer
- Adds "image" field to PartBrief serializer
- Adds "image" field to CompanyBrief serializer
v178 - 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
v178 -> 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
- Adds "external_stock" field to the Part API endpoint
- Adds "external_stock" field to the BomItem API endpoint
- Adds "external_stock" field to the BuildLine API endpoint
- Stock quantities represented in the BuildLine API endpoint are now filtered by Build.source_location
v177 - 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581
v177 -> 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581
- Adds "subcategories" count to PartCategoryTree serializer
- Adds "sublocations" count to StockLocationTree serializer
v176 - 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535
v176 -> 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535
- Adds the field "plugins_install_disabled" to the Server info API endpoint
v175 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6538
v175 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6538
- Adds "parts" count to PartParameterTemplate serializer
v174 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6536
v174 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6536
- Expose PartCategory filters to the API documentation
- Expose StockLocation filters to the API documentation
v173 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6483
v173 -> 2024-02-20 : https://github.com/inventree/InvenTree/pull/6483
- Adds "merge_items" to the PurchaseOrderLine create API endpoint
- Adds "auto_pricing" to the PurchaseOrderLine create/update API endpoint
v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
v172 -> 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
- Adds "enabled" field to the PartTestTemplate API endpoint
- Adds "enabled" filter to the PartTestTemplate list
- Adds "enabled" filter to the StockItemTestResult list
v171 - 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516
v171 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516
- Adds "key" as a filterable parameter to PartTestTemplate list endpoint
v170 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6514

View File

@ -365,6 +365,52 @@ def get_secret_key():
return key_data
def get_oidc_private_key():
"""Return the private key for OIDC authentication.
Following options are tested, in descending order of preference:
A) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY or config yalue => Use raw key data
B) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY_FILE or config value => Load key data from file
C) Create "oidc.pem" if it does not exist
"""
RSA_KEY = get_setting('INVENTREE_OIDC_PRIVATE_KEY', 'oidc_private_key')
if RSA_KEY:
logger.info('RSA_KEY loaded by INVENTREE_OIDC_PRIVATE_KEY') # pragma: no cover
return RSA_KEY
# Look for private key file
key_loc = Path(
get_setting(
'INVENTREE_OIDC_PRIVATE_KEY_FILE',
'oidc_private_key_file',
get_base_dir().joinpath('oidc.pem'),
)
)
if key_loc.exists():
return key_loc.read_text()
else:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Default location for private key file
logger.info("Generating oidc key file at '%s'", key_loc)
ensure_dir(key_loc.parent)
# Create a random key file
new_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
# Write our key to disk for safe keeping
with open(str(key_loc), 'wb') as f:
f.write(
new_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
RSA_KEY = key_loc.read_text()
return RSA_KEY
def get_custom_file(
env_ref: str, conf_ref: str, log_ref: str, lookup_media: bool = False
):

View File

@ -32,14 +32,23 @@ class InvenTreeSearchFilter(filters.SearchFilter):
"""Return a set of search fields for the request, adjusted based on request params.
The following query params are available to 'augment' the search (in decreasing order of priority)
- search_notes: If True, 'notes' is added to the search_fields if it isn't already present
- search_regex: If True, search is performed on 'regex' comparison
"""
regex = InvenTree.helpers.str2bool(
request.query_params.get('search_regex', False)
search_notes = InvenTree.helpers.str2bool(
request.query_params.get('search_notes', False)
)
search_fields = super().get_search_fields(view, request)
if search_notes and 'notes' not in search_fields:
# don't modify existing list, create a new object so further queries aren't affected
search_fields = [*search_fields, 'notes']
regex = InvenTree.helpers.str2bool(
request.query_params.get('search_regex', False)
)
fields = []
if search_fields:

View File

@ -194,6 +194,7 @@ def format_money(
money (Money): The money object to format
decimal_places (int): Number of decimal places to use
fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
include_symbol (bool): Whether to include the currency symbol in the formatted output
Returns:
str: The formatted string

View File

@ -32,18 +32,67 @@ from .settings import MEDIA_URL, STATIC_URL
logger = structlog.get_logger('inventree')
INT_CLIP_MAX = 0x7FFFFFFF
def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
"""Extract an integer out of reference."""
def extract_int(
reference, clip=INT_CLIP_MAX, try_hex=False, allow_negative=False
) -> int:
"""Extract an integer out of provided string.
Arguments:
reference: Input string to extract integer from
clip: Maximum value to return (default = 0x7FFFFFFF)
try_hex: Attempt to parse as hex if integer conversion fails (default = False)
allow_negative: Allow negative values (default = False)
"""
# Default value if we cannot convert to an integer
ref_int = 0
def do_clip(value: int, clip: int, allow_negative: bool) -> int:
"""Perform clipping on the provided value.
Arguments:
value: Value to clip
clip: Maximum value to clip to
allow_negative: Allow negative values (default = False)
"""
if clip is None:
return value
clip = min(clip, INT_CLIP_MAX)
if value > clip:
return clip
elif value < -clip:
return -clip
if not allow_negative:
value = abs(value)
return value
reference = str(reference).strip()
# Ignore empty string
if len(reference) == 0:
return 0
# Try naive integer conversion first
try:
ref_int = int(reference)
return do_clip(ref_int, clip, allow_negative)
except ValueError:
pass
# Hex?
if try_hex or reference.startswith('0x'):
try:
ref_int = int(reference, base=16)
return do_clip(ref_int, clip, allow_negative)
except ValueError:
pass
# Look at the start of the string - can it be "integerized"?
result = re.match(r'^(\d+)', reference)
@ -66,11 +115,7 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
# Ensure that the returned values are within the range that can be stored in an IntegerField
# Note: This will result in large values being "clipped"
if clip is not None:
if ref_int > clip:
ref_int = clip
elif ref_int < -clip:
ref_int = -clip
ref_int = do_clip(ref_int, clip, allow_negative)
if not allow_negative and ref_int < 0:
ref_int = abs(ref_int)
@ -437,6 +482,7 @@ def increment_serial_number(serial, part=None):
Arguments:
serial: The serial number which should be incremented
part: Optional part object to provide additional context for incrementing the serial number
Returns:
incremented value, or None if incrementing could not be performed.
@ -491,6 +537,7 @@ def extract_serial_numbers(
input_string: Input string with specified serial numbers (string, or integer)
expected_quantity: The number of (unique) serial numbers we expect
starting_value: Provide a starting value for the sequence (or None)
part: Part that should be used as context
"""
if starting_value is None:
starting_value = increment_serial_number(None, part=part)

View File

@ -4,6 +4,7 @@ from django.conf import settings
from django.core import mail as django_mail
import structlog
from allauth.account.models import EmailAddress
import InvenTree.ready
import InvenTree.tasks
@ -30,15 +31,15 @@ def is_email_configured():
# Display warning unless in test mode
if not testing: # pragma: no cover
logger.debug('EMAIL_HOST is not configured')
logger.debug('INVE-W7: EMAIL_HOST is not configured')
# Display warning unless in test mode
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
logger.debug('EMAIL_HOST_USER is not configured')
logger.debug('INVE-W7: EMAIL_HOST_USER is not configured')
# Display warning unless in test mode
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
logger.debug('EMAIL_HOST_PASSWORD is not configured')
logger.debug('INVE-W7: EMAIL_HOST_PASSWORD is not configured')
# Email sender must be configured
if not settings.DEFAULT_FROM_EMAIL:
@ -56,7 +57,6 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients = [recipients]
import InvenTree.ready
import InvenTree.status
if InvenTree.ready.isImportingData():
# If we are importing data, don't send emails
@ -64,6 +64,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
if not is_email_configured() and not settings.TESTING:
# Email is not configured / enabled
logger.info('INVE-W7: Email will not be send, no mail server configured')
return
# If a *from_email* is not specified, ensure that the default is set
@ -88,3 +89,19 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
html_message=html_message,
group='notification',
)
def get_email_for_user(user) -> str:
"""Find an email address for the specified user."""
# First check if the user has an associated email address
if user.email:
return user.email
# Otherwise, find first matching email
# Priority is given to primary or verified email addresses
if (
email := EmailAddress.objects.filter(user=user)
.order_by('-primary', '-verified')
.first()
):
return email.email

View File

@ -132,6 +132,7 @@ def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
Arguments:
key: The key for the shared state
default: The default value to return if the key does not exist
"""
return cache.get(self._get_key(key)) or default

View File

@ -265,6 +265,7 @@ def notify_responsible(
sender,
content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
exclude=None,
extra_users: Optional[list] = None,
):
"""Notify all responsible parties of a change in an instance.
@ -276,15 +277,19 @@ def notify_responsible(
sender: Sender model reference
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
exclude (User, optional): User instance that should be excluded. Defaults to None.
extra_users (list, optional): List of extra users to notify. Defaults to None.
"""
import InvenTree.ready
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
return
notify_users(
[instance.responsible], instance, sender, content=content, exclude=exclude
)
users = [instance.responsible]
if extra_users:
users.extend(extra_users)
notify_users(users, instance, sender, content=content, exclude=exclude)
def notify_users(

View File

@ -1,4 +1,4 @@
"""Check if there are any pending database migrations, and run them."""
"""Extended schema generator."""
from pathlib import Path
from typing import TypeVar
@ -108,3 +108,5 @@ class Command(spectacular.Command):
settings.SPECTACULAR_SETTINGS['APPEND_COMPONENTS'] = components
super().handle(*args, **kwargs)
return 'done'

View File

@ -13,9 +13,9 @@ from rest_framework.utils import model_meta
import common.models
import InvenTree.permissions
import users.models
from InvenTree.helpers import str2bool
from InvenTree.serializers import DependentField
from users.permissions import check_user_permission
logger = structlog.get_logger('inventree')
@ -107,28 +107,16 @@ class InvenTreeMetadata(SimpleMetadata):
self.model = InvenTree.permissions.get_model_for_view(view)
# Construct the 'table name' from the model
app_label = self.model._meta.app_label
tbl_label = self.model._meta.model_name
metadata['model'] = tbl_label
table = f'{app_label}_{tbl_label}'
actions = metadata.get('actions', None)
if actions is None:
actions = {}
check = users.models.RuleSet.check_table_permission
# Map the request method to a permission type
rolemap = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
rolemap = {**InvenTree.permissions.ACTION_MAP, 'OPTIONS': 'view'}
# let the view define a custom rolemap
if hasattr(view, 'rolemap'):
@ -136,13 +124,15 @@ class InvenTreeMetadata(SimpleMetadata):
# Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items():
result = check(user, table, permission)
result = check_user_permission(user, self.model, permission)
if method in actions and not result:
del actions[method]
# Add a 'DELETE' action if we are allowed to delete
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
if 'DELETE' in view.allowed_methods and check_user_permission(
user, self.model, 'delete'
):
actions['DELETE'] = {}
metadata['actions'] = actions
@ -170,11 +160,11 @@ class InvenTreeMetadata(SimpleMetadata):
- model_value is callable, and field_value is not (this indicates that the model value is translated)
- model_value is not a string, and field_value is a string (this indicates that the model value is translated)
Arguments:
- field_name: The name of the field
- field_key: The property key to override
- field_value: The value of the field (if available)
- model_value: The equivalent value of the model (if available)
Args:
field_name (str): The name of the field.
field_key (str): The property key to override.
field_value: The value of the field (if available).
model_value: The equivalent value of the model (if available).
"""
if field_value is None and model_value is not None:
return model_value

View File

@ -86,6 +86,11 @@ class AuthRequiredMiddleware:
response = self.get_response(request)
return response
# oAuth2 requests are handled by the oAuth2 library
if request.path_info.startswith('/o/'):
response = self.get_response(request)
return response
# Is the function exempt from auth requirements?
path_func = resolve(request.path).func

View File

@ -1,10 +1,29 @@
"""Permission set for InvenTree."""
from functools import wraps
from typing import Optional
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
from oauth2_provider.contrib.rest_framework.authentication import OAuth2Authentication
from rest_framework import permissions
import users.models
import users.permissions
import users.ruleset
from users.oauth2_scopes import (
DEFAULT_READ,
DEFAULT_STAFF,
DEFAULT_SUPERUSER,
get_granular_scope,
)
ACTION_MAP = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
'OPTIONS': DEFAULT_READ,
}
def get_model_for_view(view):
@ -21,7 +40,126 @@ def get_model_for_view(view):
raise AttributeError(f'Serializer class not specified for {view.__class__}')
class RolePermission(permissions.BasePermission):
def map_scope(
roles: Optional[list[str]] = None,
only_read=False,
read_name=DEFAULT_READ,
map_read: Optional[list[str]] = None,
map_read_name=DEFAULT_READ,
) -> dict:
"""Generate the required scopes for OAS permission views.
Args:
roles (Optional[list[str]]): A list of roles or tables to generate granular scopes for.
only_read (bool): If True, only the read scope will be returned for all actions.
read_name (str): The read scope name to use when `only_read` is True.
map_read (Optional[list[str]]): A list of HTTP methods that should map to the default read scope (use if some actions requirea differing role).
map_read_name (str): The read scope name to use for methods specified in `map_read` when `map_read` is specified.
Returns:
dict: A dictionary mapping HTTP methods to their corresponding scopes.
Each scope is represented as a list of lists of strings.
"""
def scope_name(action):
if only_read:
return [[read_name]]
if roles:
return [[get_granular_scope(action, table) for table in roles]]
return [[action]]
def get_scope(method, action):
if map_read and method in map_read:
return [[map_read_name]]
return scope_name(action)
return {
method: get_scope(method, action) if method != 'OPTIONS' else [[DEFAULT_READ]]
for method, action in ACTION_MAP.items()
}
# Precalculate the roles mapping
roles = users.ruleset.get_ruleset_models()
precalculated_roles = {}
for role, tables in roles.items():
for table in tables:
if table not in precalculated_roles:
precalculated_roles[table] = []
precalculated_roles[table].append(role)
class OASTokenMixin:
"""Mixin that combines the permissions of normal classes and token classes."""
def has_permission(self, request, view):
"""Check if the user has the required scopes or was authenticated another way."""
return self.check_oauth2_authentication(
request, view
) or super().has_permission(request, view)
def check_oauth2_authentication(self, request, view):
"""Check if the user is authenticated using OAuth2 and has the required scopes."""
return self.is_oauth2ed(
request
) and TokenMatchesOASRequirements().has_permission(request, view)
def is_oauth2ed(self, request):
"""Check if the user is authenticated using OAuth2."""
oauth2authenticated = False
if bool(request.user and request.user.is_authenticated):
oauth2authenticated = isinstance(
request.successful_authenticator, OAuth2Authentication
)
return oauth2authenticated
class InvenTreeRoleScopeMixin(OASTokenMixin):
"""Permission that discovers the required scopes from the OpenAPI schema."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
if hasattr(view, 'required_alternate_scopes'):
return view.required_alternate_scopes
try:
# Extract the model name associated with this request
model = get_model_for_view(view)
calc = precalculated_roles.get(
f'{model._meta.app_label}_{model._meta.model_name}', []
)
if model is None or not calc:
return map_scope(only_read=True)
return map_scope(roles=calc)
except AttributeError:
# We will assume that if the serializer class does *not* have a Meta,
# then we don't need a permission
return map_scope(only_read=True)
except Exception:
return map_scope(only_read=True)
class InvenTreeTokenMatchesOASRequirements(InvenTreeRoleScopeMixin):
"""Combines InvenTree role-based scope handling with OpenAPI schema token requirements.
Usesd as default permission class.
"""
def has_permission(self, request, view):
"""Check if the user has the required scopes or was authenticated another way."""
if self.is_oauth2ed(request):
# Check if the user is authenticated using OAuth2 and has the required scopes
return super().has_permission(request, view)
# If the user is authenticated using another method, check if they have the required permissions
return bool(request.user and request.user.is_authenticated)
def has_object_permission(self, request, view, obj):
"""Return `True` if permission is granted, `False` otherwise."""
return True
class RolePermission(InvenTreeRoleScopeMixin, permissions.BasePermission):
"""Role mixin for API endpoints, allowing us to specify the user "role" which is required for certain operations.
Each endpoint can have one or more of the following actions:
@ -52,14 +190,7 @@ class RolePermission(permissions.BasePermission):
return True
# Map the request method to a permission type
rolemap = {
'GET': 'view',
'OPTIONS': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
rolemap = {**ACTION_MAP, 'OPTIONS': 'view'}
# let the view define a custom rolemap
if hasattr(view, 'rolemap'):
@ -73,7 +204,7 @@ class RolePermission(permissions.BasePermission):
if '.' in role:
role, permission = role.split('.')
return users.models.check_user_role(user, role, permission)
return users.permissions.check_user_role(user, role, permission)
try:
# Extract the model name associated with this request
@ -82,27 +213,66 @@ class RolePermission(permissions.BasePermission):
if model is None:
return True
app_label = model._meta.app_label
model_name = model._meta.model_name
table = f'{app_label}_{model_name}'
except AttributeError:
# We will assume that if the serializer class does *not* have a Meta,
# then we don't need a permission
return True
return users.models.RuleSet.check_table_permission(user, table, permission)
return users.permissions.check_user_permission(user, model, permission)
class IsSuperuser(permissions.IsAdminUser):
class RolePermissionOrReadOnly(RolePermission):
"""RolePermission which also allows read access for any authenticated user."""
REQUIRE_STAFF = False
def has_permission(self, request, view):
"""Determine if the current user has the specified permissions.
- If the user does have the required role, then allow the request
- If the user does not have the required role, but is authenticated, then allow read-only access
"""
user = getattr(request, 'user', None)
if not user or not user.is_active or not user.is_authenticated:
return False
if user.is_superuser:
return True
if not self.REQUIRE_STAFF or user.is_staff:
if super().has_permission(request, view):
return True
return request.method in permissions.SAFE_METHODS
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
scopes = map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)
return scopes
class StaffRolePermissionOrReadOnly(RolePermissionOrReadOnly):
"""RolePermission which requires staff AND role access, or read-only."""
REQUIRE_STAFF = True
class IsSuperuserOrSuperScope(OASTokenMixin, permissions.IsAdminUser):
"""Allows access only to superuser users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(request.user and request.user.is_superuser)
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True, read_name=DEFAULT_SUPERUSER)
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
class IsSuperuserOrReadOnlyOrScope(OASTokenMixin, permissions.IsAdminUser):
"""Allow read-only access to any user, but write access is restricted to superuser users."""
def has_permission(self, request, view):
@ -112,17 +282,59 @@ class IsSuperuserOrReadOnly(permissions.IsAdminUser):
or request.method in permissions.SAFE_METHODS
)
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True,
read_name=DEFAULT_SUPERUSER,
map_read=permissions.SAFE_METHODS,
)
class IsStaffOrReadOnly(permissions.IsAdminUser):
"""Allows read-only access to any user, but write access is restricted to staff users."""
class IsAuthenticatedOrReadScope(OASTokenMixin, permissions.IsAuthenticated):
"""Allows access only to authenticated users or read scope tokens."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
class IsStaffOrReadOnlyScope(OASTokenMixin, permissions.IsAuthenticated):
"""Allows read-only access to any authenticated user, but write access is restricted to staff users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(
"""Check if the user is a staff."""
return bool(permissions.IsAuthenticated().has_permission(request, view)) and (
(request.user and request.user.is_staff)
or request.method in permissions.SAFE_METHODS
)
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)
class IsAdminOrAdminScope(OASTokenMixin, permissions.IsAdminUser):
"""Allows access only to admin users or admin scope tokens."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True, read_name=DEFAULT_STAFF)
class AllowAnyOrReadScope(OASTokenMixin, permissions.AllowAny):
"""Allows access to any user or read scope tokens."""
def has_permission(self, request, view):
"""Anyone is allowed."""
return True
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
def auth_exempt(view_func):
"""Mark a view function as being exempt from auth requirements."""
@ -132,3 +344,43 @@ def auth_exempt(view_func):
wrapped_view.auth_exempt = True
return wraps(view_func)(wrapped_view)
class UserSettingsPermissionsOrScope(OASTokenMixin, permissions.BasePermission):
"""Special permission class to determine if the user can view / edit a particular setting."""
def has_object_permission(self, request, view, obj):
"""Check if the user that requested is also the object owner."""
try:
user = request.user
except AttributeError: # pragma: no cover
return False
return user == obj.user
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
class GlobalSettingsPermissions(OASTokenMixin, permissions.BasePermission):
"""Special permission class to determine if the user is "staff"."""
def has_permission(self, request, view):
"""Check that the requesting user is 'admin'."""
try:
user = request.user
if request.method in permissions.SAFE_METHODS:
return True
# Any other methods require staff access permissions
return user.is_staff
except AttributeError: # pragma: no cover
return False
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)

View File

@ -1,25 +1,55 @@
"""Schema processing functions for cleaning up generated schema."""
from itertools import chain
from typing import Optional
from drf_spectacular.contrib.django_oauth_toolkit import DjangoOAuthToolkitScheme
from drf_spectacular.drainage import warn
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import ComponentRegistry
from drf_spectacular.utils import _SchemaType
from InvenTree.permissions import OASTokenMixin
from users.oauth2_scopes import oauth2_scopes
class ExtendedOAuth2Scheme(DjangoOAuthToolkitScheme):
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
target_class = 'users.authentication.ExtendedOAuth2Authentication'
def get_security_requirement(self, auto_schema):
"""Get the security requirement for the current view."""
ret = super().get_security_requirement(auto_schema)
if ret:
return ret
# If no security requirement is found, try if the view uses our OASTokenMixin
for permission in auto_schema.view.get_permissions():
if isinstance(permission, OASTokenMixin):
alt_scopes = permission.get_required_alternate_scopes(
auto_schema.view.request, auto_schema.view
)
alt_scopes = alt_scopes.get(auto_schema.method, [])
return [{self.name: group} for group in alt_scopes]
class ExtendedAutoSchema(AutoSchema):
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
def is_bulk_delete(self) -> bool:
"""Check the class of the current view for the BulkDeleteMixin."""
return 'BulkDeleteMixin' in [c.__name__ for c in type(self.view).__mro__]
def is_bulk_action(self, ref: str) -> bool:
"""Check the class of the current view for the bulk mixins."""
return ref in [c.__name__ for c in type(self.view).__mro__]
def get_operation_id(self) -> str:
"""Custom path handling overrides, falling back to default behavior."""
result_id = super().get_operation_id()
# rename bulk deletes to deconflict with single delete operation_id
if self.method == 'DELETE' and self.is_bulk_delete():
# rename bulk actions to deconflict with single action operation_id
if (self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin')) or (
(self.method == 'PUT' or self.method == 'PATCH')
and self.is_bulk_action('BulkUpdateMixin')
):
action = self.method_mapping[self.method.lower()]
result_id = result_id.replace(action, 'bulk_' + action)
@ -42,7 +72,7 @@ class ExtendedAutoSchema(AutoSchema):
# drf-spectacular doesn't support a body on DELETE endpoints because the semantics are not well-defined and
# OpenAPI recommends against it. This allows us to generate a schema that follows existing behavior.
if self.method == 'DELETE' and self.is_bulk_delete():
if self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin'):
original_method = self.method
self.method = 'PUT'
request_body = self._get_request_body()
@ -75,3 +105,52 @@ def postprocess_required_nullable(result, generator, request, public):
schema.pop('required')
return result
def postprocess_print_stats(result, generator, request, public):
"""Prints statistics against schema."""
rlt_dict = {}
for path in result['paths']:
for method in result['paths'][path]:
sec = result['paths'][path][method].get('security', [])
scopes = list(filter(None, (item.get('oauth2') for item in sec)))
rlt_dict[f'{path}:{method}'] = {
'method': method,
'oauth': list(chain(*scopes)),
'sec': sec is None,
}
# Get paths without oauth2
no_oauth2 = [
path for path, details in rlt_dict.items() if not any(details['oauth'])
]
no_oauth2_wa = [path for path in no_oauth2 if not path.startswith('/api/auth/v1/')]
# Get paths without security
no_security = [path for path, details in rlt_dict.items() if details['sec']]
# Get path counts per scope
scopes = {}
for path, details in rlt_dict.items():
if details['oauth']:
for scope in details['oauth']:
if scope not in scopes:
scopes[scope] = []
scopes[scope].append(path)
# Sort scopes by keys
scopes = dict(sorted(scopes.items()))
# Print statistics
print('\nSchema statistics:')
print(f'Paths without oauth2: {len(no_oauth2)}')
print(f'Paths without oauth2 (without allauth): {len(no_oauth2_wa)}')
print(f'Paths without security: {len(no_security)}\n')
print('Scope stats:')
for scope, paths in scopes.items():
print(f' {scope}: {len(paths)}')
print()
# Check for unknown scopes
for scope, paths in scopes.items():
if scope not in oauth2_scopes:
warn(f'unknown scope `{scope}` in {len(paths)} paths')
return result

View File

@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money
from djmoney.utils import MONEY_CLASSES, get_currency_field_name
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty
@ -86,6 +87,7 @@ class InvenTreeMoneySerializer(MoneyField):
return amount
@extend_schema_field(serializers.CharField())
class InvenTreeCurrencySerializer(serializers.ChoiceField):
"""Custom serializers for selecting currency option."""
@ -111,6 +113,14 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
if 'help_text' not in kwargs:
kwargs['help_text'] = _('Select currency from available options')
if InvenTree.ready.isGeneratingSchema():
kwargs['help_text'] = (
kwargs['help_text']
+ '\n\n'
+ '\n'.join(f'* `{value}` - {label}' for value, label in choices)
+ "\n\nOther valid currencies may be found in the 'CURRENCY_CODES' global setting."
)
super().__init__(*args, **kwargs)

View File

@ -21,13 +21,20 @@ from django.core.validators import URLValidator
from django.http import Http404, HttpResponseGone
import structlog
from corsheaders.defaults import default_headers as default_cors_headers
from dotenv import load_dotenv
from InvenTree.cache import get_cache_config, is_global_cache_enabled
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
from InvenTree.config import (
get_boolean_setting,
get_custom_file,
get_oidc_private_key,
get_setting,
)
from InvenTree.ready import isInMainThread
from InvenTree.sentry import default_sentry_dsn, init_sentry
from InvenTree.version import checkMinPythonVersion, inventreeApiVersion
from users.oauth2_scopes import oauth2_scopes
from . import config, locales
@ -298,6 +305,7 @@ INSTALLED_APPS = [
'django_otp', # OTP is needed for MFA - base package
'django_otp.plugins.otp_totp', # Time based OTP
'django_otp.plugins.otp_static', # Backup codes
'oauth2_provider', # OAuth2 provider and API access
'drf_spectacular', # API documentation
'django_ical', # For exporting calendars
]
@ -321,6 +329,7 @@ MIDDLEWARE = CONFIG.get(
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware',
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider
'maintenance_mode.middleware.MaintenanceModeMiddleware',
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
@ -353,6 +362,7 @@ QUERYCOUNT = {
AUTHENTICATION_BACKENDS = CONFIG.get(
'authentication_backends',
[
'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider
'django.contrib.auth.backends.RemoteUserBackend', # proxy login
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
@ -522,6 +532,15 @@ TEMPLATES = [
}
]
OAUTH2_PROVIDER = {
# default scopes
'SCOPES': oauth2_scopes,
# OIDC
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': get_oidc_private_key(),
'PKCE_REQUIRED': False,
}
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
@ -529,12 +548,14 @@ REST_FRAMEWORK = {
'users.authentication.ApiTokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'users.authentication.ExtendedOAuth2Authentication',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission',
'InvenTree.permissions.InvenTreeTokenMatchesOASRequirements',
],
'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema',
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
@ -1203,6 +1224,11 @@ CORS_ALLOWED_ORIGIN_REGEXES = get_setting(
typecast=list,
)
# Allow extra CORS headers in DEBUG mode
# Required for serving /static/ and /media/ files
if DEBUG:
CORS_ALLOW_HEADERS = (*default_cors_headers, 'cache-control', 'pragma', 'expires')
# In debug mode allow CORS requests from localhost
# This allows connection from the frontend development server
if DEBUG:
@ -1395,6 +1421,7 @@ FLAGS = {
'NEXT_GEN': [
{'condition': 'parameter', 'value': 'ngen='}
], # Should next-gen features be turned on?
'OIDC': [{'condition': 'parameter', 'value': 'oidc='}],
}
# Get custom flags from environment/yaml
@ -1428,7 +1455,22 @@ SPECTACULAR_SETTINGS = {
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums',
'InvenTree.schema.postprocess_required_nullable',
'InvenTree.schema.postprocess_print_stats',
],
'ENUM_NAME_OVERRIDES': {
'UserTypeEnum': 'users.models.UserProfile.UserType',
'TemplateModelTypeEnum': 'report.models.ReportTemplateBase.ModelChoices',
'AttachmentModelTypeEnum': 'common.models.Attachment.ModelChoices',
'DataImportSessionModelTypeEnum': 'importer.models.DataImportSession.ModelChoices',
# Allauth
'UnauthorizedStatus': [[401, 401]],
'IsTrueEnum': [[True, True]],
},
# oAuth2
'OAUTH2_FLOWS': ['authorizationCode', 'clientCredentials'],
'OAUTH2_AUTHORIZATION_URL': '/o/authorize/',
'OAUTH2_TOKEN_URL': '/o/token/',
'OAUTH2_REFRESH_URL': '/o/revoke_token/',
}
if SITE_URL and not TESTING:

View File

@ -67,7 +67,7 @@ def check_system_health(**kwargs):
if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover
result = False
if not settings.DEBUG:
logger.warning('Email backend not configured')
logger.warning('INVE-W7: Email backend not configured')
if not result: # pragma: no cover
if not settings.DEBUG:

View File

@ -705,4 +705,17 @@ def email_user(user_id: int, subject: str, message: str) -> None:
logger.warning('User <%s> not found - cannot send welcome message', user_id)
return
user.email_user(subject=subject, message=message)
from InvenTree.helpers_email import get_email_for_user, send_email
if email := get_email_for_user(user):
send_email(subject, message, [email])
@scheduled_task(ScheduledTask.DAILY)
def run_oauth_maintenance():
"""Run the OAuth maintenance task(s)."""
from oauth2_provider.models import clear_expired
logger.info('Starting OAuth maintenance task')
clear_expired()
logger.info('Completed OAuth maintenance task')

View File

@ -12,7 +12,8 @@ from InvenTree.api import read_license_file
from InvenTree.api_version import INVENTREE_API_VERSION
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
from InvenTree.version import inventreeApiText, parse_version_text
from users.models import RuleSet, update_group_roles
from users.ruleset import RULESET_NAMES
from users.tasks import update_group_roles
class HTMLAPITests(InvenTreeTestCase):
@ -142,7 +143,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
role_names = roles.keys()
# By default, no permissions are provided
for rule in RuleSet.RULESET_NAMES:
for rule in RULESET_NAMES:
self.assertIn(rule, role_names)
if roles[rule] is None:
@ -167,7 +168,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
roles = response.data['roles']
for rule in RuleSet.RULESET_NAMES:
for rule in RULESET_NAMES:
self.assertIn(rule, roles.keys())
for perm in ['view', 'add', 'change', 'delete']:
@ -297,6 +298,7 @@ class SearchTests(InvenTreeAPITestCase):
'stock',
'order',
'sales_order',
'build',
]
roles = ['build.view', 'part.view']
@ -353,6 +355,82 @@ class SearchTests(InvenTreeAPITestCase):
self.assertNotIn('stockitem', response.data)
self.assertNotIn('build', response.data)
def test_search_filters(self):
"""Test that the regex, whole word, and notes filters are handled correctly."""
SEARCH_TERM = 'some note'
RE_SEARCH_TERM = 'some (.*) note'
response = self.post(
reverse('api-search'),
{'search': SEARCH_TERM, 'limit': 10, 'part': {}, 'build': {}},
expected_code=200,
)
# No build or part results
self.assertEqual(response.data['build']['count'], 0)
self.assertEqual(response.data['part']['count'], 0)
# add the search_notes param
response = self.post(
reverse('api-search'),
{
'search': SEARCH_TERM,
'limit': 10,
'search_notes': True,
'part': {},
'build': {},
},
expected_code=200,
)
# now should have some build results
self.assertEqual(response.data['build']['count'], 4)
# use the regex term
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'part': {},
'build': {},
},
expected_code=200,
)
# No results again
self.assertEqual(response.data['build']['count'], 0)
# add the regex_search param
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'search_regex': True,
'part': {},
'build': {},
},
expected_code=200,
)
# we get our results back!
self.assertEqual(response.data['build']['count'], 4)
# add the search_whole param
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'search_whole': True,
'part': {},
'build': {},
},
expected_code=200,
)
# No results again
self.assertEqual(response.data['build']['count'], 0)
def test_permissions(self):
"""Test that users with insufficient permissions are handled correctly."""
# First, remove all roles
@ -437,24 +515,43 @@ class GeneralApiTests(InvenTreeAPITestCase):
def test_inventree_api_text_fnc(self):
"""Test that the inventreeApiText function works expected."""
latest_version = f'v{INVENTREE_API_VERSION}'
# Normal run
resp = inventreeApiText()
self.assertEqual(len(resp), 10)
self.assertIn(latest_version, resp)
# More responses
resp = inventreeApiText(20)
self.assertEqual(len(resp), 20)
self.assertIn(latest_version, resp)
# Specific version
resp = inventreeApiText(start_version=5)
self.assertEqual(list(resp)[0], 'v5')
self.assertEqual(list(resp)[-1], 'v14')
def test_parse_version_text_fnc(self):
"""Test that api version text is correctly parsed."""
resp = parse_version_text()
# Check that all texts are parsed
self.assertEqual(len(resp), INVENTREE_API_VERSION - 1)
latest_version = INVENTREE_API_VERSION
self.assertTrue(resp[f'v{latest_version}']['latest'])
# All fields except github link should exist for every version
latest_count = 0
for k, v in resp.items():
self.assertEqual('v', k[0], f'Version should start with v: {k}')
self.assertEqual(k, v['version'])
self.assertGreater(len(v['date']), 0, f'Date is missing from {v}')
self.assertGreater(len(v['text']), 0, f'Text is missing from {v}')
self.assertIsNotNone(v['latest'])
latest_count = latest_count + (1 if v['latest'] else 0)
self.assertEqual(1, latest_count, 'Should have a single version marked latest')
# Check that all texts are parsed: v1 and v2 are missing
self.assertEqual(len(resp), INVENTREE_API_VERSION - 2)
def test_api_license(self):
"""Test that the license endpoint is working."""

View File

@ -0,0 +1,13 @@
"""Tests for custom InvenTree management commands."""
from django.core.management import call_command
from django.test import TestCase
class CommandTestCase(TestCase):
"""Test case for custom management commands."""
def test_schema(self):
"""Test the schema generation command."""
output = call_command('schema', file='schema.yml', verbosity=0)
self.assertEqual(output, 'done')

View File

@ -87,7 +87,7 @@ class InvenTreeTaskTests(TestCase):
):
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexist')
def test_task_hearbeat(self):
def test_task_heartbeat(self):
"""Test the task heartbeat."""
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
@ -190,26 +190,41 @@ class InvenTreeTaskTests(TestCase):
from common.models import NotificationEntry, NotificationMessage
# Create a staff user (to ensure notifications are sent)
User.objects.create_user(username='staff', password='staffpass', is_staff=True)
user = User.objects.create_user(
username='staff', password='staffpass', is_staff=False
)
n_tasks = Task.objects.count()
n_entries = NotificationEntry.objects.count()
n_messages = NotificationMessage.objects.count()
test_data = {
'name': 'failed_task',
'func': 'InvenTree.tasks.failed_task',
'group': 'test',
'success': False,
'started': timezone.now(),
'stopped': timezone.now(),
'attempt_count': 10,
}
# Create a 'failed' task in the database
# Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed'
Task.objects.create(
id=n_tasks + 1,
name='failed_task',
func='InvenTree.tasks.failed_task',
group='test',
success=False,
started=timezone.now(),
stopped=timezone.now(),
attempt_count=10,
)
Task.objects.create(id=n_tasks + 1, **test_data)
# A new notification entry should be created
# A new notification entry should NOT be created (yet) - due to lack of permission for the user
self.assertEqual(NotificationEntry.objects.count(), n_entries + 0)
self.assertEqual(NotificationMessage.objects.count(), n_messages + 0)
# Give them all the required staff level permissions
user.is_staff = True
user.save()
# Create a 'failed' task in the database
# Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed'
Task.objects.create(id=n_tasks + 2, **test_data)
# A new notification entry should be created (as the user now has permission to see it)
self.assertEqual(NotificationEntry.objects.count(), n_entries + 1)
self.assertEqual(NotificationMessage.objects.count(), n_messages + 1)

View File

@ -39,6 +39,9 @@ def setup_tracing(
headers: The headers to send with the traces.
resources_input: The resources to send with the traces.
console: Whether to output the traces to the console.
auth: Dict with auth information
is_http: Whether to use HTTP or gRPC for the exporter.
append_http: Whether to append '/v1/traces' to the endpoint.
"""
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
return

View File

@ -11,7 +11,7 @@ from pathlib import Path
from unittest import mock
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.models import Group, Permission, User
from django.db import connections, models
from django.http.response import StreamingHttpResponse
from django.test import TestCase
@ -25,16 +25,23 @@ from plugin import registry
from plugin.models import PluginConfig
def addUserPermission(user, permission):
"""Shortcut function for adding a certain permission to a user."""
perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm)
def addUserPermission(user: User, app_name: str, model_name: str, perm: str) -> None:
"""Add a specific permission for the provided user.
Arguments:
user: The user to add the permission to
app_name: The name of the app (e.g. 'part')
model_name: The name of the model (e.g. 'location')
perm: The permission to add (e.g. 'add', 'change', 'delete', 'view')
"""
# Get the permission object
permission = Permission.objects.get(
content_type__model=model_name, codename=f'{perm}_{model_name}'
)
def addUserPermissions(user, permissions):
"""Shortcut function for adding multiple permissions to a user."""
for permission in permissions:
addUserPermission(user, permission)
# Add the permission to the user
user.user_permissions.add(permission)
user.save()
def getMigrationFileNames(app):

View File

@ -12,6 +12,8 @@ from django.views.generic.base import RedirectView
from allauth.headless.urls import Client, build_urlpatterns
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from flags.urls import flagged_path
from oauth2_provider import urls as oauth2_urls
from sesame.views import LoginView
import build.api
@ -125,6 +127,8 @@ backendpatterns = [
), # Used for (DRF) browsable API auth
path('auth/', auth_request), # Used for proxies to check if user is authenticated
path('accounts/', include('allauth.urls')),
# OAuth2
flagged_path('OIDC', 'o/', include(oauth2_urls)),
path(
'accounts/login/',
RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False),

View File

@ -26,13 +26,21 @@ logger = logging.getLogger('inventree')
# Discover git
try:
from dulwich.errors import NotGitRepository
from dulwich.porcelain import active_branch
from dulwich.repo import Repo
main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent)
main_commit = main_repo[main_repo.head()]
try:
main_branch = active_branch(main_repo)
main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent)
main_commit = main_repo[main_repo.head()]
except NotGitRepository:
# If we are running in a docker container, the repo may not be available
logger.warning('INVE-W3: Could not detect git information.')
main_repo = None
main_commit = None
try:
main_branch = active_branch(main_repo) if main_repo else None
except (KeyError, IndexError):
logger.warning('INVE-W1: Current branch could not be detected.')
main_branch = None
@ -129,11 +137,6 @@ def inventreeAppUrl():
return 'https://docs.inventree.org/app/'
def inventreeCreditsUrl():
"""Return URL for InvenTree credits site."""
return 'https://docs.inventree.org/en/latest/credits/'
def inventreeGithubUrl():
"""Return URL for InvenTree github site."""
return 'https://github.com/InvenTree/InvenTree/'
@ -172,22 +175,27 @@ def parse_version_text():
# Remove first newline on latest version
patched_data[0] = patched_data[0].replace('\n', '', 1)
latest_version = f'v{INVENTREE_API_VERSION}'
version_data = {}
for version in patched_data:
data = version.split('\n')
version_split = data[0].split(' -> ')
version_string = version_split[0].strip()
if version_string == '':
continue
version_detail = (
version_split[1].split(':', 1) if len(version_split) > 1 else ['']
)
new_data = {
'version': version_split[0].strip(),
'version': version_string,
'date': version_detail[0].strip(),
'gh': version_detail[1].strip() if len(version_detail) > 1 else None,
'text': data[1:],
'latest': False,
'latest': latest_version == version_string,
}
version_data[new_data['version']] = new_data
version_data[version_string] = new_data
return version_data
@ -206,7 +214,7 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
# Define the range of versions to return
if start_version == 0:
start_version = INVENTREE_API_VERSION - versions
start_version = INVENTREE_API_VERSION - versions + 1
return {
f'v{a}': version_data.get(f'v{a}', None)

View File

@ -920,8 +920,7 @@ build_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': BuildItem},
MetadataView.as_view(model=BuildItem),
name='api-build-item-metadata',
),
path('', BuildItemDetail.as_view(), name='api-build-item-detail'),
@ -967,8 +966,7 @@ build_api_urls = [
path('unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
path(
'metadata/',
MetadataView.as_view(),
{'model': Build},
MetadataView.as_view(model=Build),
name='api-build-metadata',
),
path('', BuildDetail.as_view(), name='api-build-detail'),

View File

@ -628,8 +628,13 @@ class Build(
self.allocated_stock.delete()
@transaction.atomic
def complete_build(self, user, trim_allocated_stock=False):
"""Mark this build as complete."""
def complete_build(self, user: User, trim_allocated_stock: bool = False):
"""Mark this build as complete.
Arguments:
user: The user who is completing the build
trim_allocated_stock: If True, trim any allocated stock
"""
return self.handle_transition(
self.status,
BuildStatus.COMPLETE.value,
@ -681,6 +686,9 @@ class Build(
# Notify users that this build has been completed
targets = [self.issued_by, self.responsible]
# Also inform anyone subscribed to the assembly part
targets.extend(self.part.get_subscribers())
# Notify those users interested in the parent build
if self.parent:
targets.append(self.parent.issued_by)
@ -817,6 +825,7 @@ class Build(
Build,
exclude=self.issued_by,
content=InvenTreeNotificationBodies.OrderCanceled,
extra_users=self.part.get_subscribers(),
)
trigger_event(BuildEvents.CANCELLED, id=self.pk)
@ -1470,7 +1479,10 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
# Notify the responsible users that the build order has been created
InvenTree.helpers_model.notify_responsible(
instance, sender, exclude=instance.issued_by
instance,
sender,
exclude=instance.issued_by,
extra_users=instance.part.get_subscribers(),
)
else:

View File

@ -40,7 +40,11 @@ from InvenTree.serializers import (
)
from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation
from stock.serializers import LocationBriefSerializer, StockItemSerializerBrief
from stock.serializers import (
LocationBriefSerializer,
StockItemSerializerBrief,
StockStatusCustomSerializer,
)
from stock.status_codes import StockStatus
from users.serializers import OwnerSerializer, UserSerializer
@ -581,11 +585,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
help_text=_('Location for completed build outputs'),
)
status_custom_key = serializers.ChoiceField(
choices=StockStatus.items(custom=True),
default=StockStatus.OK.value,
label=_('Status'),
)
status_custom_key = StockStatusCustomSerializer(default=StockStatus.OK.value)
accept_incomplete_allocation = serializers.BooleanField(
default=False,

View File

@ -262,6 +262,8 @@ def notify_overdue_build_order(bo: build_models.Build):
if bo.responsible:
targets.append(bo.responsible)
targets.extend(bo.part.get_subscribers())
name = _('Overdue Build Order')
context = {

View File

@ -20,9 +20,9 @@ from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from drf_spectacular.utils import OpenApiResponse, extend_schema
from error_report.models import Error
from pint._typing import UnitLike
from rest_framework import permissions, serializers
from rest_framework import serializers
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
from rest_framework.permissions import IsAdminUser
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@ -44,7 +44,15 @@ from InvenTree.mixins import (
RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI,
)
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser
from InvenTree.permissions import (
AllowAnyOrReadScope,
GlobalSettingsPermissions,
IsAdminOrAdminScope,
IsAuthenticatedOrReadScope,
IsStaffOrReadOnlyScope,
IsSuperuserOrSuperScope,
UserSettingsPermissionsOrScope,
)
from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer
@ -134,7 +142,7 @@ class WebhookView(CsrfExemptMixin, APIView):
class CurrencyExchangeView(APIView):
"""API endpoint for displaying currency information."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
serializer_class = None
@extend_schema(responses={200: common.serializers.CurrencyExchangeSerializer})
@ -178,7 +186,7 @@ class CurrencyRefreshView(APIView):
User must be a 'staff' user to access this endpoint
"""
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
serializer_class = None
def post(self, request, *args, **kwargs):
@ -208,6 +216,7 @@ class GlobalSettingsList(SettingsList):
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
def list(self, request, *args, **kwargs):
"""Ensure all global settings are created."""
@ -215,23 +224,6 @@ class GlobalSettingsList(SettingsList):
return super().list(request, *args, **kwargs)
class GlobalSettingsPermissions(permissions.BasePermission):
"""Special permission class to determine if the user is "staff"."""
def has_permission(self, request, view):
"""Check that the requesting user is 'admin'."""
try:
user = request.user
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
# Any other methods require staff access permissions
return user.is_staff
except AttributeError: # pragma: no cover
return False
class GlobalSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "global setting" object.
@ -241,6 +233,7 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
lookup_field = 'key'
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
def get_object(self):
"""Attempt to find a global setting object with the provided key."""
@ -253,14 +246,13 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
key, cache=False, create=True
)
permission_classes = [permissions.IsAuthenticated, GlobalSettingsPermissions]
class UserSettingsList(SettingsList):
"""API endpoint for accessing a list of user settings objects."""
queryset = common.models.InvenTreeUserSetting.objects.all()
serializer_class = common.serializers.UserSettingsSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def list(self, request, *args, **kwargs):
"""Ensure all user settings are created."""
@ -288,19 +280,6 @@ class UserSettingsList(SettingsList):
return queryset
class UserSettingsPermissions(permissions.BasePermission):
"""Special permission class to determine if the user can view / edit a particular setting."""
def has_object_permission(self, request, view, obj):
"""Check if the user that requested is also the object owner."""
try:
user = request.user
except AttributeError: # pragma: no cover
return False
return user == obj.user
class UserSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "user setting" object.
@ -310,6 +289,7 @@ class UserSettingsDetail(RetrieveUpdateAPI):
lookup_field = 'key'
queryset = common.models.InvenTreeUserSetting.objects.all()
serializer_class = common.serializers.UserSettingsSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def get_object(self):
"""Attempt to find a user setting object with the provided key."""
@ -325,14 +305,13 @@ class UserSettingsDetail(RetrieveUpdateAPI):
key, user=self.request.user, cache=False, create=True
)
permission_classes = [UserSettingsPermissions]
class NotificationUserSettingsList(SettingsList):
"""API endpoint for accessing a list of notification user settings objects."""
queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def filter_queryset(self, queryset):
"""Only list settings which apply to the current user."""
@ -354,7 +333,7 @@ class NotificationUserSettingsDetail(RetrieveUpdateAPI):
queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer
permission_classes = [UserSettingsPermissions]
permission_classes = [UserSettingsPermissionsOrScope]
class NotificationMessageMixin:
@ -362,7 +341,7 @@ class NotificationMessageMixin:
queryset = common.models.NotificationMessage.objects.all()
serializer_class = common.serializers.NotificationMessageSerializer
permission_classes = [UserSettingsPermissions]
permission_classes = [UserSettingsPermissionsOrScope]
def get_queryset(self):
"""Return prefetched queryset."""
@ -384,7 +363,7 @@ class NotificationMessageMixin:
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER
@ -437,7 +416,7 @@ class NewsFeedMixin:
queryset = common.models.NewsFeedEntry.objects.all()
serializer_class = common.serializers.NewsFeedEntrySerializer
permission_classes = [IsAdminUser]
permission_classes = [IsAdminOrAdminScope]
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
@ -461,7 +440,7 @@ class ConfigList(ListAPI):
queryset = CONFIG_LOOKUPS
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser]
permission_classes = [IsSuperuserOrSuperScope]
# Specifically disable pagination for this view
pagination_class = None
@ -471,7 +450,7 @@ class ConfigDetail(RetrieveAPI):
"""Detail view for an individual configuration."""
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser]
permission_classes = [IsSuperuserOrSuperScope]
def get_object(self):
"""Attempt to find a config object with the provided key."""
@ -487,7 +466,7 @@ class NotesImageList(ListCreateAPI):
queryset = common.models.NotesImage.objects.all()
serializer_class = common.serializers.NotesImageSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER
@ -505,7 +484,7 @@ class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['code']
@ -518,7 +497,7 @@ class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [IsStaffOrReadOnlyScope]
class CustomUnitList(DataExportViewMixin, ListCreateAPI):
@ -526,7 +505,7 @@ class CustomUnitList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.CustomUnit.objects.all()
serializer_class = common.serializers.CustomUnitSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER
@ -535,14 +514,14 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.CustomUnit.objects.all()
serializer_class = common.serializers.CustomUnitSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [IsStaffOrReadOnlyScope]
class AllUnitList(ListAPI):
class AllUnitList(RetrieveAPI):
"""List of all defined units."""
serializer_class = common.serializers.AllUnitListResponseSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [IsStaffOrReadOnlyScope]
def get(self, request, *args, **kwargs):
"""Return a list of all available units."""
@ -573,7 +552,7 @@ class ErrorMessageList(BulkDeleteMixin, ListAPI):
queryset = Error.objects.all()
serializer_class = common.serializers.ErrorMessageSerializer
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
filter_backends = SEARCH_ORDER_FILTER
@ -589,13 +568,13 @@ class ErrorMessageDetail(RetrieveUpdateDestroyAPI):
queryset = Error.objects.all()
serializer_class = common.serializers.ErrorMessageSerializer
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
class BackgroundTaskOverview(APIView):
"""Provides an overview of the background task queue status."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
serializer_class = None
def get(self, request, fmt=None):
@ -617,7 +596,7 @@ class BackgroundTaskOverview(APIView):
class PendingTaskList(BulkDeleteMixin, ListAPI):
"""Provides a read-only list of currently pending tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.OrmQ.objects.all()
serializer_class = common.serializers.PendingTaskSerializer
@ -626,7 +605,7 @@ class PendingTaskList(BulkDeleteMixin, ListAPI):
class ScheduledTaskList(ListAPI):
"""Provides a read-only list of currently scheduled tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.Schedule.objects.all()
serializer_class = common.serializers.ScheduledTaskSerializer
@ -646,7 +625,7 @@ class ScheduledTaskList(ListAPI):
class FailedTaskList(BulkDeleteMixin, ListAPI):
"""Provides a read-only list of currently failed tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.Failure.objects.all()
serializer_class = common.serializers.FailedTaskSerializer
@ -663,14 +642,14 @@ class FlagList(ListAPI):
queryset = settings.FLAGS
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny]
permission_classes = [AllowAnyOrReadScope]
class FlagDetail(RetrieveAPI):
"""Detail view for an individual feature flag."""
serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny]
permission_classes = [AllowAnyOrReadScope]
def get_object(self):
"""Attempt to find a config object with the provided key."""
@ -686,7 +665,7 @@ class ContentTypeList(ListAPI):
queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['app_label', 'model']
@ -696,10 +675,9 @@ class ContentTypeDetail(RetrieveAPI):
queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
@extend_schema(operation_id='contenttype_retrieve_model')
class ContentTypeModelDetail(ContentTypeDetail):
"""Detail view for a ContentType model."""
@ -714,6 +692,11 @@ class ContentTypeModelDetail(ContentTypeDetail):
raise NotFound()
raise NotFound()
@extend_schema(operation_id='contenttype_retrieve_model')
def get(self, request, *args, **kwargs):
"""Detail view for a ContentType model."""
return super().get(request, *args, **kwargs)
class AttachmentFilter(rest_filters.FilterSet):
"""Filterset for the AttachmentList API endpoint."""
@ -746,7 +729,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER
filterset_class = AttachmentFilter
@ -767,7 +750,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
- Ensure that the user has correct 'delete' permissions for each model
"""
from common.validators import attachment_model_class_from_label
from users.models import check_user_permission
from users.permissions import check_user_permission
model_types = queryset.values_list('model_type', flat=True).distinct()
@ -784,7 +767,7 @@ class AttachmentDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
def destroy(self, request, *args, **kwargs):
"""Check user permissions before deleting an attachment."""
@ -803,11 +786,11 @@ class IconList(ListAPI):
"""List view for available icon packages."""
serializer_class = common.serializers.IconPackageSerializer
permission_classes = [permissions.AllowAny]
permission_classes = [AllowAnyOrReadScope]
def get_queryset(self):
"""Return a list of all available icon packages."""
return get_icon_packs().values()
return list(get_icon_packs().values())
class SelectionListList(ListCreateAPI):
@ -815,7 +798,7 @@ class SelectionListList(ListCreateAPI):
queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
def get_queryset(self):
"""Override the queryset method to include entry count."""
@ -827,7 +810,7 @@ class SelectionListDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
class EntryMixin:
@ -835,7 +818,7 @@ class EntryMixin:
queryset = common.models.SelectionListEntry.objects.all()
serializer_class = common.serializers.SelectionEntrySerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
lookup_url_kwarg = 'entrypk'
def get_queryset(self):
@ -854,22 +837,22 @@ class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
"""Detail view for a SelectionEntry object."""
class DataOutputEndpoint:
class DataOutputEndpointMixin:
"""Mixin class for DataOutput endpoints."""
queryset = common.models.DataOutput.objects.all()
serializer_class = common.serializers.DataOutputSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [IsAuthenticatedOrReadScope]
class DataOutputList(DataOutputEndpoint, BulkDeleteMixin, ListAPI):
class DataOutputList(DataOutputEndpointMixin, BulkDeleteMixin, ListAPI):
"""List view for DataOutput objects."""
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['pk', 'user', 'plugin', 'output_type', 'created']
class DataOutputDetail(DataOutputEndpoint, RetrieveAPI):
class DataOutputDetail(DataOutputEndpointMixin, RetrieveAPI):
"""Detail view for a DataOutput object."""
@ -982,8 +965,7 @@ common_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': common.models.Attachment},
MetadataView.as_view(model=common.models.Attachment),
name='api-attachment-metadata',
),
path('', AttachmentDetail.as_view(), name='api-attachment-detail'),
@ -1008,8 +990,10 @@ common_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': common.models.ProjectCode},
MetadataView.as_view(
model=common.models.ProjectCode,
permission_classes=[IsStaffOrReadOnlyScope],
),
name='api-project-code-metadata',
),
path(

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.20 on 2025-04-07 20:53
import common.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("common", "0037_dataoutput"),
]
operations = [
migrations.AlterField(
model_name="attachment",
name="model_type",
field=models.CharField(
help_text="Target model type for image",
max_length=100,
validators=[common.validators.validate_attachment_model_type],
verbose_name="Model type",
),
),
]

View File

@ -40,15 +40,11 @@ from djmoney.contrib.exchange.models import convert_money
from rest_framework.exceptions import PermissionDenied
from taggit.managers import TaggableManager
import common.currency
import common.validators
import InvenTree.exceptions
import InvenTree.fields
import InvenTree.helpers
import InvenTree.models
import InvenTree.ready
import InvenTree.tasks
import InvenTree.validators
import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from generic.states import ColorEnum
@ -59,6 +55,24 @@ from InvenTree.sanitizer import sanitize_svg
logger = structlog.get_logger('inventree')
class RenderMeta(models.enums.ChoicesMeta):
"""Metaclass for rendering choices."""
choice_fnc = None
@property
def choices(self):
"""Return a list of choices for the enum class."""
fnc = getattr(self, 'choice_fnc', None)
if fnc:
return fnc()
return []
class RenderChoices(models.TextChoices, metaclass=RenderMeta):
"""Class for creating enumerated string choices for schema rendering."""
class MetaMixin(models.Model):
"""A base class for InvenTree models to include shared meta fields.
@ -1766,15 +1780,15 @@ def after_custom_unit_updated(sender, instance, **kwargs):
reload_unit_registry()
def rename_attachment(instance, filename):
def rename_attachment(instance, filename: str):
"""Callback function to rename an uploaded attachment file.
Arguments:
- instance: The Attachment instance
- filename: The original filename of the uploaded file
Args:
instance (Attachment): The Attachment instance for which the file is being renamed.
filename (str): The original filename of the uploaded file.
Returns:
- The new filename for the uploaded file, e.g. 'attachments/<model_type>/<model_id>/<filename>'
str: The new filename for the uploaded file, e.g. 'attachments/<model_type>/<model_id>/<filename>'.
"""
# Remove any illegal characters from the filename
illegal_chars = '\'"\\`~#|!@#$%^&*()[]{}<>?;:+=,'
@ -1811,6 +1825,11 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
verbose_name = _('Attachment')
class ModelChoices(RenderChoices):
"""Model choices for attachments."""
choice_fnc = common.validators.attachment_model_options
def save(self, *args, **kwargs):
"""Custom 'save' method for the Attachment model.
@ -1859,7 +1878,8 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
model_type = models.CharField(
max_length=100,
validators=[common.validators.validate_attachment_model_type],
help_text=_('Target model type for this image'),
verbose_name=_('Model type'),
help_text=_('Target model type for image'),
)
model_id = models.PositiveIntegerField()

View File

@ -6,6 +6,7 @@ from typing import Optional
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db.models import Model
from django.utils.translation import gettext_lazy as _
import structlog
@ -16,6 +17,7 @@ from InvenTree.ready import isImportingData, isRebuildingData
from plugin import registry
from plugin.models import NotificationUserSetting, PluginConfig
from users.models import Owner
from users.permissions import check_user_permission
logger = structlog.get_logger('inventree')
@ -31,7 +33,7 @@ class NotificationMethod:
GLOBAL_SETTING = None
USER_SETTING = None
def __init__(self, obj, category, targets, context) -> None:
def __init__(self, obj: Model, category: str, targets: list, context) -> None:
"""Check that the method is read.
This checks that:
@ -357,8 +359,21 @@ class InvenTreeNotificationBodies:
)
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
"""Send out a notification."""
def trigger_notification(
obj: Model, category: Optional[str] = None, obj_ref: str = 'pk', **kwargs
):
"""Send out a notification.
Args:
obj: The object (model instance) that is triggering the notification
category: The category (label) for the notification
obj_ref: The reference to the object that should be used for the notification
kwargs: Additional arguments to pass to the notification method
"""
# Check if data is importing currently
if isImportingData() or isRebuildingData():
return
targets = kwargs.get('targets')
target_fnc = kwargs.get('target_fnc')
target_args = kwargs.get('target_args', [])
@ -368,10 +383,6 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
delivery_methods = kwargs.get('delivery_methods')
check_recent = kwargs.get('check_recent', True)
# Check if data is importing currently
if isImportingData() or isRebuildingData():
return
# Resolve object reference
refs = [obj_ref, 'pk', 'id', 'uid']
@ -441,26 +452,40 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
)
if target_users:
logger.info("Sending notification '%s' for '%s'", category, str(obj))
# Filter out any users who are inactive, or do not have the required model permissions
valid_users = list(
filter(
lambda u: u and u.is_active and check_user_permission(u, obj, 'view'),
list(target_users),
)
)
# Collect possible methods
if delivery_methods is None:
delivery_methods = storage.methods or []
else:
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
if len(valid_users) > 0:
logger.info(
"Sending notification '%s' for '%s' to %s users",
category,
str(obj),
len(valid_users),
)
for method in delivery_methods:
logger.info("Triggering notification method '%s'", method.METHOD_NAME)
try:
deliver_notification(method, obj, category, target_users, context)
except NotImplementedError as error:
# Allow any single notification method to fail, without failing the others
logger.error(error)
except Exception as error:
logger.error(error)
# Collect possible methods
if delivery_methods is None:
delivery_methods = storage.methods or []
else:
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
# Set delivery flag
common.models.NotificationEntry.notify(category, obj_ref_value)
for method in delivery_methods:
logger.info("Triggering notification method '%s'", method.METHOD_NAME)
try:
deliver_notification(method, obj, category, valid_users, context)
except NotImplementedError as error:
# Allow any single notification method to fail, without failing the others
logger.error(error)
except Exception as error:
logger.error(error)
# Set delivery flag
common.models.NotificationEntry.notify(category, obj_ref_value)
else:
logger.info("No possible users for notification '%s'", category)
@ -484,12 +509,18 @@ def trigger_superuser_notification(plugin: PluginConfig, msg: str):
def deliver_notification(
cls: NotificationMethod, obj, category: str, targets, context: dict
cls: NotificationMethod, obj: Model, category: str, targets: list, context: dict
):
"""Send notification with the provided class.
This:
- Intis the method
Arguments:
cls: The class that should be used to send the notification
obj: The object (model instance) that triggered the notification
category: The category (label) for the notification
targets: List of users that should receive the notification
context: Context dictionary with additional information for the notification
- Initializes the method
- Checks that there are valid targets
- Runs the delivery setup
- Sends notifications either via `send_bulk` or send`

View File

@ -428,7 +428,7 @@ class AllUnitListResponseSerializer(serializers.Serializer):
default_system = serializers.CharField()
available_systems = serializers.ListField(child=serializers.CharField())
available_units = Unit(many=True)
available_units = serializers.DictField(child=Unit())
class ErrorMessageSerializer(InvenTreeModelSerializer):
@ -619,7 +619,7 @@ class AttachmentSerializer(InvenTreeModelSerializer):
def save(self, **kwargs):
"""Override the save method to handle the model_type field."""
from InvenTree.models import InvenTreeAttachmentMixin
from users.models import check_user_permission
from users.permissions import check_user_permission
model_type = self.validated_data.get('model_type', None)

View File

@ -165,6 +165,14 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False,
'validator': bool,
},
'SEARCH_NOTES': {
'name': _('Search Notes'),
'description': _(
"Search queries return results for matches from the item's notes"
),
'default': False,
'validator': bool,
},
'PART_SHOW_QUANTITY_IN_FORMS': {
'name': _('Show Quantity in Forms'),
'description': _('Display available part quantity in some forms'),

View File

@ -28,6 +28,7 @@ from InvenTree.unit_test import (
InvenTreeAPITestCase,
InvenTreeTestCase,
PluginMixin,
addUserPermission,
)
from part.models import Part, PartParameterTemplate
from plugin import registry
@ -1077,6 +1078,9 @@ class NotificationTest(InvenTreeAPITestCase):
"""Tests for bulk deletion of user notifications."""
from error_report.models import Error
# Ensure *this* user has permission to view error reports
addUserPermission(self.user, 'error_report', 'error', 'view')
# Create some notification messages by throwing errors
for _ii in range(10):
Error.objects.create()
@ -1088,7 +1092,7 @@ class NotificationTest(InvenTreeAPITestCase):
# However, one user is marked as inactive
self.assertEqual(messages.count(), 20)
# Only 10 messages related to *this* user
# Only messages related to *this* user
my_notifications = messages.filter(user=self.user)
self.assertEqual(my_notifications.count(), 10)
@ -1169,7 +1173,7 @@ class CommonTest(InvenTreeAPITestCase):
"""Test flag URLs."""
# Not superuser
response = self.get(reverse('api-flag-list'), expected_code=200)
self.assertEqual(len(response.data), 2)
self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
# Turn into superuser
@ -1178,7 +1182,7 @@ class CommonTest(InvenTreeAPITestCase):
# Successful checks
response = self.get(reverse('api-flag-list'), expected_code=200)
self.assertEqual(len(response.data), 2)
self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
self.assertTrue(response.data[0]['conditions'])

View File

@ -516,8 +516,7 @@ manufacturer_part_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': ManufacturerPart},
MetadataView.as_view(model=ManufacturerPart),
name='api-manufacturer-part-metadata',
),
path(
@ -538,8 +537,7 @@ supplier_part_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': SupplierPart},
MetadataView.as_view(model=SupplierPart),
name='api-supplier-part-metadata',
),
path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
@ -574,8 +572,7 @@ company_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': Company},
MetadataView.as_view(model=Company),
name='api-company-metadata',
),
path('', CompanyDetail.as_view(), name='api-company-detail'),
@ -589,8 +586,7 @@ company_api_urls = [
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': Contact},
MetadataView.as_view(model=Contact),
name='api-contact-metadata',
),
path('', ContactDetail.as_view(), name='api-contact-detail'),

View File

@ -2,10 +2,10 @@
from django.urls import reverse
from company.models import Address, Company, Contact, ManufacturerPart, SupplierPart
from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part
from .models import Address, Company, Contact, ManufacturerPart, SupplierPart
from users.permissions import check_user_permission
class CompanyTest(InvenTreeAPITestCase):
@ -384,7 +384,7 @@ class AddressTest(InvenTreeAPITestCase):
self.assertIn(key, response.data)
def test_edit(self):
"""Test editing an object."""
"""Test editing an Address object."""
addr = Address.objects.first()
url = reverse('api-address-detail', kwargs={'pk': addr.pk})
@ -392,6 +392,7 @@ class AddressTest(InvenTreeAPITestCase):
self.patch(url, {'title': 'Hello'}, expected_code=403)
self.assignRole('purchase_order.change')
self.assertTrue(check_user_permission(self.user, Address, 'change'))
self.patch(url, {'title': 'World'}, expected_code=200)
@ -407,7 +408,10 @@ class AddressTest(InvenTreeAPITestCase):
self.delete(url, expected_code=403)
# Assign role, check permission
self.assertFalse(check_user_permission(self.user, Address, 'delete'))
self.assignRole('purchase_order.delete')
self.assertTrue(check_user_permission(self.user, Address, 'delete'))
self.delete(url, expected_code=204)

View File

@ -5,6 +5,9 @@
# Secret key for backend
# Use the environment variable INVENTREE_SECRET_KEY_FILE
#secret_key_file: '/etc/inventree/secret_key.txt'
# Use the environment variable INVENTREE_OIDC_PRIVATE_KEY or INVENTREE_OIDC_PRIVATE_KEY_FILE
#oidc_private_key: '-----BEGIN PRIVATE KEY-----aaa'
#oidc_private_key_file: '/etc/inventree/oidc.key'
# Database backend selection - Configure backend database settings
# Documentation: https://docs.inventree.org/en/latest/start/config/

View File

@ -5,16 +5,16 @@ import inspect
from django.urls import include, path
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions, serializers
from rest_framework import serializers
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
import common.models
import common.serializers
import InvenTree.permissions
from data_exporter.mixins import DataExportViewMixin
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.permissions import IsStaffOrReadOnly
from InvenTree.serializers import EmptySerializer
from .serializers import GenericStateClassSerializer
@ -36,7 +36,7 @@ class StatusView(GenericAPIView):
all available 'StockStatus' codes
"""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = GenericStateClassSerializer
# Override status_class for implementing subclass
@ -96,9 +96,10 @@ class StatusView(GenericAPIView):
class AllStatusViews(StatusView):
"""Endpoint for listing all defined status models."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = EmptySerializer
@extend_schema(operation_id='generic_status_retrieve_all')
def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes."""
from InvenTree.helpers import inheritors
@ -135,7 +136,7 @@ class CustomStateList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['key']
search_fields = ['key', 'name', 'label', 'reference_status']
@ -147,7 +148,7 @@ class CustomStateDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
urlpattern = [

View File

@ -8,9 +8,13 @@ from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.fields import ChoiceField
import InvenTree.ready
from .custom import get_logical_value
@ -73,6 +77,7 @@ class CustomChoiceField(serializers.ChoiceField):
return field_info
@extend_schema_field(OpenApiTypes.INT)
class ExtraCustomChoiceField(CustomChoiceField):
"""Custom Choice Field that returns value of status if empty.
@ -138,14 +143,27 @@ class InvenTreeCustomStatusModelField(models.PositiveIntegerField):
if self.status_class:
validators.append(CustomStatusCodeValidator(status_class=self.status_class))
help_text = _('Additional status information for this item')
if InvenTree.ready.isGeneratingSchema():
help_text = (
help_text
+ '\n\n'
+ '\n'.join(
f'* `{value}` - {label}'
for value, label in self.status_class.items(custom=True)
)
+ "\n\nAdditional custom status keys may be retrieved from the corresponding 'status_retrieve' call."
)
custom_key_field = ExtraInvenTreeCustomStatusModelField(
default=None,
verbose_name=_('Custom status key'),
help_text=_('Additional status information for this item'),
help_text=help_text,
validators=validators,
blank=True,
null=True,
)
cls.add_to_class(f'{name}_custom_key', custom_key_field)
self._custom_key_field = custom_key_field

View File

@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
from django.urls import include, path
from drf_spectacular.utils import extend_schema
from rest_framework import permissions
from rest_framework import permissions, serializers
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.views import APIView
@ -12,6 +12,7 @@ from rest_framework.views import APIView
import importer.models
import importer.registry
import importer.serializers
import InvenTree.permissions
from InvenTree.api import BulkDeleteMixin
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import (
@ -21,14 +22,14 @@ from InvenTree.mixins import (
RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI,
)
from users.models import check_user_permission
from users.permissions import check_user_permission
class DataImporterPermission(permissions.BasePermission):
"""Mixin class for determining if the user has correct permissions."""
def has_permission(self, request, view):
"""Class level permission checks are handled via permissions.IsAuthenticated."""
"""Class level permission checks are handled via InvenTree.permissions.IsAuthenticatedOrReadScope."""
return True
def has_object_permission(self, request, view, obj):
@ -53,13 +54,25 @@ class DataImporterPermissionMixin:
"""Mixin class for checking permissions on DataImporter objects."""
# Default permissions: User must be authenticated
permission_classes = [permissions.IsAuthenticated, DataImporterPermission]
permission_classes = [
InvenTree.permissions.IsAuthenticatedOrReadScope,
DataImporterPermission,
]
class DataImporterModelSerializer(serializers.Serializer):
"""Model references to map info that might get imported."""
serializer = serializers.CharField(read_only=True)
model_type = serializers.CharField(read_only=True)
api_url = serializers.URLField(read_only=True)
class DataImporterModelList(APIView):
"""API endpoint for displaying a list of models available for import."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = DataImporterModelSerializer(many=True)
def get(self, request):
"""Return a list of models available for import."""
@ -101,7 +114,8 @@ class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
class DataImportSessionAcceptFields(APIView):
"""API endpoint to accept the field mapping for a DataImportSession."""
permission_classes = [permissions.IsAuthenticated]
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = None
@extend_schema(
responses={200: importer.serializers.DataImportSessionSerializer(many=False)}

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.20 on 2025-04-07 20:53
from django.db import migrations, models
import importer.validators
class Migration(migrations.Migration):
dependencies = [
("importer", "0003_dataimportsession_field_filters"),
]
operations = [
migrations.AlterField(
model_name="dataimportsession",
name="model_type",
field=models.CharField(
help_text="Target model type for this import session",
max_length=100,
validators=[importer.validators.validate_importer_model_type],
verbose_name="Model Type",
),
),
]

View File

@ -18,6 +18,7 @@ import importer.registry
import importer.tasks
import importer.validators
import InvenTree.helpers
from common.models import RenderChoices
from importer.status_codes import DataImportStatusCode
logger = structlog.get_logger('inventree')
@ -38,6 +39,11 @@ class DataImportSession(models.Model):
field_filters: JSONField for field filter values - optional field API filters
"""
class ModelChoices(RenderChoices):
"""Model choices for data import sessions."""
choice_fnc = importer.registry.supported_models
@staticmethod
def get_api_url():
"""Return the API URL associated with the DataImportSession model."""
@ -77,6 +83,8 @@ class DataImportSession(models.Model):
blank=False,
max_length=100,
validators=[importer.validators.validate_importer_model_type],
verbose_name=_('Model Type'),
help_text=_('Target model type for this import session'),
)
status = models.PositiveIntegerField(

View File

@ -72,9 +72,8 @@ def extract_column_names(data_file) -> list:
headers = []
for idx, header in enumerate(data.headers):
header = header.strip()
if header:
header = str(header).strip()
headers.append(header)
else:
# If the header is empty, generate a default header

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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