Compare commits

...

32 Commits

Author SHA1 Message Date
Oliver
4b564929d2 Extended error handling for get_user_color_theme (#8854) 2025-01-07 23:53:10 +11:00
github-actions[bot]
8576fbbade Ensure error is raised (#8849) (#8850)
(cherry picked from commit 3a62bdd276)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-01-07 17:31:59 +11:00
github-actions[bot]
6045925ebe Order creation fix (#8846) (#8847)
* Bug fix for PurchaseOrder

- Correctly record the user who created a PO
- Code refactoring

* Updated unit tests

(cherry picked from commit dcf0bb103e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-01-07 15:03:18 +11:00
github-actions[bot]
3715c42fed Fix for test results in stock item report context (#8843) (#8844)
- Allow for "cascade" installed items
- i.e. support multi-level installed items

(cherry picked from commit 94f7890a41)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-01-07 10:40:08 +11:00
github-actions[bot]
4c6e3490c0 Navigate to index when deleting a company (#8831) (#8833)
(cherry picked from commit ae1f9bf274)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-01-05 22:51:55 +11:00
github-actions[bot]
91c095a011 Fix for buggy Caddyfile (#8830) (#8832)
(cherry picked from commit decccf8163)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-01-05 22:33:22 +11:00
github-actions[bot]
d42e3087a8 Fix package install in envs using python lower than 3.12 on debian like OSs (#8793) (#8813)
* do not install with uv for now - fixes #8789
#8742 #8495 #8494

* Ensure errors are raised if install / update fails

(cherry picked from commit d7939efaa9)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-01-01 10:32:16 +11:00
github-actions[bot]
4f7a12bd70 Allow barcode linking for stock location (#8802) (#8810)
(cherry picked from commit 16e9729308)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 16:01:15 +11:00
github-actions[bot]
5048c1d667 Update .env file (#8799) (#8808)
* Update .env file

- No functional changes
- Improved file comments

* Update .env

Improved comment

(cherry picked from commit 74cd0b9aed)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 14:50:33 +11:00
github-actions[bot]
0a522709b1 Add FAQ on cookie setting change (#8805) (#8807)
(cherry picked from commit 7419944301)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 14:44:10 +11:00
github-actions[bot]
c32362456b Add documentation on transferring media files (#8803) (#8806)
(cherry picked from commit c917c64aa1)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 14:43:54 +11:00
github-actions[bot]
4d07a49dfd Caddyfile documentation (#8798) (#8800)
* basic mixin file

* Add basic check for model type support

* Enhanced documentation for Caddyfile

* Additional documentation around proxy server

* Remove code from other PR

(cherry picked from commit ecc1c937ed)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 13:47:07 +11:00
github-actions[bot]
ef5fd93207 [Setup] Support X-Forwarded-Proto header (#8790) (#8797)
* Remove use_x_forwarded_port setting

- As per the docs, this is ignored in favour of use_x_forwarded_host
- So, is not being used anyway

* Add note on x_forwarded_host option

* Add warning message if SITE_URL not provided

* Add support for SECURE_PROXY_SSL_HEADER

* Update configuration template file

* Update SITE_URL docs

* Remove line

* Re-add use_x_forwarded_port

* Docs tweak

* Improve wording

* Fix broken link

(cherry picked from commit 23e4f2f2a2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-31 12:50:51 +11:00
github-actions[bot]
df7204a334 [Docs] Update FAQ (#8777) (#8778)
* Update FAQ

* Add links to FAQ

* Extra info

* Extend documentation for debug options

(cherry picked from commit b0ce67fcd7)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-27 19:13:56 +11:00
Oliver
a70382ac7a Update version.py (#8776)
Bump version number to 0.17.2
2024-12-27 14:32:26 +11:00
github-actions[bot]
eed6223187 Fix default value for SESSION_COOKIE_SECURE (#8767) (#8769)
- Default value was previously 'True'
- Documentation indicated that it was 'False'
- Value in config_template.yaml was 'False' (but commented out)

(cherry picked from commit d4ee8c53b2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-26 22:40:26 +11:00
github-actions[bot]
cab7a06146 Zero stock fix (#8766) (#8768)
* Change backend validation

- Allow stock adjustments with zero quantity

* Frontend changes

(cherry picked from commit ae7f4e33d5)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-26 11:26:04 +11:00
Matthias Mair
40245a6c4a [0.17.x] Fix REST registration endpoint (#8738) (#8763)
* Fix REST registration endpoint (#8738)

* Re-add html account base
Fixes #8690

* fix base template

* override dj-rest-auth pattern to fix fixed token model reference

* pin req

* fix urls.py

* move definition out to separate file

* fix possible issues where email is not enabled but UI shows that registration is enabled

* fix import order

* fix token recovery

* make sure registration redirects

* fix name change

* fix import name

* adjust description

* cleanup

* bum api version

* add test for registration

* add test for registration requirements

* fix merge issues

* fix merge from https://github.com/inventree/InvenTree/pull/8724
2024-12-25 11:38:02 +11:00
github-actions[bot]
3cb806d20a Handle error when loading icon pack (#8753) (#8755)
* Handle error when loading icon pack

* Update

(cherry picked from commit 8fcebefa0b)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-24 10:29:24 +11:00
github-actions[bot]
8f1bf95463 Add separate dialog for 'Ship Order' button (#8734) (#8735)
(cherry picked from commit 0bcad6b340)

Co-authored-by: Joe Rogers <1337joe@users.noreply.github.com>
2024-12-22 16:12:57 +11:00
github-actions[bot]
4019dc9c9c Forms fixes (#8722) (#8729)
* Refactor form fields

- Allow error message to be passed through via field definition
- Return error information to onFormError

* Fix debounce issue for text fields

* Fix for useForm hook

* Badge fix

- Fix badge rendering for SalesOrderShipment

* Cleanup unit test

(cherry picked from commit aabcf52cd2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 15:52:06 +11:00
github-actions[bot]
70f17997eb [UI] Link fix (#8726) (#8728)
- Fixes anchor issues in stock tracking table

(cherry picked from commit 68ac4118e9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 12:28:49 +11:00
github-actions[bot]
2d773a7b3e Badge fix (#8725) (#8727)
- Fix badge rendering for SalesOrderShipment

(cherry picked from commit 130bc84b44)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-20 12:21:42 +11:00
Oliver
39211ff4b6 Fix MFA auth flow (#8720) (#8724)
* Re-add html account base
Fixes #8690

* fix base template

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-12-20 09:12:14 +11:00
github-actions[bot]
e37ff5c3d5 [UI] Enhanced null checks (#8706) (#8711)
* Extra null check in SettingList.tsx

* Null checks on error responses

(cherry picked from commit 378d69f0b3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-19 01:01:51 +11:00
github-actions[bot]
6bd32c9236 Image upload error (#8700) (#8704)
* Add helper function for displaying API error message

* Provide feedback on image upload

* Update notification

(cherry picked from commit 1eaf3a4594)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-19 00:16:39 +11:00
github-actions[bot]
04aec83e95 Cast barcode scan IDs to list (#8701) (#8702)
- Fixes issues with limitations on old MySQL server

(cherry picked from commit 4569fd273d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-18 17:04:18 +11:00
github-actions[bot]
b57d035f7f Fix for table update (#8698) (#8699)
- Retain data when updating a single record
- Fixes https://github.com/inventree/InvenTree/issues/8693

(cherry picked from commit 1910612725)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-12-18 15:39:37 +11:00
Oliver
3ac49441ca Update version.py (#8689)
Bump version number to 0.17.1
2024-12-17 22:31:20 +11:00
github-actions[bot]
156c3cc9b2 Bump mkdocs-material from 9.5.48 to 9.5.49 in /docs in the dependencies group across 1 directory (#8675) (#8678)
* Bump mkdocs-material

Bumps the dependencies group with 1 update in the /docs directory: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).

Updates `mkdocs-material` from 9.5.48 to 9.5.49
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.48...9.5.49)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

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

* fix req

---------

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 09:16:26 +11:00
github-actions[bot]
52a26c9887 Bump the dependencies group with 5 updates (#8673) (#8679)
Bumps the dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.7.1` | `3.8.0` |
| [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.17.8` | `0.17.9` |
| [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `2.0.1` | `2.1.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.27.6` | `3.27.9` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `2.4.0` | `2.5.0` |

Updates `docker/setup-buildx-action` from 3.7.1 to 3.8.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](c47758b77c...6524bf65af)

Updates `anchore/sbom-action` from 0.17.8 to 0.17.9
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](55dc4ee224...df80a981bc)

Updates `actions/attest-build-provenance` from 2.0.1 to 2.1.0
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](c4fbc64884...7668571508)

Updates `github/codeql-action` from 3.27.6 to 3.27.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](aa57810251...df409f7d92)

Updates `crowdin/github-action` from 2.4.0 to 2.5.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](a9ffb7d5ac...8dfaf9c206)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: crowdin/github-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1e4e3e65cc)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 09:16:23 +11:00
Oliver
667e0a1bee Update version number (#8676) 2024-12-17 09:15:43 +11:00
57 changed files with 580 additions and 213 deletions

View File

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

View File

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

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@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
sarif_file: results.sarif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@@ -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():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{% extends "account/base.html" %}
{% extends "skeleton.html" %}
{% load i18n %}
{% load inventree_extras %}

View File

@@ -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>&nbsp;`);
}
});
});
</script>
</body>
</html>

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -284,7 +284,7 @@ export function RelatedModelField({
return (
<Input.Wrapper
{...fieldDefinition}
error={error?.message}
error={definition.error ?? error?.message}
styles={{ description: { paddingBottom: '5px' } }}
>
<Select

View File

@@ -258,7 +258,7 @@ export function TableFieldExtraRow({
fieldName={fieldName ?? 'field'}
fieldDefinition={field}
defaultValue={defaultValue}
error={error}
error={fieldDefinition.error ?? error}
/>
</Group>
</Table.Td>

View File

@@ -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('')} />

View File

@@ -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>
) : (

View File

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

View File

@@ -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'
});
}

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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`
});
});
}, []);

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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])
)
});
}

View File

@@ -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();