mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-18 04:45:12 -06:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b564929d2 | ||
|
|
8576fbbade | ||
|
|
6045925ebe | ||
|
|
3715c42fed | ||
|
|
4c6e3490c0 | ||
|
|
91c095a011 | ||
|
|
d42e3087a8 | ||
|
|
4f7a12bd70 | ||
|
|
5048c1d667 | ||
|
|
0a522709b1 | ||
|
|
c32362456b | ||
|
|
4d07a49dfd | ||
|
|
ef5fd93207 | ||
|
|
df7204a334 | ||
|
|
a70382ac7a | ||
|
|
eed6223187 | ||
|
|
cab7a06146 | ||
|
|
40245a6c4a | ||
|
|
3cb806d20a | ||
|
|
8f1bf95463 | ||
|
|
4019dc9c9c | ||
|
|
70f17997eb | ||
|
|
2d773a7b3e | ||
|
|
39211ff4b6 | ||
|
|
e37ff5c3d5 | ||
|
|
6bd32c9236 | ||
|
|
04aec83e95 | ||
|
|
b57d035f7f | ||
|
|
3ac49441ca | ||
|
|
156c3cc9b2 | ||
|
|
52a26c9887 | ||
|
|
667e0a1bee |
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -127,7 +127,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # pin@v3.7.1
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # pin@v3.8.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # pin@v3.7.0
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Create SBOM for frontend
|
||||
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # pin@v0
|
||||
uses: anchore/sbom-action@df80a981bc6edbc4e220a492d3cbe9f5547a6e75 # pin@v0
|
||||
with:
|
||||
artifact-name: frontend-build.spdx
|
||||
path: src/frontend
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
zip -r ../frontend-build.zip * .vite
|
||||
- name: Attest Build Provenance
|
||||
id: attest
|
||||
uses: actions/attest-build-provenance@c4fbc648846ca6f503a13a2281a5e7b98aa57202 # pin@v1
|
||||
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # pin@v1
|
||||
with:
|
||||
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
|
||||
|
||||
|
||||
2
.github/workflows/scorecard.yaml
vendored
2
.github/workflows/scorecard.yaml
vendored
@@ -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@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
|
||||
uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
git reset --hard
|
||||
git reset HEAD~
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@a9ffb7d5ac46eca1bb1f06656bf888b39462f161 # pin@v2
|
||||
uses: crowdin/github-action@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # pin@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
# InvenTree environment variables for docker compose deployment
|
||||
# For a full list of the available configuration options, refer to the InvenTree documentation:
|
||||
# https://docs.inventree.org/en/stable/start/config/
|
||||
|
||||
# Specify the name of the docker-compose project
|
||||
COMPOSE_PROJECT_NAME=inventree
|
||||
|
||||
# InvenTree version tag (e.g. 'stable' / 'latest' / 'x.x.x')
|
||||
INVENTREE_TAG=stable
|
||||
|
||||
# InvenTree server URL - update this to match your host
|
||||
INVENTREE_SITE_URL="http://inventree.localhost"
|
||||
|
||||
# Specify the location of the external data volume
|
||||
# By default, placed in local directory 'inventree-data'
|
||||
INVENTREE_EXT_VOLUME=./inventree-data
|
||||
|
||||
# Ensure debug is false for a production setup
|
||||
INVENTREE_DEBUG=False
|
||||
INVENTREE_LOG_LEVEL=WARNING
|
||||
|
||||
# InvenTree admin account details
|
||||
# Un-comment (and complete) these lines to auto-create an admin acount
|
||||
#INVENTREE_ADMIN_USER=
|
||||
#INVENTREE_ADMIN_PASSWORD=
|
||||
#INVENTREE_ADMIN_EMAIL=
|
||||
|
||||
# Database configuration options
|
||||
INVENTREE_DB_ENGINE=postgresql
|
||||
INVENTREE_DB_NAME=inventree
|
||||
INVENTREE_DB_HOST=inventree-db
|
||||
INVENTREE_DB_PORT=5432
|
||||
|
||||
# Database credentials - These should be changed from the default values!
|
||||
INVENTREE_DB_USER=pguser
|
||||
INVENTREE_DB_PASSWORD=pgpassword
|
||||
|
||||
# Redis cache setup
|
||||
# Refer to settings.py for other cache options
|
||||
INVENTREE_CACHE_ENABLED=True
|
||||
INVENTREE_CACHE_HOST=inventree-cache
|
||||
INVENTREE_CACHE_PORT=6379
|
||||
|
||||
# Options for gunicorn server
|
||||
INVENTREE_GUNICORN_TIMEOUT=90
|
||||
|
||||
# Enable custom plugins?
|
||||
INVENTREE_PLUGINS_ENABLED=True
|
||||
|
||||
# Run migrations automatically?
|
||||
INVENTREE_AUTO_UPDATE=True
|
||||
|
||||
# Image tag that should be used
|
||||
INVENTREE_TAG=stable
|
||||
# InvenTree superuser account details
|
||||
# Un-comment (and complete) these lines to auto-create an admin acount
|
||||
#INVENTREE_ADMIN_USER=
|
||||
#INVENTREE_ADMIN_PASSWORD=
|
||||
#INVENTREE_ADMIN_EMAIL=
|
||||
|
||||
# Site URL - update this to match your host
|
||||
INVENTREE_SITE_URL="http://inventree.localhost"
|
||||
# Database configuration options
|
||||
# DO NOT CHANGE THESE SETTINGS (unless you really know what you are doing)
|
||||
INVENTREE_DB_ENGINE=postgresql
|
||||
INVENTREE_DB_NAME=inventree
|
||||
INVENTREE_DB_HOST=inventree-db
|
||||
INVENTREE_DB_PORT=5432
|
||||
|
||||
COMPOSE_PROJECT_NAME=inventree
|
||||
# Database credentials - These should be changed from the default values!
|
||||
# Note: These are *NOT* the InvenTree server login credentials,
|
||||
# they are the credentials for the PostgreSQL database
|
||||
INVENTREE_DB_USER=pguser
|
||||
INVENTREE_DB_PASSWORD=pgpassword
|
||||
|
||||
# Redis cache setup
|
||||
# Refer to the documentation for other cache options
|
||||
INVENTREE_CACHE_ENABLED=True
|
||||
INVENTREE_CACHE_HOST=inventree-cache
|
||||
INVENTREE_CACHE_PORT=6379
|
||||
|
||||
# Options for gunicorn server
|
||||
INVENTREE_GUNICORN_TIMEOUT=90
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
# - INVENTREE_SERVER: The internal URL of the InvenTree container (default: http://inventree-server:8000)
|
||||
#
|
||||
# Note that while this file is a good starting point, it may need to be modified to suit your specific requirements
|
||||
#
|
||||
# Ref to the Caddyfile documentation: https://caddyserver.com/docs/caddyfile
|
||||
|
||||
|
||||
# Logging configuration for Caddy
|
||||
(log_common) {
|
||||
log {
|
||||
output file /var/log/caddy/{args[0]}.access.log
|
||||
}
|
||||
}
|
||||
|
||||
# CORS headers control (used for static and media files)
|
||||
(cors-headers) {
|
||||
header Allow GET,HEAD,OPTIONS
|
||||
header Access-Control-Allow-Origin *
|
||||
@@ -25,8 +29,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
# Change the host to your domain (this will serve at inventree.localhost)
|
||||
{$INVENTREE_SITE_URL:inventree.localhost} {
|
||||
# The default server address is configured in the .env file
|
||||
# If not specified, the default address is used - http://inventree.localhost
|
||||
# If you need to listen on multiple addresses, or use a different port, you can modify this section directly
|
||||
{$INVENTREE_SITE_URL:http://inventree.localhost} {
|
||||
import log_common inventree
|
||||
|
||||
encode gzip
|
||||
@@ -35,6 +41,7 @@
|
||||
max_size 100MB
|
||||
}
|
||||
|
||||
# Handle static request files
|
||||
handle_path /static/* {
|
||||
import cors-headers static
|
||||
|
||||
@@ -42,18 +49,29 @@
|
||||
file_server
|
||||
}
|
||||
|
||||
# Handle media request files
|
||||
handle_path /media/* {
|
||||
import cors-headers media
|
||||
|
||||
root * /var/www/media
|
||||
file_server
|
||||
|
||||
# Force download of media files (for security)
|
||||
# Comment out this line if you do not want to force download
|
||||
header Content-Disposition attachment
|
||||
|
||||
# Authentication is handled by the forward_auth directive
|
||||
# This is required to ensure that media files are only accessible to authenticated users
|
||||
forward_auth {$INVENTREE_SERVER:"http://inventree-server:8000"} {
|
||||
uri /auth/
|
||||
}
|
||||
}
|
||||
|
||||
reverse_proxy {$INVENTREE_SERVER:"http://inventree-server:8000"}
|
||||
# All other requests are proxied to the InvenTree server
|
||||
reverse_proxy {$INVENTREE_SERVER:"http://inventree-server:8000"} {
|
||||
|
||||
# If you are running behind another proxy, you may need to specify 'trusted_proxies'
|
||||
# Ref: https://caddyserver.com/docs/json/apps/http/servers/trusted_proxies/
|
||||
# trusted_proxies ...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,14 +292,15 @@ function stop_inventree() {
|
||||
}
|
||||
|
||||
function update_or_install() {
|
||||
set -e
|
||||
|
||||
# Set permissions so app user can write there
|
||||
chown ${APP_USER}:${APP_GROUP} ${APP_HOME} -R
|
||||
|
||||
# Run update as app user
|
||||
echo "# POI12| Updating InvenTree"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install uv wheel"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update --uv | sed -e 's/^/# POI12| u | /;'"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install wheel"
|
||||
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update | sed -e 's/^/# POI12| u | /;'"
|
||||
|
||||
# Make sure permissions are correct again
|
||||
echo "# POI12| Set permissions for data dir and media: ${DATA_DIR}"
|
||||
|
||||
@@ -44,6 +44,27 @@ This error occurs because your installed python version is not up to date. We [r
|
||||
|
||||
You (or your system administrator) needs to update python to meet the minimum requirements for InvenTree.
|
||||
|
||||
### InvenTree Site URL
|
||||
|
||||
During the installation or update process, you may see an error similar to:
|
||||
|
||||
```
|
||||
'No CSRF_TRUSTED_ORIGINS specified. Please provide a list of trusted origins, or specify INVENTREE_SITE_URL'
|
||||
```
|
||||
|
||||
If you see this error, it means that the `INVENTREE_SITE_URL` environment variable has not correctly specified. Refer to the [configuration documentation](./start/config.md#site-url) for more information.
|
||||
|
||||
### Login Issues
|
||||
|
||||
If you have successfully started the InvenTree server, but are experiencing issues logging in, it may be due to the security interactions between your web browser and the server. While the default configuration should work for most users, if you do experience login issues, ensure that your [server access settings](./start/config.md#server-access) are correctly configured.
|
||||
|
||||
### Session Cookies
|
||||
|
||||
The [0.17.0 release](https://github.com/inventree/InvenTree/releases/tag/0.17.0) included [a change to the way that session cookies were handled](https://github.com/inventree/InvenTree/pull/8269). This change may cause login issues for existing InvenTree installs which are upgraded from an older version. System administrators should refer to the [server access settings](./start/config.md#server-access) and ensure that the following settings are correctly configured:
|
||||
|
||||
- **INVENTREE_SESSION_COOKIE_SECURE**: `False`
|
||||
- **INVENTREE_COOKIE_SAMESITE**: `False`
|
||||
|
||||
## Update Issues
|
||||
|
||||
Sometimes, users may encounter unexpected error messages when updating their InvenTree installation to a newer version.
|
||||
|
||||
@@ -61,13 +61,6 @@ The following basic options are available:
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_SITE_URL | site_url | Specify a fixed site URL | *Not specified* |
|
||||
| INVENTREE_DEBUG | debug | Enable [debug mode](./intro.md#debug-mode) | True |
|
||||
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable [query count logging](https://github.com/bradmontgomery/django-querycount) in the terminal | False |
|
||||
| INVENTREE_DEBUG_SHELL | debug_shell | Enable [administrator shell](https://github.com/djk2/django-admin-shell) (only in debug mode) | False |
|
||||
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
|
||||
| INVENTREE_JSON_LOG | json_log | log as json | False |
|
||||
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
|
||||
| INVENTREE_WRITE_LOG | write_log | Enable writing of log messages to file at config base | False |
|
||||
| INVENTREE_TIMEZONE | timezone | Server timezone | UTC |
|
||||
| INVENTREE_ADMIN_ENABLED | admin_enabled | Enable the [django administrator interface]({% include "django.html" %}/ref/contrib/admin/) | True |
|
||||
| INVENTREE_ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin |
|
||||
@@ -78,6 +71,8 @@ The following basic options are available:
|
||||
|
||||
The *INVENTREE_SITE_URL* option defines the base URL for the InvenTree server. This is a critical setting, and it is required for correct operation of the server. If not specified, the server will attempt to determine the site URL automatically - but this may not always be correct!
|
||||
|
||||
The site URL is the URL that users will use to access the InvenTree server. For example, if the server is accessible at `https://inventree.example.com`, the site URL should be set to `https://inventree.example.com`. Note that this is not necessarily the same as the internal URL that the server is running on - the internal URL will depend entirely on your server configuration and may be obscured by a reverse proxy or other such setup.
|
||||
|
||||
### Timezone
|
||||
|
||||
By default, the InvenTree server is configured to use the UTC timezone. This can be adjusted to your desired local timezone. You can refer to [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of available timezones. Use the values specified in the *TZ Identifier* column in the linked page.
|
||||
@@ -90,6 +85,36 @@ By default, the InvenTree server will not automatically apply database migration
|
||||
|
||||
With "auto update" enabled, the InvenTree server will automatically apply database migrations as required. To enable automatic database updates, set `INVENTREE_AUTO_UPDATE` to `True`.
|
||||
|
||||
## Debugging and Logging Options
|
||||
|
||||
The following debugging / logging options are available:
|
||||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_DEBUG | debug | Enable [debug mode](./intro.md#debug-mode) | False |
|
||||
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable [query count logging](https://github.com/bradmontgomery/django-querycount) in the terminal | False |
|
||||
| INVENTREE_DEBUG_SHELL | debug_shell | Enable [administrator shell](https://github.com/djk2/django-admin-shell) (only in debug mode) | False |
|
||||
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
|
||||
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
|
||||
| INVENTREE_JSON_LOG | json_log | log as json | False |
|
||||
| INVENTREE_WRITE_LOG | write_log | Enable writing of log messages to file at config base | False |
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enabling the `INVENTREE_DEBUG` setting will turn on [Django debug mode]({% include "django.html" %}/ref/settings/#debug). This mode is intended for development purposes, and should not be enabled in a production environment. Read more about [InvenTree debug mode](./intro.md#debug-mode).
|
||||
|
||||
### Query Count Logging
|
||||
|
||||
Enabling the `INVENTREE_DEBUG_QUERYCOUNT` setting will log the number of database queries executed for each page load. This can be useful for identifying performance bottlenecks in the InvenTree server. Note that this setting is only available if `INVENTREE_DEBUG` is also enabled.
|
||||
|
||||
### Debug Shell
|
||||
|
||||
Enabling the `INVENTREE_DEBUG_SHELL` setting will allow the use of the [administrator shell](https://github.com/djk2/django-admin-shell). Note that this setting is only available if `INVENTREE_DEBUG` is also enabled, and is only accessible to superuser accounts.
|
||||
|
||||
### Database Logging
|
||||
|
||||
Enabling the `INVENTREE_DB_LOGGING` setting will log all database queries to the terminal. This can be useful for debugging database-related issues.
|
||||
|
||||
## Server Access
|
||||
|
||||
Depending on how your InvenTree installation is configured, you will need to pay careful attention to the following settings. If you are running your server behind a proxy, or want to adjust support for [CORS requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), one or more of the following settings may need to be adjusted.
|
||||
@@ -116,12 +141,13 @@ Depending on how your InvenTree installation is configured, you will need to pay
|
||||
| INVENTREE_CORS_ALLOW_CREDENTIALS | cors.allow_credentials | Allow cookies in cross-site requests | `True` |
|
||||
| INVENTREE_USE_X_FORWARDED_HOST | use_x_forwarded_host | Use forwarded host header | `False` |
|
||||
| INVENTREE_USE_X_FORWARDED_PORT | use_x_forwarded_port | Use forwarded port header | `False` |
|
||||
| INVENTREE_USE_X_FORWARDED_PROTO | use_x_forwarded_proto | Use forwarded protocol header | `False` |
|
||||
| INVENTREE_SESSION_COOKIE_SECURE | cookie.secure | Enforce secure session cookies | `False` |
|
||||
| INVENTREE_COOKIE_SAMESITE | cookie.samesite | Session cookie mode. Must be one of `Strict | Lax | None | False`. Refer to the [mozilla developer docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) and the [django documentation]({% include "django.html" %}/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE) for more information. | False |
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Note that in [debug mode](./intro.md#debug-mode), some of the above settings are automatically adjusted to allow for easier development:
|
||||
Note that in [debug mode](./intro.md#debug-mode), some of the above settings are automatically adjusted to allow for easier development. The following settings are internally overridden in debug mode with the values specified below:
|
||||
|
||||
| Setting | Value in Debug Mode | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -130,13 +156,41 @@ Note that in [debug mode](./intro.md#debug-mode), some of the above settings are
|
||||
| `INVENTREE_COOKIE_SAMESITE` | `False` | Disable all same-site cookie checks in debug mode |
|
||||
| `INVENTREE_SESSION_COOKIE_SECURE` | `False` | Disable secure session cookies in debug mode (allow non-https cookies) |
|
||||
|
||||
### INVENTREE_COOKIE_SAMESITE vs INVENTREE_SESSION_COOKIE_SECURE
|
||||
### Cookie Settings
|
||||
|
||||
Note that if you set the `INVENTREE_COOKIE_SAMESITE` to `None`, then `INVENTREE_SESSION_COOKIE_SECURE` is automatically set to `True` to ensure that the session cookie is secure! This means that the session cookie will only be sent over secure (https) connections.
|
||||
|
||||
### Proxy Settings
|
||||
### Proxy Considerations
|
||||
|
||||
If you are running InvenTree behind a proxy, or forwarded HTTPS connections, you will need to ensure that the InvenTree server is configured to listen on the correct host and port. You will likely have to adjust the `INVENTREE_ALLOWED_HOSTS` setting to ensure that the server will accept requests from the proxy.
|
||||
|
||||
Additionally, you may need to configure the following header to ensure that the InvenTree server is watching for information forwarded by the proxy:
|
||||
|
||||
**X-Forwarded-Host**
|
||||
|
||||
By default, InvenTree *will not* look at the [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) header.
|
||||
If you are running InvenTree behind a proxy which obscures the upstream host information, you will need to ensure that the `INVENTREE_USE_X_FORWARDED_HOST` setting is enabled. This will ensure that the InvenTree server uses the forwarded host header for processing requests.
|
||||
|
||||
You can also refer to the [Django documentation]({% include "django.html" %}/ref/settings/#secure-proxy-ssl-header) for more information on this header.
|
||||
|
||||
**X-Forwarded-Port**
|
||||
|
||||
InvenTree provides support for the `X-Forwarded-Port` header, which can be used to determine if the incoming request is using a forwarded port. If you are running InvenTree behind a proxy which forwards port information, you should ensure that the `INVENTREE_USE_X_FORWARDED_PORT` setting is enabled.
|
||||
|
||||
Note: This header is overridden by the `X-Forwarded-Host` header.
|
||||
|
||||
You can also refer to the [Django documentation]({% include "django.html" %}/ref/settings/#use-x-forwarded-port) for more information on this header.
|
||||
|
||||
**X-Forwarded-Proto**
|
||||
|
||||
InvenTree provides support for the [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header, which can be used to determine if the incoming request is using HTTPS, even if the server is running behind a proxy which forwards SSL connections. If you are running InvenTree behind a proxy which forwards SSL connections, you should ensure that the `INVENTREE_USE_X_FORWARDED_PROTO` setting is enabled.
|
||||
|
||||
You can also refer to the [Django documentation]({% include "django.html" %}/ref/settings/#use-x-forwarded-host) for more information on this header.
|
||||
|
||||
Proxy configuration can be complex, and any configuration beyond the basic setup is outside the scope of this documentation. You should refer to the documentation for the specific proxy server you are using.
|
||||
|
||||
Refer to the [proxy server documentation](./processes.md#proxy-server) for more information.
|
||||
|
||||
If you are running InvenTree behind another proxy, you will need to ensure that the InvenTree server is configured to listen on the correct host and port. You will likely have to adjust the `INVENTREE_ALLOWED_HOSTS` setting to ensure that the server will accept requests from the proxy.
|
||||
|
||||
## Admin Site
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ The following guide provides a streamlined production InvenTree installation, wi
|
||||
!!! warning "Docker Knowledge Required"
|
||||
This guide assumes that you are reasonably comfortable with the basic concepts of docker and docker compose.
|
||||
|
||||
### Frequently Asked Questions
|
||||
|
||||
If you encounter any issues during the installation process, please refer first to the [FAQ](../faq.md) for common problems and solutions.
|
||||
|
||||
## Docker Installation
|
||||
|
||||
### Required Files
|
||||
|
||||
@@ -23,6 +23,10 @@ The above command may need to be run with `sudo` permissions, depending on the s
|
||||
sudo wget -qO install.sh https://get.inventree.org && sudo bash install.sh
|
||||
```
|
||||
|
||||
#### Frequently Asked Questions
|
||||
|
||||
If you encounter any issues during the installation process, please refer first to the [FAQ](../faq.md) for common problems and solutions.
|
||||
|
||||
### File Locations
|
||||
|
||||
The installer creates the following directories:
|
||||
|
||||
@@ -46,7 +46,6 @@ invoke update
|
||||
|
||||
This step ensures that the required database tables exist, and are at the correct schema version, which must be the case before data can be imported.
|
||||
|
||||
|
||||
### Import Data
|
||||
|
||||
The new database should now be correctly initialized with the correct table structures required to import the data. Run the following command to load the databased dump file into the new database.
|
||||
@@ -64,6 +63,18 @@ invoke import-records -c -f data.json
|
||||
!!! warning "Character Encoding"
|
||||
If the character encoding of the data file does not exactly match the target database, the import operation may not succeed. In this case, some manual editing of the database JSON file may be required.
|
||||
|
||||
### Copy Media Files
|
||||
|
||||
Any media files (images, documents, etc) that were stored in the original database must be copied to the new database. In a typical InvenTree installation, these files are stored in the `media` subdirectory of the InvenTree data location.
|
||||
|
||||
Copy the entire directory tree from the original InvenTree installation to the new InvenTree installation.
|
||||
|
||||
!!! warning "File Ownership"
|
||||
Ensure that the file ownership and permissions are correctly set on the copied files. The InvenTree server process **must** have read / write access to these files. If not, the server will not be able to serve the media files correctly, and the user interface may not function as expected.
|
||||
|
||||
!!! warning "Directory Structure"
|
||||
The expected locations of each file is stored in the database, and if the file paths are not correct, the media files will not be displayed correctly in the user interface. Thus, it is important that the files are transferred across to the new installation in the same directory structure.
|
||||
|
||||
## Migrating Data to Newer Version
|
||||
|
||||
If you are updating from an older version of InvenTree to a newer version, the migration steps outlined above *do not apply*.
|
||||
|
||||
@@ -44,6 +44,12 @@ Further, it provides an authentication endpoint for accessing files in the `/sta
|
||||
|
||||
Finally, it provides a [Let's Encrypt](https://letsencrypt.org/) endpoint for automatic SSL certificate generation and renewal.
|
||||
|
||||
### Proxy Functionality
|
||||
|
||||
#### API and Web Requests
|
||||
|
||||
All API and web requests are reverse-proxied to the InvenTree django server. This allows the InvenTree web server to be accessed via a standard HTTP/HTTPS port, and allows the proxy server to handle SSL termination.
|
||||
|
||||
#### Static Files
|
||||
|
||||
Static files can be served without any need for authentication. In fact, they must be accessible *without* authentication, otherwise the unauthenticated views (such as the login screen) will not function correctly.
|
||||
@@ -52,15 +58,34 @@ Static files can be served without any need for authentication. In fact, they mu
|
||||
|
||||
It is highly recommended that the *media* files are served behind an authentication layer. This is because the media files are user-uploaded, and may contain sensitive information. Most modern web servers provide a way to serve files behind an authentication layer.
|
||||
|
||||
#### Example Configuration
|
||||
### Proxy Configuration
|
||||
|
||||
The [docker production example](./docker.md) provides an example using [Caddy](https://caddyserver.com) to serve *static* and *media* files, and redirecting other requests to the InvenTree web server itself.
|
||||
We provide some *sample* configuration files for getting your proxy server off the ground. The exact setup and configuration of your proxy server will depend on your specific requirements, and the software you choose to use. You may be integrating InvenTree with an existing web server, and the configuration may be different to the provided examples.
|
||||
|
||||
Caddy is a modern web server which is easy to configure and provides a number of useful features, including automatic SSL certificate generation.
|
||||
#### Example Configurations
|
||||
|
||||
#### Alternatives to Caddy
|
||||
**Caddy**
|
||||
|
||||
An alternative is to run nginx as the reverse proxy. A sample configuration file is provided in the `./contrib/container/` source directory.
|
||||
The [docker production example](./docker.md) provides an example using [Caddy](https://caddyserver.com) to serve *static* and *media* files, and redirecting other requests to the InvenTree web server itself. Caddy is a modern web server which is easy to configure and provides a number of useful features, including automatic SSL certificate generation.
|
||||
|
||||
You can find the sample Caddy configuration [here]({{ sourcefile("contrib/container/Caddyfile") }}).
|
||||
|
||||
**Nginx**
|
||||
|
||||
An alternative is to run nginx as the reverse proxy. A sample configuration file is provided [here]({{ sourcefile("contrib/container/nginx.conf") }}).
|
||||
|
||||
#### Extending the Proxy Configuration
|
||||
|
||||
You may wish to extend the proxy configuration to include additional features, based on your particular requirements. Some examples of where additional configuration may be required include:
|
||||
|
||||
- **Upstream Proxy**: You may be running the InvenTree server behind another proxy server, and need to configure the proxy server to forward requests to the upstream proxy.
|
||||
- **Authentication**: You may wish to add an authentication layer to the proxy server, to restrict access to the InvenTree web interface.
|
||||
- **SSL Termination**: You may wish to terminate SSL connections at the proxy server, and forward unencrypted traffic to the InvenTree web server.
|
||||
- **Load Balancing**: You may wish to run multiple instances of the InvenTree web server, and use the proxy server to load balance between them.
|
||||
- **Custom Error Pages**: You may wish to provide custom error pages for certain HTTP status codes.
|
||||
|
||||
!!! warning "No Support"
|
||||
We do not provide support for configuring your proxy server. The configuration of the proxy server is outside the scope of this documentation. If you require assistance with configuring your proxy server, please refer to the documentation for the specific software you are using.
|
||||
|
||||
#### Integrating with Existing Proxy
|
||||
|
||||
|
||||
@@ -319,9 +319,9 @@ mkdocs-macros-plugin==1.3.7 \
|
||||
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
|
||||
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material==9.5.48 \
|
||||
--hash=sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db \
|
||||
--hash=sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8
|
||||
mkdocs-material==9.5.49 \
|
||||
--hash=sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d \
|
||||
--hash=sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material-extensions==1.3.1 \
|
||||
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 293
|
||||
INVENTREE_API_VERSION = 294
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
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
|
||||
- Adds new fields to the supplier barcode API endpoints
|
||||
|
||||
|
||||
40
src/backend/InvenTree/InvenTree/auth_override_views.py
Normal file
40
src/backend/InvenTree/InvenTree/auth_override_views.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Overrides for registration view."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth.account import app_settings as allauth_account_settings
|
||||
from dj_rest_auth.app_settings import api_settings
|
||||
from dj_rest_auth.registration.views import RegisterView
|
||||
|
||||
|
||||
class CustomRegisterView(RegisterView):
|
||||
"""Registers a new user.
|
||||
|
||||
Accepts the following POST parameters: username, email, password1, password2.
|
||||
"""
|
||||
|
||||
# Fixes https://github.com/inventree/InvenTree/issues/8707
|
||||
# This contains code from dj-rest-auth 7.0 - therefore the version was pinned
|
||||
def get_response_data(self, user):
|
||||
"""Override to fix check for auth_model."""
|
||||
if (
|
||||
allauth_account_settings.EMAIL_VERIFICATION
|
||||
== allauth_account_settings.EmailVerificationMethod.MANDATORY
|
||||
):
|
||||
return {'detail': _('Verification e-mail sent.')}
|
||||
|
||||
if api_settings.USE_JWT:
|
||||
data = {
|
||||
'user': user,
|
||||
'access': self.access_token,
|
||||
'refresh': self.refresh_token,
|
||||
}
|
||||
return api_settings.JWT_SERIALIZER(
|
||||
data, context=self.get_serializer_context()
|
||||
).data
|
||||
elif self.token_model:
|
||||
# Only change in this block is below
|
||||
return api_settings.TOKEN_SERIALIZER(
|
||||
user.api_tokens.last(), context=self.get_serializer_context()
|
||||
).data
|
||||
return None
|
||||
@@ -20,7 +20,9 @@ from allauth_2fa.utils import user_has_valid_totp_device
|
||||
from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Field, Layout
|
||||
from dj_rest_auth.registration.serializers import RegisterSerializer
|
||||
from dj_rest_auth.registration.serializers import (
|
||||
RegisterSerializer as DjRestRegisterSerializer,
|
||||
)
|
||||
from rest_framework import serializers
|
||||
|
||||
import InvenTree.helpers_model
|
||||
@@ -385,16 +387,11 @@ class CustomSocialAccountAdapter(
|
||||
|
||||
|
||||
# override dj-rest-auth
|
||||
class CustomRegisterSerializer(RegisterSerializer):
|
||||
"""Override of serializer to use dynamic settings."""
|
||||
class RegisterSerializer(DjRestRegisterSerializer):
|
||||
"""Registration requires email, password (twice) and username."""
|
||||
|
||||
email = serializers.EmailField()
|
||||
|
||||
def __init__(self, instance=None, data=..., **kwargs):
|
||||
"""Check settings to influence which fields are needed."""
|
||||
kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED')
|
||||
super().__init__(instance, data, **kwargs)
|
||||
|
||||
def save(self, request):
|
||||
"""Override to check if registration is open."""
|
||||
if registration_enabled():
|
||||
|
||||
@@ -620,12 +620,10 @@ REST_AUTH = {
|
||||
'TOKEN_MODEL': 'users.models.ApiToken',
|
||||
'TOKEN_CREATOR': 'users.models.default_create_token',
|
||||
'USE_JWT': USE_JWT,
|
||||
'REGISTER_SERIALIZER': 'InvenTree.forms.RegisterSerializer',
|
||||
}
|
||||
|
||||
OLD_PASSWORD_FIELD_ENABLED = True
|
||||
REST_AUTH_REGISTER_SERIALIZERS = {
|
||||
'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'
|
||||
}
|
||||
|
||||
# JWT settings - rest_framework_simplejwt
|
||||
if USE_JWT:
|
||||
@@ -1106,6 +1104,12 @@ if SITE_URL:
|
||||
print(f"Invalid SITE_URL value: '{SITE_URL}'. InvenTree server cannot start.")
|
||||
sys.exit(-1)
|
||||
|
||||
else:
|
||||
logger.warning('No SITE_URL specified. Some features may not work correctly')
|
||||
logger.warning(
|
||||
'Specify a SITE_URL in the configuration file or via an environment variable'
|
||||
)
|
||||
|
||||
# Enable or disable multi-site framework
|
||||
SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False)
|
||||
|
||||
@@ -1220,10 +1224,24 @@ SESSION_COOKIE_SECURE = (
|
||||
if DEBUG
|
||||
else (
|
||||
SESSION_COOKIE_SAMESITE == 'None'
|
||||
or get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', True)
|
||||
or get_boolean_setting(
|
||||
'INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-SECURE_PROXY_SSL_HEADER
|
||||
if ssl_header := get_boolean_setting(
|
||||
'INVENTREE_USE_X_FORWARDED_PROTO', 'use_x_forwarded_proto', False
|
||||
):
|
||||
# The default header name is 'HTTP_X_FORWARDED_PROTO', but can be adjusted
|
||||
ssl_header_name = get_setting(
|
||||
'INVENTREE_X_FORWARDED_PROTO_NAME',
|
||||
'x_forwarded_proto_name',
|
||||
'HTTP_X_FORWARDED_PROTO',
|
||||
)
|
||||
SECURE_PROXY_SSL_HEADER = (ssl_header_name, 'https')
|
||||
|
||||
USE_X_FORWARDED_HOST = get_boolean_setting(
|
||||
'INVENTREE_USE_X_FORWARDED_HOST',
|
||||
config_key='use_x_forwarded_host',
|
||||
|
||||
@@ -18,6 +18,7 @@ from rest_framework.response import Response
|
||||
|
||||
import InvenTree.sso
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.forms import registration_enabled
|
||||
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
|
||||
from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer
|
||||
|
||||
@@ -204,7 +205,7 @@ class SocialProviderListView(ListAPI):
|
||||
and get_global_setting('LOGIN_ENFORCE_MFA'),
|
||||
'mfa_enabled': settings.MFA_ENABLED,
|
||||
'providers': provider_list,
|
||||
'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'),
|
||||
'registration_enabled': registration_enabled(),
|
||||
'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'),
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
@@ -463,7 +463,7 @@ def get_user_color_theme(user):
|
||||
user_theme_name = user_theme.name
|
||||
if not user_theme_name or not ColorTheme.is_valid_choice(user_theme):
|
||||
user_theme_name = 'default'
|
||||
except ColorTheme.DoesNotExist:
|
||||
except Exception:
|
||||
user_theme_name = 'default'
|
||||
|
||||
return user_theme_name
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Test the sso module functionality."""
|
||||
"""Test the sso and auth module functionality."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import override_settings
|
||||
from django.test.testcases import TransactionTestCase
|
||||
|
||||
@@ -9,6 +11,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree import sso
|
||||
from InvenTree.forms import RegistratonMixin
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
|
||||
|
||||
class Dummy:
|
||||
@@ -119,3 +122,90 @@ class TestSsoGroupSync(TransactionTestCase):
|
||||
self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0)
|
||||
sso.ensure_sso_groups(None, self.sociallogin)
|
||||
self.assertEqual(Group.objects.filter(name='inventree_group').count(), 1)
|
||||
|
||||
|
||||
class EmailSettingsContext:
|
||||
"""Context manager to enable email settings for tests."""
|
||||
|
||||
def __enter__(self):
|
||||
"""Enable stuff."""
|
||||
InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', True)
|
||||
settings.EMAIL_HOST = 'localhost'
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
"""Exit stuff."""
|
||||
InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', False)
|
||||
settings.EMAIL_HOST = ''
|
||||
|
||||
|
||||
class TestAuth(InvenTreeAPITestCase):
|
||||
"""Test authentication functionality."""
|
||||
|
||||
def email_args(self, user=None, email=None):
|
||||
"""Generate registration arguments."""
|
||||
return {
|
||||
'username': user or 'user1',
|
||||
'email': email or 'test@example.com',
|
||||
'password1': '#asdf1234',
|
||||
'password2': '#asdf1234',
|
||||
}
|
||||
|
||||
def test_registration(self):
|
||||
"""Test the registration process."""
|
||||
self.logout()
|
||||
|
||||
# Duplicate username
|
||||
resp = self.post(
|
||||
'/api/auth/registration/',
|
||||
self.email_args(user='testuser'),
|
||||
expected_code=400,
|
||||
)
|
||||
self.assertIn(
|
||||
'A user with that username already exists.', resp.data['username']
|
||||
)
|
||||
|
||||
# Registration is disabled
|
||||
resp = self.post(
|
||||
'/api/auth/registration/', self.email_args(), expected_code=400
|
||||
)
|
||||
self.assertIn('Registration is disabled.', resp.data['non_field_errors'])
|
||||
|
||||
# Enable registration - now it should work
|
||||
with EmailSettingsContext():
|
||||
resp = self.post(
|
||||
'/api/auth/registration/', self.email_args(), expected_code=201
|
||||
)
|
||||
self.assertIn('key', resp.data)
|
||||
|
||||
def test_registration_email(self):
|
||||
"""Test that LOGIN_SIGNUP_MAIL_RESTRICTION works."""
|
||||
self.logout()
|
||||
|
||||
# Check the setting validation is working
|
||||
with self.assertRaises(ValidationError):
|
||||
InvenTreeSetting.set_setting(
|
||||
'LOGIN_SIGNUP_MAIL_RESTRICTION', 'example.com,inventree.org'
|
||||
)
|
||||
|
||||
# Setting setting correctly
|
||||
correct_setting = '@example.com,@inventree.org'
|
||||
InvenTreeSetting.set_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', correct_setting)
|
||||
self.assertEqual(
|
||||
InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION'),
|
||||
correct_setting,
|
||||
)
|
||||
|
||||
# Wrong email format
|
||||
resp = self.post(
|
||||
'/api/auth/registration/',
|
||||
self.email_args(email='admin@invenhost.com'),
|
||||
expected_code=400,
|
||||
)
|
||||
self.assertIn('The provided email domain is not approved.', resp.data['email'])
|
||||
|
||||
# Right format should work
|
||||
with EmailSettingsContext():
|
||||
resp = self.post(
|
||||
'/api/auth/registration/', self.email_args(), expected_code=201
|
||||
)
|
||||
self.assertIn('key', resp.data)
|
||||
@@ -32,6 +32,7 @@ import users.api
|
||||
from build.urls import build_urls
|
||||
from common.urls import common_urls
|
||||
from company.urls import company_urls, manufacturer_part_urls, supplier_part_urls
|
||||
from InvenTree.auth_override_views import CustomRegisterView
|
||||
from order.urls import order_urls
|
||||
from part.urls import part_urls
|
||||
from plugin.urls import get_plugin_urls
|
||||
@@ -202,6 +203,7 @@ apipatterns = [
|
||||
ConfirmEmailView.as_view(),
|
||||
name='account_confirm_email',
|
||||
),
|
||||
path('registration/', CustomRegisterView.as_view(), name='rest_register'),
|
||||
path('registration/', include('dj_rest_auth.registration.urls')),
|
||||
path(
|
||||
'providers/', SocialProviderListView.as_view(), name='social_providers'
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.conf import settings
|
||||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = '0.17.0 dev'
|
||||
INVENTREE_SW_VERSION = '0.17.2'
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@@ -25,6 +25,9 @@ database:
|
||||
# HOST: Database host address (if required)
|
||||
# PORT: Database host port (if required)
|
||||
|
||||
# Base URL for the InvenTree server (or use the environment variable INVENTREE_SITE_URL)
|
||||
# site_url: 'http://localhost:8000'
|
||||
|
||||
# Set debug to False to run in production mode, or use the environment variable INVENTREE_DEBUG
|
||||
debug: True
|
||||
|
||||
@@ -45,8 +48,10 @@ log_level: WARNING
|
||||
# Configure if logs should be output in JSON format
|
||||
# Use environment variable INVENTREE_JSON_LOG
|
||||
json_log: False
|
||||
|
||||
# Enable database-level logging, or use the environment variable INVENTREE_DB_LOGGING
|
||||
db_logging: False
|
||||
|
||||
# Enable writing a log file, or use the environment variable INVENTREE_WRITE_LOG
|
||||
write_log: False
|
||||
|
||||
@@ -56,8 +61,6 @@ language: en-us
|
||||
# System time-zone (default is UTC). Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
timezone: UTC
|
||||
|
||||
# Base URL for the InvenTree server (or use the environment variable INVENTREE_SITE_URL)
|
||||
site_url: 'http://localhost:8000'
|
||||
|
||||
# Add new user on first startup by either adding values here or from a file
|
||||
#admin_user: admin
|
||||
@@ -114,19 +117,16 @@ allowed_hosts:
|
||||
# - 'http://localhost'
|
||||
# - 'http://*.localhost'
|
||||
|
||||
# Proxy forwarding settings
|
||||
# If InvenTree is running behind a proxy, you may need to configure these settings
|
||||
|
||||
# Override with the environment variable INVENTREE_USE_X_FORWARDED_HOST
|
||||
use_x_forwarded_host: false
|
||||
|
||||
# Override with the environment variable INVENTREE_USE_X_FORWARDED_PORT
|
||||
use_x_forwarded_port: false
|
||||
# Enable Proxy header passthrough
|
||||
# Override with the environment variable INVENTREE_USE_X_FORWARDED_<HEADER>
|
||||
# use_x_forwarded_host: true
|
||||
# use_x_forwarded_port: true
|
||||
# use_x_forwarded_proto: true
|
||||
|
||||
# Cookie settings (nominally the default settings should be fine)
|
||||
#cookie:
|
||||
# secure: false
|
||||
# samesite: false
|
||||
cookie:
|
||||
secure: false
|
||||
samesite: false
|
||||
|
||||
# Cross Origin Resource Sharing (CORS) settings (see https://github.com/adamchainz/django-cors-headers)
|
||||
cors:
|
||||
@@ -160,7 +160,6 @@ cache:
|
||||
host: 'inventree-cache'
|
||||
port: 6379
|
||||
|
||||
|
||||
# Login configuration
|
||||
login_confirm_days: 3
|
||||
login_attempts: 5
|
||||
|
||||
@@ -75,6 +75,24 @@ class GeneralExtraLineList(DataExportViewMixin):
|
||||
filterset_fields = ['order']
|
||||
|
||||
|
||||
class OrderCreateMixin:
|
||||
"""Mixin class which handles order creation via API."""
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Save user information on order creation."""
|
||||
serializer = self.get_serializer(data=self.clean_data(request.data))
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
item = serializer.save()
|
||||
item.created_by = request.user
|
||||
item.save()
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
|
||||
class OrderFilter(rest_filters.FilterSet):
|
||||
"""Base class for custom API filters for the OrderList endpoint."""
|
||||
|
||||
@@ -260,7 +278,9 @@ class PurchaseOrderMixin:
|
||||
return queryset
|
||||
|
||||
|
||||
class PurchaseOrderList(PurchaseOrderMixin, DataExportViewMixin, ListCreateAPI):
|
||||
class PurchaseOrderList(
|
||||
PurchaseOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
|
||||
):
|
||||
"""API endpoint for accessing a list of PurchaseOrder objects.
|
||||
|
||||
- GET: Return list of PurchaseOrder objects (with filters)
|
||||
@@ -722,7 +742,9 @@ class SalesOrderMixin:
|
||||
return queryset
|
||||
|
||||
|
||||
class SalesOrderList(SalesOrderMixin, DataExportViewMixin, ListCreateAPI):
|
||||
class SalesOrderList(
|
||||
SalesOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
|
||||
):
|
||||
"""API endpoint for accessing a list of SalesOrder objects.
|
||||
|
||||
- GET: Return list of SalesOrder objects (with filters)
|
||||
@@ -731,20 +753,6 @@ class SalesOrderList(SalesOrderMixin, DataExportViewMixin, ListCreateAPI):
|
||||
|
||||
filterset_class = SalesOrderFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Save user information on create."""
|
||||
serializer = self.get_serializer(data=self.clean_data(request.data))
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
item = serializer.save()
|
||||
item.created_by = request.user
|
||||
item.save()
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Perform custom filtering operations on the SalesOrder queryset."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
@@ -1339,25 +1347,13 @@ class ReturnOrderMixin:
|
||||
return queryset
|
||||
|
||||
|
||||
class ReturnOrderList(ReturnOrderMixin, DataExportViewMixin, ListCreateAPI):
|
||||
class ReturnOrderList(
|
||||
ReturnOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
|
||||
):
|
||||
"""API endpoint for accessing a list of ReturnOrder objects."""
|
||||
|
||||
filterset_class = ReturnOrderFilter
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Save user information on create."""
|
||||
serializer = self.get_serializer(data=self.clean_data(request.data))
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
item = serializer.save()
|
||||
item.created_by = request.user
|
||||
item.save()
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
||||
ordering_field_aliases = {
|
||||
|
||||
@@ -255,7 +255,10 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
order = models.PurchaseOrder.objects.get(pk=response.data['pk'])
|
||||
|
||||
self.assertEqual(order.reference, 'PO-92233720368547758089999999999999999')
|
||||
# Check that the created_by field is set correctly
|
||||
self.assertEqual(order.created_by.username, 'testuser')
|
||||
|
||||
self.assertEqual(order.reference, huge_number)
|
||||
self.assertEqual(order.reference_int, 0x7FFFFFFF)
|
||||
|
||||
def test_po_reference_wildcard_default(self):
|
||||
@@ -1407,6 +1410,11 @@ class SalesOrderTest(OrderTest):
|
||||
# Grab the PK for the newly created SalesOrder
|
||||
pk = response.data['pk']
|
||||
|
||||
# Basic checks against the newly created SalesOrder
|
||||
so = models.SalesOrder.objects.get(pk=pk)
|
||||
self.assertEqual(so.reference, 'SO-12345')
|
||||
self.assertEqual(so.created_by.username, 'testuser')
|
||||
|
||||
# Try to create a SO with identical reference (should fail)
|
||||
response = self.post(
|
||||
url,
|
||||
|
||||
@@ -92,7 +92,7 @@ class BarcodeView(CreateAPIView):
|
||||
|
||||
if num_scans > max_scans:
|
||||
n = num_scans - max_scans
|
||||
old_scan_ids = (
|
||||
old_scan_ids = list(
|
||||
BarcodeScanResult.objects.all()
|
||||
.order_by('timestamp')
|
||||
.values_list('pk', flat=True)[:n]
|
||||
|
||||
@@ -579,9 +579,7 @@ class PluginsRegistry:
|
||||
except Exception as error:
|
||||
# Handle the error, log it and try again
|
||||
if attempts == 0:
|
||||
handle_error(
|
||||
error, log_name='init', do_raise=settings.PLUGIN_TESTING
|
||||
)
|
||||
handle_error(error, log_name='init', do_raise=True)
|
||||
|
||||
logger.exception(
|
||||
'[PLUGIN] Encountered an error with %s:\n%s',
|
||||
|
||||
@@ -429,7 +429,7 @@ class StockItem(
|
||||
'parameters': self.part.parameters_map(),
|
||||
'quantity': InvenTree.helpers.normalize(self.quantity),
|
||||
'result_list': self.testResultList(include_installed=True),
|
||||
'results': self.testResultMap(include_installed=True),
|
||||
'results': self.testResultMap(include_installed=True, cascade=True),
|
||||
'serial': self.serial,
|
||||
'stock_item': self,
|
||||
'tests': self.testResultMap(),
|
||||
@@ -1508,17 +1508,22 @@ class StockItem(
|
||||
"""
|
||||
return self.children.count()
|
||||
|
||||
def is_in_stock(self, check_status: bool = True):
|
||||
def is_in_stock(
|
||||
self, check_status: bool = True, check_quantity: bool = True
|
||||
) -> bool:
|
||||
"""Return True if this StockItem is "in stock".
|
||||
|
||||
Args:
|
||||
check_status: If True, check the status of the StockItem. Defaults to True.
|
||||
check_quantity: If True, check the quantity of the StockItem. Defaults to True.
|
||||
"""
|
||||
if check_status and self.status not in StockStatusGroups.AVAILABLE_CODES:
|
||||
return False
|
||||
|
||||
if check_quantity and self.quantity <= 0:
|
||||
return False
|
||||
|
||||
return all([
|
||||
self.quantity > 0, # Quantity must be greater than zero
|
||||
self.sales_order is None, # Not assigned to a SalesOrder
|
||||
self.belongs_to is None, # Not installed inside another StockItem
|
||||
self.customer is None, # Not assigned to a customer
|
||||
@@ -2401,6 +2406,7 @@ class StockItem(
|
||||
"""
|
||||
# Do we wish to include test results from installed items?
|
||||
include_installed = kwargs.pop('include_installed', False)
|
||||
cascade = kwargs.pop('cascade', False)
|
||||
|
||||
# Filter results by "date", so that newer results
|
||||
# will override older ones.
|
||||
@@ -2411,9 +2417,6 @@ class StockItem(
|
||||
for result in results:
|
||||
result_map[result.key] = result
|
||||
|
||||
# Do we wish to "cascade" and include test results from installed stock items?
|
||||
cascade = kwargs.get('cascade', False)
|
||||
|
||||
if include_installed:
|
||||
installed_items = self.get_installed_items(cascade=cascade)
|
||||
|
||||
|
||||
@@ -1571,7 +1571,9 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
|
||||
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
|
||||
)
|
||||
|
||||
if not allow_out_of_stock_transfer and not pk.is_in_stock(check_status=False):
|
||||
if not allow_out_of_stock_transfer and not pk.is_in_stock(
|
||||
check_status=False, check_quantity=False
|
||||
):
|
||||
raise ValidationError(_('Stock item is not in stock'))
|
||||
|
||||
return pk
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "account/base.html" %}
|
||||
{% extends "skeleton.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{% static 'img/favicon/apple-icon-57x57.png' %}">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{% static 'img/favicon/apple-icon-60x60.png' %}">
|
||||
@@ -28,8 +26,6 @@
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="{% static 'img/favicon/ms-icon-144x144.png' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
@@ -37,13 +33,10 @@
|
||||
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
|
||||
<link rel="stylesheet" href="{% get_color_theme_css request.user %}">
|
||||
|
||||
<title>
|
||||
{% inventree_title %} | {% block head_title %}{% endblock head_title %}
|
||||
</title>
|
||||
|
||||
{% block extra_head %}
|
||||
{% endblock extra_head %}
|
||||
</head>
|
||||
@@ -85,37 +78,26 @@
|
||||
|
||||
<!-- general JS -->
|
||||
{% include "third_party_js.html" %}
|
||||
|
||||
<script type='text/javascript' src='{% static "script/inventree/inventree.js" %}'></script>
|
||||
<script type='text/javascript' src='{% static "script/inventree/message.js" %}'></script>
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
showMessage("{{ message }}");
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
showCachedAlerts();
|
||||
|
||||
// Add brand icons for SSO providers, if available
|
||||
$('.socialaccount_provider').each(function(i, obj) {
|
||||
var el = $(this);
|
||||
var tag = el.attr('brand_name');
|
||||
|
||||
var icon = window.FontAwesome.icon({prefix: 'fab', iconName: tag});
|
||||
|
||||
if (icon) {
|
||||
el.prepend(`<span class='fab fa-${tag}'></span> `);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -34,7 +34,7 @@ django-weasyprint # django weasyprint integration
|
||||
djangorestframework<3.15 # DRF framework # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521
|
||||
djangorestframework-simplejwt[crypto] # JWT authentication
|
||||
django-xforwardedfor-middleware # IP forwarding metadata
|
||||
dj-rest-auth # Authentication API endpoints
|
||||
dj-rest-auth==7.0.0 # Authentication API endpoints # FIXED 2024-12-22 due to https://github.com/inventree/InvenTree/issues/8707
|
||||
dulwich # pure Python git integration
|
||||
drf-spectacular # DRF API documentation
|
||||
feedparser # RSS newsfeed parser
|
||||
|
||||
@@ -19,10 +19,12 @@ import { useHover } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { api } from '../../App';
|
||||
import type { UserRoles } from '../../enums/Roles';
|
||||
import { cancelEvent } from '../../functions/events';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { showApiErrorMessage } from '../../functions/notifications';
|
||||
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@@ -159,12 +161,24 @@ function UploadModal({
|
||||
const formData = new FormData();
|
||||
formData.append('image', file, file.name);
|
||||
|
||||
const response = await api.patch(apiPath, formData);
|
||||
|
||||
if (response.data.image.includes(file.name)) {
|
||||
setImage(response.data.image);
|
||||
modals.closeAll();
|
||||
}
|
||||
api
|
||||
.patch(apiPath, formData)
|
||||
.then((response) => {
|
||||
setImage(response.data.image);
|
||||
modals.closeAll();
|
||||
showNotification({
|
||||
title: t`Image uploaded`,
|
||||
message: t`Image has been uploaded successfully`,
|
||||
color: 'green'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showApiErrorMessage({
|
||||
error: error,
|
||||
title: t`Upload Error`,
|
||||
field: 'image'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface ApiFormProps {
|
||||
postFormContent?: JSX.Element;
|
||||
successMessage?: string;
|
||||
onFormSuccess?: (data: any) => void;
|
||||
onFormError?: () => void;
|
||||
onFormError?: (response: any) => void;
|
||||
processFormData?: (data: any) => any;
|
||||
table?: TableState;
|
||||
modelType?: ModelType;
|
||||
@@ -482,7 +482,7 @@ export function ApiForm({
|
||||
default:
|
||||
// Unexpected state on form success
|
||||
invalidResponse(response.status);
|
||||
props.onFormError?.();
|
||||
props.onFormError?.(response);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -534,26 +534,30 @@ export function ApiForm({
|
||||
|
||||
processErrors(error.response.data);
|
||||
setNonFieldErrors(_nonFieldErrors);
|
||||
props.onFormError?.(error);
|
||||
|
||||
break;
|
||||
default:
|
||||
// Unexpected state on form error
|
||||
invalidResponse(error.response.status);
|
||||
props.onFormError?.();
|
||||
props.onFormError?.(error);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
showTimeoutNotification();
|
||||
props.onFormError?.();
|
||||
props.onFormError?.(error);
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
};
|
||||
|
||||
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
|
||||
props.onFormError?.();
|
||||
}, [props.onFormError]);
|
||||
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(
|
||||
(error: any) => {
|
||||
props.onFormError?.(error);
|
||||
},
|
||||
[props.onFormError]
|
||||
);
|
||||
|
||||
if (optionsLoading || initialDataQuery.isFetching) {
|
||||
return (
|
||||
|
||||
@@ -192,7 +192,7 @@ export function RegistrationForm() {
|
||||
headers: { Authorization: '' }
|
||||
})
|
||||
.then((ret) => {
|
||||
if (ret?.status === 204) {
|
||||
if (ret?.status === 204 || ret?.status === 201) {
|
||||
setIsRegistering(false);
|
||||
showLoginNotification({
|
||||
title: t`Registration successful`,
|
||||
@@ -202,7 +202,7 @@ export function RegistrationForm() {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status === 400) {
|
||||
if (err.response?.status === 400) {
|
||||
setIsRegistering(false);
|
||||
for (const [key, value] of Object.entries(err.response.data)) {
|
||||
registrationForm.setFieldError(key, value as string);
|
||||
|
||||
@@ -46,6 +46,7 @@ export type ApiFormFieldChoice = {
|
||||
* @param required : Whether the field is required
|
||||
* @param hidden : Whether the field is hidden
|
||||
* @param disabled : Whether the field is disabled
|
||||
* @param error : Optional error message to display
|
||||
* @param exclude : Whether to exclude the field from the submitted data
|
||||
* @param placeholder : The placeholder text to display
|
||||
* @param description : The description to display for the field
|
||||
@@ -88,6 +89,7 @@ export type ApiFormFieldType = {
|
||||
child?: ApiFormFieldType;
|
||||
children?: { [key: string]: ApiFormFieldType };
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
choices?: ApiFormFieldChoice[];
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -256,7 +258,7 @@ export function ApiFormField({
|
||||
aria-label={`boolean-field-${fieldName}`}
|
||||
radius='lg'
|
||||
size='sm'
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
onChange={(event) => onChange(event.currentTarget.checked)}
|
||||
/>
|
||||
);
|
||||
@@ -277,7 +279,7 @@ export function ApiFormField({
|
||||
id={fieldId}
|
||||
aria-label={`number-field-${field.name}`}
|
||||
value={numericalValue}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
decimalScale={definition.field_type == 'integer' ? 0 : 10}
|
||||
onChange={(value: number | string | null) => onChange(value)}
|
||||
step={1}
|
||||
@@ -299,7 +301,7 @@ export function ApiFormField({
|
||||
ref={field.ref}
|
||||
radius='sm'
|
||||
value={value}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
onChange={(payload: File | null) => onChange(payload)}
|
||||
/>
|
||||
);
|
||||
@@ -343,6 +345,7 @@ export function ApiFormField({
|
||||
booleanValue,
|
||||
control,
|
||||
controller,
|
||||
definition,
|
||||
field,
|
||||
fieldId,
|
||||
fieldName,
|
||||
|
||||
@@ -63,7 +63,7 @@ export function ChoiceField({
|
||||
<Select
|
||||
id={fieldId}
|
||||
aria-label={`choice-field-${field.name}`}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
radius='sm'
|
||||
{...field}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function DateField({
|
||||
radius='sm'
|
||||
ref={field.ref}
|
||||
type={undefined}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
value={dateValue ?? null}
|
||||
clearable={!definition.required}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function IconField({
|
||||
label={definition.label}
|
||||
description={definition.description}
|
||||
required={definition.required}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
ref={field.ref}
|
||||
component='button'
|
||||
type='button'
|
||||
|
||||
@@ -284,7 +284,7 @@ export function RelatedModelField({
|
||||
return (
|
||||
<Input.Wrapper
|
||||
{...fieldDefinition}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
styles={{ description: { paddingBottom: '5px' } }}
|
||||
>
|
||||
<Select
|
||||
|
||||
@@ -258,7 +258,7 @@ export function TableFieldExtraRow({
|
||||
fieldName={fieldName ?? 'field'}
|
||||
fieldDefinition={field}
|
||||
defaultValue={defaultValue}
|
||||
error={error}
|
||||
error={fieldDefinition.error ?? error}
|
||||
/>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function TextField({
|
||||
aria-label={`text-field-${field.name}`}
|
||||
type={definition.field_type}
|
||||
value={rawText || ''}
|
||||
error={error?.message}
|
||||
error={definition.error ?? error?.message}
|
||||
radius='sm'
|
||||
onChange={(event) => onTextChange(event.currentTarget.value)}
|
||||
onBlur={(event) => {
|
||||
@@ -64,7 +64,13 @@ export default function TextField({
|
||||
onChange(event.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => onKeyDown(event.code)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.code === 'Enter') {
|
||||
// Bypass debounce on enter key
|
||||
onChange(event.currentTarget.value);
|
||||
}
|
||||
onKeyDown(event.code);
|
||||
}}
|
||||
rightSection={
|
||||
value && !definition.required ? (
|
||||
<IconX size='1rem' color='red' onClick={() => onTextChange('')} />
|
||||
|
||||
@@ -199,7 +199,7 @@ export function RenderInlineModel({
|
||||
{prefix}
|
||||
{image && <Thumbnail src={image} size={18} />}
|
||||
{url ? (
|
||||
<Anchor href={url} onClick={(event: any) => onClick(event)}>
|
||||
<Anchor href='' onClick={(event: any) => onClick(event)}>
|
||||
<Text size='sm'>{primary}</Text>
|
||||
</Anchor>
|
||||
) : (
|
||||
|
||||
@@ -158,7 +158,7 @@ export function SettingList({
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{(keys || allKeys).length === 0 && (
|
||||
{(keys || allKeys)?.length === 0 && (
|
||||
<Text style={{ fontStyle: 'italic' }}>
|
||||
<Trans>No settings specified</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -74,3 +74,31 @@ export function showLoginNotification({
|
||||
autoClose: 2500
|
||||
});
|
||||
}
|
||||
|
||||
export function showApiErrorMessage({
|
||||
error,
|
||||
title,
|
||||
message,
|
||||
field
|
||||
}: {
|
||||
error: any;
|
||||
title: string;
|
||||
message?: string;
|
||||
field?: string;
|
||||
}) {
|
||||
// Extract error description from response
|
||||
const error_data: any = error.response?.data ?? {};
|
||||
|
||||
let error_msg: any =
|
||||
message ?? error_data[field ?? 'error'] ?? error_data['non_field_errors'];
|
||||
|
||||
if (!error_msg) {
|
||||
error_msg = t`An error occurred`;
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
title: title,
|
||||
message: error_msg,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,9 +48,8 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
||||
modalClose.current();
|
||||
props.onFormSuccess?.(data);
|
||||
},
|
||||
onFormError: () => {
|
||||
modalClose.current();
|
||||
props.onFormError?.();
|
||||
onFormError: (error: any) => {
|
||||
props.onFormError?.(error);
|
||||
}
|
||||
}),
|
||||
[props]
|
||||
|
||||
@@ -167,7 +167,10 @@ export function useTable(tableName: string): TableState {
|
||||
const index = _records.findIndex((r) => r.pk === record.pk);
|
||||
|
||||
if (index >= 0) {
|
||||
_records[index] = record;
|
||||
_records[index] = {
|
||||
..._records[index],
|
||||
...record
|
||||
};
|
||||
} else {
|
||||
_records.push(record);
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ export default function Set_Password() {
|
||||
})
|
||||
.catch((err) => {
|
||||
if (
|
||||
err.response.status === 400 &&
|
||||
err.response.data?.token == 'Invalid value'
|
||||
err.response?.status === 400 &&
|
||||
err.response?.data?.token == 'Invalid value'
|
||||
) {
|
||||
invalidToken();
|
||||
} else {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ActionButton } from '../../../../components/buttons/ActionButton';
|
||||
import { FactCollection } from '../../../../components/settings/FactCollection';
|
||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||
import { showApiErrorMessage } from '../../../../functions/notifications';
|
||||
import { useTable } from '../../../../hooks/UseTable';
|
||||
import { apiUrl } from '../../../../states/ApiState';
|
||||
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
|
||||
@@ -46,10 +47,9 @@ export function CurrencyTable({
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification({
|
||||
title: t`Exchange rate update error`,
|
||||
message: error,
|
||||
color: 'red'
|
||||
showApiErrorMessage({
|
||||
error: error,
|
||||
title: t`Exchange rate update error`
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
IconUsersGroup
|
||||
} from '@tabler/icons-react';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import {
|
||||
@@ -66,6 +66,7 @@ export type CompanyDetailProps = {
|
||||
export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
const { id } = useParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
|
||||
const {
|
||||
@@ -283,7 +284,9 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
url: ApiEndpoints.company_list,
|
||||
pk: company?.pk,
|
||||
title: t`Delete Company`,
|
||||
onFormSuccess: refreshInstance
|
||||
onFormSuccess: () => {
|
||||
navigate('/');
|
||||
}
|
||||
});
|
||||
|
||||
const companyActions = useMemo(() => {
|
||||
|
||||
@@ -399,6 +399,17 @@ export default function SalesOrderDetail() {
|
||||
successMessage: t`Order placed on hold`
|
||||
});
|
||||
|
||||
const shipOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_complete, order.pk),
|
||||
title: t`Ship Sales Order`,
|
||||
onFormSuccess: refreshInstance,
|
||||
preFormWarning: t`Ship this order?`,
|
||||
successMessage: t`Order shipped`,
|
||||
fields: {
|
||||
accept_incomplete: {}
|
||||
}
|
||||
});
|
||||
|
||||
const completeOrder = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.sales_order_complete, order.pk),
|
||||
title: t`Complete Sales Order`,
|
||||
@@ -444,7 +455,7 @@ export default function SalesOrderDetail() {
|
||||
icon='deliver'
|
||||
hidden={!canShip}
|
||||
color='blue'
|
||||
onClick={completeOrder.open}
|
||||
onClick={shipOrder.open}
|
||||
/>,
|
||||
<PrimaryActionButton
|
||||
title={t`Complete Order`}
|
||||
@@ -510,6 +521,7 @@ export default function SalesOrderDetail() {
|
||||
{issueOrder.modal}
|
||||
{cancelOrder.modal}
|
||||
{holdOrder.modal}
|
||||
{shipOrder.modal}
|
||||
{completeOrder.modal}
|
||||
{editSalesOrder.modal}
|
||||
{duplicateSalesOrder.modal}
|
||||
|
||||
@@ -295,7 +295,7 @@ export default function SalesOrderShipmentDetail() {
|
||||
visible={!!shipment.delivery_date}
|
||||
/>
|
||||
];
|
||||
}, [shipment, shipmentQuery]);
|
||||
}, [isPending, shipment.deliveryDate, shipmentQuery.isFetching]);
|
||||
|
||||
const shipmentActions = useMemo(() => {
|
||||
const canEdit: boolean = user.hasChangePermission(
|
||||
|
||||
@@ -280,6 +280,8 @@ export default function Stock() {
|
||||
<BarcodeActionDropdown
|
||||
model={ModelType.stocklocation}
|
||||
pk={location.pk}
|
||||
hash={location?.barcode_hash}
|
||||
perm={user.hasChangeRole(UserRoles.stock_location)}
|
||||
actions={[
|
||||
{
|
||||
name: 'Scan in stock items',
|
||||
|
||||
@@ -661,7 +661,6 @@ export default function StockDetail() {
|
||||
const stockActions = useMemo(() => {
|
||||
const inStock =
|
||||
user.hasChangeRole(UserRoles.stock) &&
|
||||
stockitem.quantity > 0 &&
|
||||
!stockitem.sales_order &&
|
||||
!stockitem.belongs_to &&
|
||||
!stockitem.customer &&
|
||||
@@ -717,7 +716,7 @@ export default function StockDetail() {
|
||||
{
|
||||
name: t`Remove`,
|
||||
tooltip: t`Remove Stock`,
|
||||
hidden: serialized || !inStock,
|
||||
hidden: serialized || !inStock || stockitem.quantity <= 0,
|
||||
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
||||
onClick: () => {
|
||||
stockitem.pk && removeStockItem.open();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { api } from '../App';
|
||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { generateUrl } from '../functions/urls';
|
||||
@@ -38,17 +40,29 @@ export const useIconState = create<IconState>()((set, get) => ({
|
||||
|
||||
await Promise.all(
|
||||
packs.data.map(async (pack: any) => {
|
||||
const fontName = `inventree-icon-font-${pack.prefix}`;
|
||||
const src = Object.entries(pack.fonts as Record<string, string>)
|
||||
.map(
|
||||
([format, url]) => `url(${generateUrl(url)}) format("${format}")`
|
||||
)
|
||||
.join(',\n');
|
||||
const font = new FontFace(fontName, `${src};`);
|
||||
await font.load();
|
||||
document.fonts.add(font);
|
||||
if (pack.prefix && pack.fonts) {
|
||||
const fontName = `inventree-icon-font-${pack.prefix}`;
|
||||
const src = Object.entries(pack.fonts as Record<string, string>)
|
||||
.map(
|
||||
([format, url]) => `url(${generateUrl(url)}) format("${format}")`
|
||||
)
|
||||
.join(',\n');
|
||||
const font = new FontFace(fontName, `${src};`);
|
||||
await font.load();
|
||||
document.fonts.add(font);
|
||||
return font;
|
||||
} else {
|
||||
console.error(
|
||||
"ERR: Icon package is missing 'prefix' or 'fonts' field"
|
||||
);
|
||||
showNotification({
|
||||
title: t`Error`,
|
||||
message: t`Error loading icon package from server`,
|
||||
color: 'red'
|
||||
});
|
||||
|
||||
return font;
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -56,7 +70,7 @@ export const useIconState = create<IconState>()((set, get) => ({
|
||||
hasLoaded: true,
|
||||
packages: packs.data,
|
||||
packagesMap: Object.fromEntries(
|
||||
packs.data.map((pack: any) => [pack.prefix, pack])
|
||||
packs.data?.map((pack: any) => [pack.prefix, pack])
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,10 +90,8 @@ test('Build Order - Basic Tests', async ({ page }) => {
|
||||
test('Build Order - Build Outputs', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/part/`);
|
||||
|
||||
// Navigate to the correct build order
|
||||
await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
|
||||
await page.goto(`${baseUrl}/manufacturing/index/`);
|
||||
await page.getByRole('tab', { name: 'Build Orders', exact: true }).click();
|
||||
|
||||
// We have now loaded the "Build Order" table. Check for some expected texts
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
|
||||
Reference in New Issue
Block a user