Compare commits

...

30 Commits
1.0.3 ... 1.0.8

Author SHA1 Message Date
Oliver
0dcb706f4c Default stock currency (#10641) (#10644)
* Default stock currency (#10641)

* Fix for useStockFields

- Use default currency

* Ensure default currency is observed

* Specify field default

* Improve import (for ty)

* Update migration files

- Point currency fields to the correct default method

* Unit tests

- Ensure stock item gets correct default currency

* Cleaner generation of default currency value

- Return empty string during migratoins

* Update existing migrations

* Reduce noise

* Ignore "no-matching-overload" rule for ty

* Tweak money_kwargs

* Remove conflicting code

* Fix import

* Tweak currency_code_default
2025-10-21 16:29:24 +11:00
github-actions[bot]
f8bcc3ec17 [UI] Bug fix for build output forms (#10640) (#10642)
(cherry picked from commit 2187a77153)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-21 08:22:50 +11:00
Oliver
bc42450e0a Bump software version to 1.0.8 (#10635) 2025-10-20 16:26:26 +11:00
github-actions[bot]
fa698e7e2b Fixes for SITE_URL validity checks (#10619) (#10634)
* [docker] Allow HTTPS port to be specified for Caddy proxy

* Fix naming collision for INVENTREE_WEB_PORT

* Push InvenTree version first

* Adjust Caddyfile

- Change backup server

* Fix docstring

* Tweak for site URL check:

- Ignore port if SITE_LAX_PROTOCOL_CHECK is set
- Invert logic for readability

* Additional checks for port mismatch

* Adjust middleware checks

- Allow for less strict checking of CSRF_TRUSTED_ORIGINS

* Slight refactor

(cherry picked from commit f9ce9e20b2)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-20 16:19:30 +11:00
github-actions[bot]
c59fd55a00 Setup: allow more python versions (#10615) (#10616)
* extend supported python versions

* bump max python

(cherry picked from commit 874be9920d)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-19 07:09:49 +11:00
github-actions[bot]
486e338b0b BOM updates (#10611) (#10612)
* BOM updates

- Allow variants of substititute parts to be allocated
- Closes https://github.com/inventree/InvenTree/issues/10606

* Check self.allow_variants

* Add comment

(cherry picked from commit a7c4f2adba)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-18 12:06:16 +11:00
Oliver
eb32546824 Bump software version to 1.0.7 (#10591) 2025-10-17 09:18:55 +11:00
github-actions[bot]
cc508a544c Dashboard item fix (#10596) (#10597)
* Fix for "subscribed categories" dashboard item

* Tweak filter display

* Tweak filter for "Subscribed Parts"

(cherry picked from commit 485aa6324c)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-16 23:17:11 +11:00
github-actions[bot]
c471a1cd38 Order labels (#10588) (#10590)
* Add label actions for build orders

* Support other order types

(cherry picked from commit e040d99665)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-16 07:12:27 +11:00
github-actions[bot]
1249ae3a84 [UI] Fix broken dashboard link (#10577) (#10582)
Ref: https://github.com/inventree/inventree/issues/10548
(cherry picked from commit e0559bb2b4)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-14 12:07:24 +11:00
github-actions[bot]
4fe949811d [docs] Troubleshooting Guide (#10574) (#10575)
* [docs] troubleshooting guide

Adds a rough troubleshooting guide

* Add docs for showing docker logs

(cherry picked from commit 71b9373f44)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 19:45:01 +11:00
github-actions[bot]
afdb4090bf Fix missing closing </div> tag (#10572) (#10573)
(cherry picked from commit f9bdad975f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 17:50:13 +11:00
github-actions[bot]
ae2cf931a5 [UI] Remove placeholder text (#10569) (#10570)
- Remove TODO entry
- Placeholder when adding external build order support

(cherry picked from commit a466926aef)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 14:26:22 +11:00
github-actions[bot]
6fa54c0e0e [UI] Fix stock actions (#10566) (#10568)
* Clear selected records when search term changes

* Clear selection after performing stock actions

(cherry picked from commit f22417fd1f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 13:37:49 +11:00
github-actions[bot]
ff79ab87e5 [UI] About InvenTree Tweak (#10565) (#10567)
- Hide version entries without data

(cherry picked from commit ea868b3179)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 13:23:53 +11:00
github-actions[bot]
9fe290b01f [UI] Adjust login error messages (#10556) (#10564)
* Adjust config template

- Don't hard-code cookie mode into template
- Revert to the "default" values (which are the same)

* [ui] better feedback on login error

- Show error code, at least

* Revert removed code

* Adjust playwright tests

(cherry picked from commit 6badc0148f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 12:31:31 +11:00
github-actions[bot]
a63fe64aa2 Installer docs (#10552) (#10563)
* Better formatting

* Tweak setup docs

* Add information on process control and logs

* Fix typo

* Change `cli` to `invoke`

* Update docs/docs/start/installer.md



* Update docs/docs/start/installer.md



* Remove available commands section from installer.md

Removed section on available commands for InvenTree services.

---------


(cherry picked from commit 30e91eb226)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-13 10:01:31 +11:00
github-actions[bot]
b6acbf0a48 fix(installer): make VERSION information accessible in invoke calls (#10558) (#10562)
* implement version loading in more contexts
closes #10554

* factor version file out

* ensure results do not contain new strings

(cherry picked from commit 6327707c0e)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-13 09:13:02 +11:00
github-actions[bot]
4a83b98cd9 Catch error during auto-migrations (#10553) (#10560)
- Prevent process interlock
- Prevent migration check if already running migrations

(cherry picked from commit 067cb1fccb)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-13 09:01:04 +11:00
github-actions[bot]
fe1cc56e94 Fix "override pricing" display (#10545) (#10547)
- Use specified currency values
- Closes #10537

(cherry picked from commit 4edbe9bab1)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-11 12:12:11 +11:00
github-actions[bot]
ef35591110 Update docs for running playwright tests (#10544) (#10546)
(cherry picked from commit 0dbb0306a5)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-11 10:53:44 +11:00
github-actions[bot]
df7817735d [UI] Prevent warning flash (#10540) (#10541)
* [UI] Prevent warning flash

- Display warning element after a delay

* Support no-javascript

(cherry picked from commit 1a171b5705)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-10 12:55:44 +11:00
Oliver
c3ea3a5566 Bump InvenTree software version to 1.0.6 (#10524) 2025-10-07 12:24:16 +11:00
github-actions[bot]
be3e2b4e01 feat(backend): Improve error message on INVE-7 (#10518) (#10523)
* feat(backend): Improve error message on INVE-7

* change wording

* fix test

* another fix

(cherry picked from commit 13845c69da)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-07 11:35:25 +11:00
github-actions[bot]
3fc2db6c3a fix: correct admin credential msg (#10521) (#10522)
* fix installers final message

* adjust text

(cherry picked from commit ac6028b871)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-07 11:25:15 +11:00
github-actions[bot]
79325c96d0 [docs] BOM import (#10517) (#10520)
- Add brief section about importing BOM data

(cherry picked from commit 492613952c)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2025-10-07 11:10:12 +11:00
github-actions[bot]
62425f1f3b fix: package distribution (#10515) (#10519)
* show warning text

* fix formatting

* try to gather site-url correctly

* protect envs that are set in the config

* raise issue if they appear

* prefer app setting over file config

* preserve setting if injected via envs

(cherry picked from commit c83a9b01ce)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-07 09:13:33 +11:00
Oliver
200c1dfe06 Bump software version to 1.0.5 (#10509) 2025-10-06 22:19:19 +11:00
Oliver
8c0b35ba69 Bump InvenTree software version to 1.0.4 (#10508) 2025-10-06 22:10:48 +11:00
github-actions[bot]
b1d0f3cb9e fix(backend): ensure internal notifications are created in a valid format (#10502) (#10506)
Closes #10435

(cherry picked from commit 96a5291766)

Co-authored-by: Matthias Mair <code@mjmair.com>
2025-10-06 22:10:26 +11:00
56 changed files with 542 additions and 150 deletions

View File

@@ -22,9 +22,9 @@ before:
- contrib/packager.io/before.sh
dependencies:
- curl
- "python3.9 | python3.10 | python3.11"
- "python3.9-venv | python3.10-venv | python3.11-venv"
- "python3.9-dev | python3.10-dev | python3.11-dev"
- "python3.9 | python3.10 | python3.11 | python3.12 | python3.13 | python3.14"
- "python3.9-venv | python3.10-venv | python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
- "python3.9-dev | python3.10-dev | python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
- python3-pip
- python3-cffi
- python3-brotli

View File

@@ -30,9 +30,9 @@
}
# The default server address is configured in the .env file
# If not specified, the default address is used - http://inventree.localhost
# If not specified, the proxy listens for all http/https traffic
# 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} {
{$INVENTREE_SITE_URL:"http://, https://"} {
import log_common inventree
encode gzip

View File

@@ -101,6 +101,7 @@ services:
restart: unless-stopped
# caddy acts as reverse proxy and static file server
# You can adjust the ports that the proxy listens on via the .env file
# https://hub.docker.com/_/caddy
inventree-proxy:
container_name: inventree-proxy
@@ -109,8 +110,8 @@ services:
depends_on:
- inventree-server
ports:
- ${INVENTREE_WEB_PORT:-80}:80
- 443:443
- ${INVENTREE_HTTP_PORT:-80}:80
- ${INVENTREE_HTTPS_PORT:-443}:443
env_file:
- .env
volumes:

View File

@@ -5,7 +5,7 @@
Color_Off='\033[0m'
On_Red='\033[41m'
PYTHON_FROM=9
PYTHON_TO=12
PYTHON_TO=14
function detect_docker() {
if [ -n "$(grep docker </proc/1/cgroup)" ]; then
@@ -166,6 +166,18 @@ function detect_envs() {
export INVENTREE_DB_PASSWORD=$(jq -r '.[].database.PASSWORD' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_HOST=$(jq -r '.[].database.HOST' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_PORT=$(jq -r '.[].database.PORT' <<< ${INVENTREE_CONF_DATA})
# Parse site URL if not already set
if [ -z "${INVENTREE_SITE_URL}" ]; then
# Try to read out the app config
if [ -n "$(inventree config:get INVENTREE_SITE_URL)" ]; then
echo "# POI03| Getting site URL from app config"
export INVENTREE_SITE_URL=$(inventree config:get INVENTREE_SITE_URL)
else
echo "# POI03| Getting site URL from config file"
export INVENTREE_SITE_URL=$(jq -r '.[].site_url' <<< ${INVENTREE_CONF_DATA})
fi
fi
else
echo "# POI03| No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
@@ -190,6 +202,8 @@ function detect_envs() {
export INVENTREE_DB_HOST=${INVENTREE_DB_HOST:-samplehost}
export INVENTREE_DB_PORT=${INVENTREE_DB_PORT:-123456}
export INVENTREE_SITE_URL=${INVENTREE_SITE_URL}
export SETUP_CONF_LOADED=true
fi
@@ -209,6 +223,7 @@ function detect_envs() {
fi
echo "# POI03| INVENTREE_DB_HOST=${INVENTREE_DB_HOST}"
echo "# POI03| INVENTREE_DB_PORT=${INVENTREE_DB_PORT}"
echo "# POI03| INVENTREE_SITE_URL=${INVENTREE_SITE_URL}"
}
function create_initscripts() {
@@ -313,7 +328,7 @@ function update_or_install() {
# Run update as app user
echo "# POI12| Updating InvenTree"
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 | /;'"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && set -e && invoke update | sed -e 's/^/# POI12| u | /;'"
# Make sure permissions are correct again
echo "# POI12| Set permissions for data dir and media: ${DATA_DIR}"
@@ -373,10 +388,15 @@ function set_site() {
# Check if INVENTREE_SITE_URL in inventree config
if [ -z "$(inventree config:get INVENTREE_SITE_URL)" ]; then
echo "# POI14| Setting up InvenTree site URL"
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
# Prefer current INVENTREE_SITE_URL if set
if [ -n "${INVENTREE_SITE_URL}" ]; then
inventree config:set INVENTREE_SITE_URL=${INVENTREE_SITE_URL}
else
echo "# POI14| Setting up InvenTree site URL"
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
fi
else
echo "# POI14| Site URL already set - skipping"
echo "# POI14| Site URL already set to '$INVENTREE_SITE_URL' - skipping"
fi
}
@@ -385,11 +405,16 @@ function final_message() {
echo -e "####################################################################################"
echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in"
echo -e "${SETUP_NGINX_FILE}"
echo -e "Try opening InvenTree with either\nhttp://localhost/ or http://${INVENTREE_IP}/\n"
echo -e "Admin user data:"
echo -e " Email: ${INVENTREE_ADMIN_EMAIL}"
echo -e " Username: ${INVENTREE_ADMIN_USER}"
echo -e " Password: ${INVENTREE_ADMIN_PASSWORD}"
echo -e "Try opening InvenTree with any of \n${INVENTREE_SITE_URL} , http://localhost/ or http://${INVENTREE_IP}/ \n"
# Print admin user data only if set
if ["${INVENTREE_ADMIN_USER}" ]; then
echo -e "Admin user data:"
echo -e " Email: ${INVENTREE_ADMIN_EMAIL}"
echo -e " Username: ${INVENTREE_ADMIN_USER}"
echo -e " Password: ${INVENTREE_ADMIN_PASSWORD}"
else
echo -e "No admin set during this operation - depending on the deployment method a admin user might have been created with an initial password saved in `${SETUP_ADMIN_PASSWORD_FILE}`"
fi
echo -e "####################################################################################"
}

View File

@@ -14,7 +14,7 @@ echo "# POI01| Importing functions"
echo "# POI01| Functions imported"
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_SITE_URL,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,INVENTREE_SITE_URL,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
# Get the envs
detect_local_env

View File

@@ -6,7 +6,7 @@ echo "# PRI01| Running preinstall script - start - $(date)"
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,INVENTREE_SITE_URL,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON
if test -f "${APP_HOME}/env/bin/pip"; then
# Check if clear-generated is available

View File

@@ -73,6 +73,16 @@ sudo npx playwright install-deps
npx playwright install
```
### Dataset
The playwright tests assume that the [InvenTree test dataset](../demo.md#local-setup) is loaded into the InvenTree installation. This dataset provides a known set of data that the tests can run against.
Before running the frontend tests, ensure that a clean copy of the test dataset is loaded into your InvenTree instance, by running the following command:
```bash
invoke dev.setup-test -i
```
### Running Tests
To run the tests locally, in an interactive editor, you can use the following command:

View File

@@ -6,6 +6,10 @@ title: FAQ
Below is a list of frequently asked questions. If you are having issues with InvenTree please consult this list first!
Also, you can refer to our [GitHub page](https://github.com/inventree/inventree/issues) for known issues and bug reports - perhaps your issue has already been reported!
If you cannot resolve the issue, please refer to the [troubleshooting guide](#troubleshooting-guide).
## Installation Issues
### Installing on Windows
@@ -191,3 +195,76 @@ This means that either:
- The docker user does not have write permission to the specified directory
In either case, ensure that the directory is available *on your local machine* and the user account has the required permissions.
## Troubleshooting Guide
If you are struggling with an issue which is not covered in the FAQ above, please refer to the following troubleshooting steps.
Even if you cannot immediately resolve the issue, the information below will be very useful when reporting the issue on GitHub.
### Run Update Step
If you have recently installed or updated your InvenTree instance, make sure that you have run the `invoke update` command, which will perform any required database migrations and other update tasks. This is a *critical step* after any system update.
#### Docker
If you are have installed InvenTree via Docker:
```bash
docker-compose exec inventree-server invoke update
```
#### Installer
If you have installed InvenTree via the installer script:
```bash
inventree run invoke update
```
### Logged Errors
Look at the logged error reports in the admin section - you will need to be an administrator to access this section. If a critical error has occurred, it may be logged here.
### GitHub Issues
Before raising a new issue, please check the [GitHub issues page](https://github.com/inventree/inventree) for reported issues. If your issue is a common one, it may already have been reported - and perhaps even resolved!
### Web Browser Console
If you are experiencing issues with the web interface, you can open the developer console in your web browser to check for error messages. This may vary slightly between web browsers, but there is a wealth of information available online if you need help.
Once the developer console is open, there are two places to check for error messages:
#### Console Tab
Navigate to the *Console* tab in the developer tools. Any error messages will be highlighted in red. They may indicate either a rendering issue, or a problem with a network request.
#### Network Tab
Navigate to the *Network* tab in the developer tools. Check for any requests which have a status code of 400 or greater (indicating an error). Click on the request to see more information about the error.
### Server Logs
Finally, you can check the server logs for error messages. The location of the server logs will depend on how you have installed InvenTree.
#### Docker
If you are using Docker, you can view the server logs with the following command:
To display logs for all running containers:
```bash
docker compose logs
```
Refer to the [docker documentation](./start/docker_install.md#viewing-logs) for more information.
#### Installer
If you are using the installer script, you can view the server logs with the following command:
```bash
inventree logs
```
Refer to the [installer documentation](./start/installer.md#viewing-logs) for more information.

View File

@@ -88,11 +88,17 @@ Note that inherited BOM Line Items only flow "downwards" in the variant inherita
## BOM Creation
BOMs can be created manually, by adjusting individual line items, or by upload an existing BOM file.
BOMs can be created manually, by adjusting individual line items, or by uploading (importing) an existing BOM file.
### Importing a BOM
BOM data can be imported from an existing file (such as CSV or Excel) from the *BOM* panel for a particular part/assembly. This process is a special case of the more general [data import process](../settings/import.md).
At the top of the *BOM* panel, click on the {{ icon("file-arrow-left", color="green", title="Import BOM Data") }} icon to open the import dialog.
### Add BOM Item
To manually add a BOM item, navigate to the part/assembly detail page then click on the "BOM" tab. On top of the tab view, click on the {{ icon("edit", color="blue", title="Edit") }} icon then, after the page reloads, click on the {{ icon("plus-circle") }} icon.
To manually add a BOM item, navigate to the part/assembly detail page then click on the *BOM* panel tab. On top of the *BOM* view, click on the {{ icon("edit", color="blue", title="Edit") }} icon then, after the page reloads, click on the {{ icon("plus-circle") }} icon.
The `Create BOM Item` form will be displayed:

View File

@@ -9,9 +9,12 @@ Errors - These are critical errors which should be addressed as soon as possible
#### INVE-E1
**No frontend included - Backend/web**
Only stable / production releases of InvenTree include the frontend panel. This is both a measure of resource-saving and attack surface reduction. If you want to use the frontend panel, you can either:″
Only stable / production releases of InvenTree include the frontend panel. This is both a measure of resource-saving and attack surface reduction.
If you want to use the frontend panel, you can either:
- use a docker image that is version-tagged or the stable version
- use a package version that is from the stable or version stream
- use a package version that is from the stable or version stream - if you are and it is not working, run `sudo inventree run invoke update` to re-run the upgrade
- install node and yarn on the server to build the frontend with the [invoke](../start/invoke.md) task `int.frontend-build`
Raise an issue if none of these options work.

View File

@@ -183,6 +183,32 @@ docker compose run --rm inventree-server invoke export-records -f /home/inventre
This will export database records to the file `data.json` in your mounted volume directory.
## Viewing Logs
To view the logs for the InvenTree container(s), use the following command:
```bash
docker compose logs
```
To view the logs for a specific container, use the following command:
```bash
docker compose logs <container-name>
```
e.g.
```bash
docker compose logs inventree-server
```
You can also "follow" the logs in real time, using the `-f` flag:
```bash
docker compose logs -f
```
## Further Configuration
### Check your security posture

View File

@@ -37,7 +37,7 @@ The installer creates the following directories:
| `/opt/inventree/` | InvenTree application files |
| `/opt/inventree/data/` | InvenTree data files |
#### Performed steps
#### Performed Steps
The installer script performs the following functions:
@@ -111,7 +111,7 @@ To stop the automatic generation of an admin user, generate an empty file needs
By default, InvenTree is served internally on port 6000 and then proxied via Nginx. The config is placed in `/etc/nginx/sites-enabled/inventree.conf` and overwritten on each update. The location can be set with the environment variable `SETUP_NGINX_FILE`.
This only serves an HTTP version of InvenTree, to use HTTPS (recommended for production) or customize any further an additional config file should be used.
#### Extra python packages
#### Extra Python Packages
Extra python packages can be installed by setting the environment variable `SETUP_EXTRA_PIP`.
#### Database Options
@@ -120,25 +120,51 @@ The used database backend can be configured with environment variables (before t
## Moving Data
To change the data storage location, link the new location to `/opt/inventree/data`.
A rough outline of steps to achieve this could be:
- shut down the app service(s) `inventree` and webserver `nginx`
- copy data to the new location
- check everything was transferred successfully
- delete the old location
- create a symlink from the old location to the new one
- start up the services again
To change the data storage location, link the new location to `/opt/inventree/data`. A rough outline of steps to achieve this could be:
- Shut down the app service(s) `inventree` and webserver `nginx`
- Copy data to the new location
- Check everything was transferred successfully
- Delete the old location
- Create a symlink from the old location to the new one
- Start up the services again
## Updating InvenTree
To update InvenTree run `apt install --only-upgrade inventree` - this might need to be run as a sudo user.
To update InvenTree run the following command, which updates the InvenTree package to the latest version:
```bash
apt install --only-upgrade inventree
```
Note that this command may need to be run as a sudo user.
## Controlling InvenTree
### Services
InvenTree installs multiple services that can be controlled with your local system runner (`service` or `systemctl`).
The service `inventree` controls everything, `inventree-web` (the [InvenTree web server](./processes.md#web-server)) and `inventree-worker` the [background worker(s)](./processes.md#background-worker).
InvenTree installs multiple services that can be controlled with your local system runner (`service` or `systemctl`):
- `inventree` - The main InvenTree service that controls the web server and background worker(s)
- `inventree-web` - The InvenTree [web server](./processes.md#web-server) process(es)
- `inventree-worker` - The InvenTree [background worker(s)](./processes.md#background-worker) process(es)
#### Restarting Services
To restart the InvenTree services, use the following commands as necessary:
```bash
# Restart all InvenTree services
inventree restart
# Restart the web server only
inventree restart web
# Restart the worker only
inventree restart worker
```
### Scaling Workers
More instances of the worker can be instantiated from the command line. This is only meant for advanced users.
@@ -180,6 +206,26 @@ For example, to print InvenTree version information:
inventree run invoke version
```
### Viewing Logs
To view the logs of the InvenTree services, use the following commands:
```bash
inventree logs
```
To view just the tail of the logs, use:
```bash
inventree logs --tail
```
Or, to follow the logs in real-time:
```bash
inventree logs --follow
```
## Architecture
The packages are provided by [packager.io](https://packager.io/). They are built each time updates are pushed to GitHub and released about 10 minutes later. The local package index must be updated to see the new release in the package manager.

View File

@@ -132,9 +132,10 @@ nav:
- Docker:
- Introduction: start/docker.md
- Installation: start/docker_install.md
- Installer:
- Installer: start/installer.md
- Bare Metal:
- Introduction: start/install.md
- Installer: start/installer.md
- Production: start/bare_prod.md
- Development: start/bare_dev.md
- User Accounts: start/accounts.md

View File

@@ -76,8 +76,9 @@ def get_root_dir() -> Path:
def inventreeInstaller() -> Optional[str]:
"""Returns the installer for the running codebase - if set or detectable."""
# First look in the environment variables, e.g. if running in docker
load_version_file()
# First look in the environment variables, e.g. if running in docker
installer = os.environ.get('INVENTREE_PKG_INSTALLER', '')
if installer:
@@ -121,6 +122,11 @@ def get_testfolder_dir() -> Path:
return get_base_dir().joinpath('_testfolder').resolve()
def get_version_file() -> Path:
"""Returns the path of the InvenTree VERSION file. This does not ensure that the file exists."""
return get_root_dir().joinpath('VERSION').resolve()
def ensure_dir(path: Path, storage=None) -> None:
"""Ensure that a directory exists.
@@ -592,3 +598,28 @@ def check_config_dir(
pass
return
VERSION_LOADED = False
"""Flag to indicate if the VERSION file has been loaded in this process."""
def load_version_file():
"""Load the VERSION file if it exists and place the contents into the general execution environment.
Returns:
True if the VERSION file was loaded (now or previously), False otherwise.
"""
global VERSION_LOADED
if VERSION_LOADED:
return True
# Load the VERSION file if it exists
from dotenv import load_dotenv
version_file = get_version_file()
if version_file.exists():
load_dotenv(version_file)
VERSION_LOADED = True
return True
return False

View File

@@ -15,6 +15,8 @@ from rest_framework.fields import URLField as RestURLField
from rest_framework.fields import empty
import InvenTree.helpers
import InvenTree.ready
from common.currency import currency_code_default
from common.settings import get_global_setting
from .validators import AllowedURLValidator, allowable_url_schemes
@@ -59,7 +61,7 @@ class InvenTreeURLField(models.URLField):
def money_kwargs(**kwargs):
"""Returns the database settings for MoneyFields."""
from common.currency import currency_code_default, currency_code_mappings
from common.currency import currency_code_mappings
# Default values (if not specified)
if 'max_digits' not in kwargs:
@@ -71,8 +73,14 @@ def money_kwargs(**kwargs):
if 'currency_choices' not in kwargs:
kwargs['currency_choices'] = currency_code_mappings()
if 'default_currency' not in kwargs:
kwargs['default_currency'] = currency_code_default()
if InvenTree.ready.isRunningMigrations():
# During migrations, avoid setting a default currency
# This prevents issues related to early evaluation of the default currency value
kwargs['default_currency'] = ''
else:
# Override default currency with a callable function
# This ensures that the default currency is always up-to-date
kwargs['default_currency'] = currency_code_default
return kwargs

View File

@@ -17,6 +17,7 @@ from error_report.middleware import ExceptionProcessor
from common.settings import get_global_setting
from InvenTree.AllUserRequire2FAMiddleware import AllUserRequire2FAMiddleware
from InvenTree.cache import create_session_cache, delete_session_cache
from InvenTree.config import CONFIG_LOOKUPS, inventreeInstaller
from users.models import ApiToken
logger = structlog.get_logger('inventree')
@@ -238,13 +239,29 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin):
accessed_scheme = request._current_scheme_host
referer = urlsplit(accessed_scheme)
# Ensure that the settings are set correctly with the current request
matches = (
(accessed_scheme and not accessed_scheme.startswith(settings.SITE_URL))
if not settings.SITE_LAX_PROTOCOL_CHECK
else not is_same_domain(referer.netloc, urlsplit(settings.SITE_URL).netloc)
site_url = urlsplit(settings.SITE_URL)
# Check if the accessed URL matches the SITE_URL setting
site_url_match = (
(
# Exact match on domain
is_same_domain(referer.netloc, site_url.netloc)
and referer.scheme == site_url.scheme
)
or (
# Lax protocol match, accessed URL starts with SITE_URL
settings.SITE_LAX_PROTOCOL_CHECK
and accessed_scheme.startswith(settings.SITE_URL)
)
or (
# Lax protocol match, same domain
settings.SITE_LAX_PROTOCOL_CHECK
and referer.hostname == site_url.hostname
)
)
if matches:
if not site_url_match:
# The accessed URL does not match the SITE_URL setting
if (
isinstance(settings.CSRF_TRUSTED_ORIGINS, list)
and len(settings.CSRF_TRUSTED_ORIGINS) > 1
@@ -252,23 +269,41 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin):
# The used url might not be the primary url - next check determines if in a trusted origins
pass
else:
msg = f'INVE-E7: The used path `{accessed_scheme}` does not match the SITE_URL `{settings.SITE_URL}`'
source = CONFIG_LOOKUPS.get('INVENTREE_SITE_URL', {}).get(
'source', 'unknown'
)
dpl_method = inventreeInstaller()
msg = f'INVE-E7: The visited path `{accessed_scheme}` does not match the SITE_URL `{settings.SITE_URL}`. The INVENTREE_SITE_URL is set via `{source}` config method - deployment method `{dpl_method}`'
logger.error(msg)
return render(
request, 'config_error.html', {'error_message': msg}, status=500
)
# Check trusted origins
if not any(
is_same_domain(referer.netloc, host)
for host in [
urlsplit(origin).netloc.lstrip('*')
trusted_origins_match = (
# Matching domain found in allowed origins
any(
is_same_domain(referer.netloc, host)
for host in [
urlsplit(origin).netloc.lstrip('*')
for origin in settings.CSRF_TRUSTED_ORIGINS
]
)
) or (
# Lax protocol match allowed
settings.SITE_LAX_PROTOCOL_CHECK
and any(
referer.hostname == urlsplit(origin).hostname
for origin in settings.CSRF_TRUSTED_ORIGINS
]
):
)
)
# Check trusted origins
if not trusted_origins_match:
msg = f'INVE-E7: The used path `{accessed_scheme}` is not in the TRUSTED_ORIGINS'
logger.error(msg)
return render(
request, 'config_error.html', {'error_message': msg}, status=500
)
# All checks passed
return None

View File

@@ -23,7 +23,6 @@ from django.http import Http404, HttpResponseGone
import structlog
from corsheaders.defaults import default_headers as default_cors_headers
from dotenv import load_dotenv
from InvenTree.cache import get_cache_config, is_global_cache_enabled
from InvenTree.config import (
@@ -73,11 +72,7 @@ BASE_DIR = config.get_base_dir()
# Load configuration data
CONFIG = config.load_config_data(set_cache=True)
# Load VERSION data if it exists
version_file = config.get_root_dir().joinpath('VERSION')
if version_file.exists():
load_dotenv(version_file)
config.load_version_file()
# Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production!

View File

@@ -668,6 +668,11 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True) -> b
Returns bool indicating if migrations are up to date
"""
from . import ready
if ready.isRunningMigrations() or ready.isRunningBackup():
# Migrations are already running!
return False
def set_pending_migrations(n: int):
"""Helper function to inform the user about pending migrations."""
@@ -718,6 +723,8 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True) -> b
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
raise e
logger.exception('Error during migrations: %s', e)
except Exception as e: # pragma: no cover
logger.exception('Error during migrations: %s', e)
else:
set_pending_migrations(0)

View File

@@ -112,6 +112,15 @@ class MiddlewareTests(InvenTreeTestCase):
def test_site_lax_protocol(self):
"""Test that the site URL check is correctly working with/without lax protocol check."""
# Test that a completely different host fails
with self.settings(
SITE_URL='https://testserver', CSRF_TRUSTED_ORIGINS=['https://testserver']
):
response = self.client.get(
reverse('web'), HTTP_HOST='otherhost.example.com'
)
self.assertContains(response, 'INVE-E7: The visited path', status_code=500)
# Simple setup with proxy
with self.settings(
SITE_URL='https://testserver', CSRF_TRUSTED_ORIGINS=['https://testserver']
@@ -126,7 +135,25 @@ class MiddlewareTests(InvenTreeTestCase):
SITE_LAX_PROTOCOL_CHECK=False,
):
response = self.client.get(reverse('web'))
self.assertContains(response, 'INVE-E7: The used path', status_code=500)
self.assertContains(response, 'INVE-E7: The visited path', status_code=500)
def test_site_url_port(self):
"""URL checks with different ports."""
with self.settings(
SITE_URL='https://testserver:8000',
CSRF_TRUSTED_ORIGINS=['https://testserver:8000'],
):
response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008')
self.do_positive_test(response)
# Try again with strict protocol check
with self.settings(
SITE_URL='https://testserver:8000',
CSRF_TRUSTED_ORIGINS=['https://testserver:8000'],
SITE_LAX_PROTOCOL_CHECK=False,
):
response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008')
self.assertContains(response, 'INVE-E7: The visited path', status_code=500)
def test_site_url_checks_multi(self):
"""Test that the site URL check is correctly working in a multi-site setup."""
@@ -149,7 +176,7 @@ class MiddlewareTests(InvenTreeTestCase):
)
self.do_positive_test(response)
# A non-trsuted origin must still fail in multi - origin setup
# A non-trusted origin must still fail in multi - origin setup
response = self.client.get(
'https://not-my-testserver.example.com/web/',
SERVER_NAME='not-my-testserver.example.com',
@@ -194,7 +221,9 @@ class MiddlewareTests(InvenTreeTestCase):
):
response = self.client.get(reverse('web'))
self.assertContains(
response, 'INVE-E7: The used path `http://testserver` ', status_code=500
response,
'INVE-E7: The visited path `http://testserver` ',
status_code=500,
)
self.assertNotContains(
response, 'window.INVENTREE_SETTINGS', status_code=500
@@ -212,6 +241,6 @@ class MiddlewareTests(InvenTreeTestCase):
# Check that the correct step triggers the error message
self.assertContains(
response,
'INVE-E7: The used path `http://testserver` does not match',
'INVE-E7: The visited path `http://testserver` does not match',
status_code=500,
)

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 = '1.0.3'
INVENTREE_SW_VERSION = '1.0.8'
logger = logging.getLogger('inventree')
@@ -269,7 +269,7 @@ def inventreeBranch():
branch = os.environ.get('INVENTREE_PKG_BRANCH', '')
if branch:
return branch
return ' '.join(branch.splitlines())
if main_branch is None:
return None

View File

@@ -11,6 +11,7 @@ import structlog
from moneyed import CURRENCIES
import InvenTree.helpers
import InvenTree.ready
logger = structlog.get_logger('inventree')

View File

@@ -3,7 +3,6 @@
from django.db import migrations, connection
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='supplierpricebreak',
name='price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
),
migrations.AddField(
model_name='supplierpricebreak',
name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
),
]

View File

@@ -2,7 +2,6 @@
import InvenTree.validators
import common.currency
import common.settings
from django.db import migrations, models

View File

@@ -110,7 +110,7 @@ sentry_enabled: False
#sentry_dsn: https://custom@custom.ingest.sentry.io/custom
# OpenTelemetry tracing/metrics - disabled by default - refer to the documentation for full list of options
# This can be used to send tracing data, logs and metrics to OpenTelemtry compatible backends
# This can be used to send tracing data, logs and metrics to OpenTelemetry compatible backends
tracing:
enabled: false
@@ -142,9 +142,9 @@ allowed_hosts:
# 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:
@@ -203,7 +203,7 @@ remote_login_header: HTTP_REMOTE_USER
# - 'allauth.socialaccount.providers.github'
# Add specific settings for social account providers (if required)
# Refer to the djngo-allauth documentation for more details:
# Refer to the django-allauth documentation for more details:
# https://docs.allauth.org/en/latest/socialaccount/provider_configuration.html
# social_providers:
# github:

View File

@@ -3,7 +3,6 @@
from django.db import migrations
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -17,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='purchaseorderlineitem',
name='purchase_price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
),
migrations.AddField(
model_name='purchaseorderlineitem',
name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
),
]

View File

@@ -2,8 +2,6 @@
from django.db import migrations
import djmoney.models.fields
import common.currency
import common.settings
class Migration(migrations.Migration):
@@ -16,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='purchaseorderlineitem',
name='purchase_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'),
),
]

View File

@@ -2,7 +2,6 @@
from django.db import migrations
import common.currency
import common.settings
import djmoney.models.fields
@@ -16,11 +15,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='salesorderlineitem',
name='sale_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Unit sale price', max_digits=19, null=True, verbose_name='Sale Price'),
),
migrations.AddField(
model_name='salesorderlineitem',
name='sale_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
),
]

View File

@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='partsellpricebreak',
name='price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
),
migrations.AddField(
model_name='partsellpricebreak',
name='price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3),
field=djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3),
),
]

View File

@@ -3,7 +3,6 @@
import InvenTree.fields
import django.core.validators
import common.currency
import common.settings
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
@@ -21,8 +20,8 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity')),
('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default=common.currency.currency_code_default(), editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
('price_currency', djmoney.models.fields.CurrencyField(choices=common.currency.currency_code_mappings(), default='', editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price')),
('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='internalpricebreaks', to='part.part', verbose_name='Part')),
],
options={

View File

@@ -8,7 +8,7 @@ import djmoney.models.validators
import InvenTree.fields
import common.currency
import common.settings
class Migration(migrations.Migration):

View File

@@ -4271,6 +4271,11 @@ class BomItem(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel):
for sub in self.substitutes.all():
parts.add(sub.part)
# Account for variants of the substitute part (if allowed)
if allow_variants and self.allow_variants:
for sub_variant in sub.part.get_descendants(include_self=False):
parts.add(sub_variant)
valid_parts = []
for p in parts:

View File

@@ -38,6 +38,10 @@ class InvenTreeUINotifications(NotificationMixin, InvenTreePlugin):
if not users:
return False
# Ensure that there is always target object - see https://github.com/inventree/InvenTree/issues/10435
if not target:
target = self.plugin_config()
# Bulk create notification messages for all provided users
for user in users:
entries.append(

View File

@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='stockitem',
name='purchase_price',
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
),
migrations.AddField(
model_name='stockitem',
name='purchase_price_currency',
field=djmoney.models.fields.CurrencyField(choices=common.currency.all_currency_codes(), default=common.currency.currency_code_default(), editable=False, max_length=3),
field=djmoney.models.fields.CurrencyField(choices=common.currency.all_currency_codes(), default='', editable=False, max_length=3),
),
]

View File

@@ -2,7 +2,6 @@
from django.db import migrations
import djmoney.models.fields
import common.currency
class Migration(migrations.Migration):
@@ -15,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='stockitem',
name='purchase_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency=common.currency.currency_code_default(), help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
),
]

View File

@@ -6,6 +6,8 @@ from django.core.exceptions import ValidationError
from django.db.models import Sum
from django.test import override_settings
from djmoney.money import Money
from build.models import Build
from common.models import InvenTreeSetting
from company.models import Company
@@ -803,6 +805,27 @@ class StockTest(StockTestBase):
self.assertTrue(check_func())
def test_purchase_price(self):
"""Test purchase price field."""
from common.currency import currency_code_default
from common.settings import set_global_setting
part = Part.objects.filter(virtual=False).first()
for currency in ['AUD', 'USD', 'JPY']:
set_global_setting('INVENTREE_DEFAULT_CURRENCY', currency)
self.assertEqual(currency_code_default(), currency)
# Create stock item, do not specify currency - should get default
item = StockItem.objects.create(part=part, quantity=10)
self.assertEqual(item.purchase_price_currency, currency)
# Create stock item, specify currency
item = StockItem.objects.create(
part=part, quantity=10, purchase_price=Money(5, 'GBP')
)
self.assertEqual(item.purchase_price_currency, 'GBP')
class StockBarcodeTest(StockTestBase):
"""Run barcode tests for the stock app."""

View File

@@ -13,7 +13,34 @@
</head>
<body>
<div id="root"></div>
<div id="root">
<div id="update-warning" style="display: none;">
If you see this text there might be an issue with your update.
<br>
See <a href="https://docs.inventree.org/en/stable/settings/error_codes/#inve-e1">INVE-E1</a> in the docs for help.
</div>
<div id="no-javascript-warning" style="display: none;">
<hr>
This application requires JavaScript to function correctly. Please enable JavaScript in your browser settings.
</div>
<noscript>
<!-- fallback if javascript is blocked -->
<style>
#update-warning { display: block !important; }
#no-javascript-warning { display: block !important; }
</style>
</noscript>
<script>
setTimeout(() => {
let warningElement = document.getElementById('update-warning');
if (warningElement) {
warningElement.style.display = 'block';
}
}, 1000);
</script>
</div>
<div id="spa_settings">{% spa_settings %}</div>
{% if bundle == "NOT_FOUND" %}
<div id="spa_bundle_error">

View File

@@ -24,14 +24,17 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
title: t`Subscribed Parts`,
description: t`Show the number of parts which you have subscribed to`,
modelType: ModelType.part,
params: { starred: true }
params: { starred: true, active: true }
}),
QueryCountDashboardWidget({
label: 'sub-cat',
title: t`Subscribed Categories`,
description: t`Show the number of part categories which you have subscribed to`,
modelType: ModelType.partcategory,
params: { starred: true }
params: {
starred: true,
top_level: 'none'
}
}),
QueryCountDashboardWidget({
label: 'invalid-bom',

View File

@@ -68,27 +68,29 @@ const AboutContent = ({
});
function fillTable(lookup: AboutLookupRef[], data: any, alwaysLink = false) {
return lookup.map((map: AboutLookupRef, idx) => (
<Table.Tr key={idx}>
<Table.Td>{map.title}</Table.Td>
<Table.Td>
<Group justify='space-between' gap='xs'>
{alwaysLink ? (
<Anchor href={data[map.ref]} target='_blank'>
{data[map.ref]}
</Anchor>
) : map.link ? (
<Anchor href={map.link} target='_blank'>
{data[map.ref]}
</Anchor>
) : (
data[map.ref]
)}
{map.copy && <CopyButton value={data[map.ref]} />}
</Group>
</Table.Td>
</Table.Tr>
));
return lookup
.filter((entry: AboutLookupRef) => !!data[entry.ref])
.map((entry: AboutLookupRef, idx) => (
<Table.Tr key={idx}>
<Table.Td>{entry.title}</Table.Td>
<Table.Td>
<Group justify='space-between' gap='xs'>
{alwaysLink ? (
<Anchor href={data[entry.ref]} target='_blank'>
{data[entry.ref]}
</Anchor>
) : entry.link ? (
<Anchor href={entry.link} target='_blank'>
{data[entry.ref]}
</Anchor>
) : (
data[entry.ref]
)}
{entry.copy && <CopyButton value={data[entry.ref]} />}
</Group>
</Table.Td>
</Table.Tr>
));
}
/* renderer */
if (isLoading) return <Trans>Loading</Trans>;

View File

@@ -27,7 +27,7 @@ export function getActions(navigate: NavigateFunction) {
id: 'dashboard',
label: t`Dashboard`,
description: t`Go to the InvenTree dashboard`,
onClick: () => {}, // navigate(menuItems.dashboard.link),
onClick: () => navigate('/'),
leftSection: <IconLink size='1.2rem' />
},
{

View File

@@ -219,7 +219,7 @@ export function useBuildOrderOutputFields({
location: {
value: location,
onValueChange: (value: any) => {
setQuantity(value);
setLocation(value);
}
},
auto_allocate: {

View File

@@ -212,7 +212,8 @@ export function useStockFields({
icon: <IconCurrencyDollar />
},
purchase_price_currency: {
icon: <IconCoins />
icon: <IconCoins />,
default: globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY')
},
packaging: {
icon: <IconPackage />

View File

@@ -106,14 +106,37 @@ export async function doBasicLogin(
}
})
.catch(async (err) => {
if (err?.response?.status == 401) {
await handlePossibleMFAError(err);
} else if (err?.response?.status == 409) {
notifications.hide('auth-login-error');
if (err?.response?.status) {
switch (err.response.status) {
case 401:
await handlePossibleMFAError(err);
break;
case 409:
notifications.show({
title: t`Already logged in`,
message: t`There is a conflicting session on the server for this browser. Please logout of that first.`,
color: 'red',
id: 'auth-login-error',
autoClose: false
});
break;
default:
notifications.show({
title: `${t`Login failed`} (${err.response.status})`,
message: t`Check your input and try again.`,
id: 'auth-login-error',
color: 'red'
});
break;
}
} else {
notifications.show({
title: t`Already logged in`,
message: t`There is a conflicting session on the server for this browser. Please logout of that first.`,
title: t`Login failed`,
message: t`No response from server.`,
color: 'red',
autoClose: false
id: 'login-error'
});
}
});

View File

@@ -238,17 +238,6 @@ export default function BuildDetail() {
icon: 'manufacturers',
hidden: !build.external
},
{
type: 'text',
name: 'purchase_order',
label: t`Purchase Order`,
icon: 'purchase_orders',
copy: true,
hidden: !build.external,
value_formatter: () => {
return 'TODO: external PO';
}
},
{
type: 'text',
name: 'reference',
@@ -690,6 +679,7 @@ export default function BuildDetail() {
<PrintingActions
modelType={ModelType.build}
items={[build.pk]}
enableLabels
enableReports
/>,
<OptionsActionDropdown

View File

@@ -47,6 +47,8 @@ interface PricingOverviewEntry {
min_value: number | null | undefined;
max_value: number | null | undefined;
visible?: boolean;
min_currency?: string | null | undefined;
max_currency?: string | null | undefined;
currency?: string | null | undefined;
}
@@ -161,7 +163,8 @@ export default function PricingOverviewPanel({
return '-';
}
return formatCurrency(record?.min_value, {
currency: record.currency ?? pricing?.currency
currency:
record.min_currency ?? record.currency ?? pricing?.currency
});
}
},
@@ -174,7 +177,8 @@ export default function PricingOverviewPanel({
}
return formatCurrency(record?.max_value, {
currency: record.currency ?? pricing?.currency
currency:
record.max_currency ?? record.currency ?? pricing?.currency
});
}
}
@@ -189,6 +193,9 @@ export default function PricingOverviewPanel({
icon: <IconExclamationCircle />,
min_value: Number.parseFloat(pricing?.override_min),
max_value: Number.parseFloat(pricing?.override_max),
min_currency: pricing?.override_min_currency ?? pricing?.currency,
max_currency: pricing?.override_max_currency ?? pricing?.currency,
currency: pricing?.currency,
valid: pricing?.override_min != null && pricing?.override_max != null
},
{

View File

@@ -460,6 +460,7 @@ export default function PurchaseOrderDetail() {
<PrintingActions
modelType={ModelType.purchaseorder}
items={[order.pk]}
enableLabels
enableReports
/>,
<OptionsActionDropdown

View File

@@ -458,6 +458,7 @@ export default function ReturnOrderDetail() {
modelType={ModelType.returnorder}
items={[order.pk]}
enableReports
enableLabels
/>,
<OptionsActionDropdown
tooltip={t`Order Actions`}

View File

@@ -516,6 +516,7 @@ export default function SalesOrderDetail() {
modelType={ModelType.salesorder}
items={[order.pk]}
enableReports
enableLabels
/>,
<OptionsActionDropdown
tooltip={t`Order Actions`}

View File

@@ -352,6 +352,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
// Reset the pagination state when the search term changes
useEffect(() => {
tableState.setPage(1);
tableState.clearSelectedRecords();
}, [
tableState.searchTerm,
tableState.filterSet.activeFilters,

View File

@@ -250,7 +250,10 @@ export default function InvenTreeTableHeader({
<HoverCard
position='bottom-end'
withinPortal={true}
disabled={!tableState.filterSet.activeFilters?.length}
disabled={
hasCustomFilters ||
!tableState.filterSet.activeFilters?.length
}
>
<HoverCard.Target>
<Tooltip

View File

@@ -248,6 +248,7 @@ export function BuildOrderTable({
modelType: ModelType.build,
enableSelection: true,
enableReports: true,
enableLabels: true,
enableDownload: true
}}
/>

View File

@@ -190,7 +190,8 @@ export function PurchaseOrderTable({
modelType: ModelType.purchaseorder,
enableSelection: true,
enableDownload: true,
enableReports: true
enableReports: true,
enableLabels: true
}}
/>
</>

View File

@@ -189,7 +189,8 @@ export function ReturnOrderTable({
modelType: ModelType.returnorder,
enableSelection: true,
enableDownload: true,
enableReports: true
enableReports: true,
enableLabels: true
}}
/>
</>

View File

@@ -201,7 +201,8 @@ export function SalesOrderTable({
modelType: ModelType.salesorder,
enableSelection: true,
enableDownload: true,
enableReports: true
enableReports: true,
enableLabels: true
}}
/>
</>

View File

@@ -507,7 +507,10 @@ export function StockItemTable({
return {
items: table.selectedRecords,
model: ModelType.stockitem,
refresh: table.refreshTable,
refresh: () => {
table.clearSelectedRecords();
table.refreshTable();
},
filters: {
in_stock: true
}

View File

@@ -9,8 +9,8 @@ import { doLogin } from './login.js';
test('Login - Failures', async ({ page }) => {
const loginWithError = async () => {
await page.getByRole('button', { name: 'Log In' }).click();
await page.getByText('Login failed').waitFor();
await page.getByText('Check your input and try again').waitFor();
await page.getByText('Login failed', { exact: true }).waitFor();
await page.getByText('Check your input and try again').first().waitFor();
await page.locator('#login').getByRole('button').click();
};

View File

@@ -1501,10 +1501,10 @@ Static {get_static_dir(error=False) or NOT_SPECIFIED}
Backup {get_backup_dir(error=False) or NOT_SPECIFIED}
Versions:
Python {python_version()}
Django {InvenTreeVersion.inventreeDjangoVersion()}
InvenTree {InvenTreeVersion.inventreeVersion()}
API {InvenTreeVersion.inventreeApiVersion()}
Python {python_version()}
Django {InvenTreeVersion.inventreeDjangoVersion()}
Node {node if node else NA}
Yarn {yarn if yarn else NA}