mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-18 12:56:31 -06:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa698e7e2b | ||
|
|
c59fd55a00 | ||
|
|
486e338b0b | ||
|
|
eb32546824 | ||
|
|
cc508a544c | ||
|
|
c471a1cd38 | ||
|
|
1249ae3a84 | ||
|
|
4fe949811d | ||
|
|
afdb4090bf | ||
|
|
ae2cf931a5 | ||
|
|
6fa54c0e0e | ||
|
|
ff79ab87e5 | ||
|
|
9fe290b01f | ||
|
|
a63fe64aa2 | ||
|
|
b6acbf0a48 | ||
|
|
4a83b98cd9 | ||
|
|
fe1cc56e94 | ||
|
|
ef35591110 | ||
|
|
df7817735d | ||
|
|
c3ea3a5566 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,7 +14,7 @@ Only stable / production releases of InvenTree include the frontend panel. This
|
||||
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 - if you are and it is not working, run `sudo inventree run cli update` to re-run the upgrade
|
||||
- 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -239,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
|
||||
@@ -263,17 +279,31 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin):
|
||||
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
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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']
|
||||
@@ -128,6 +137,24 @@ class MiddlewareTests(InvenTreeTestCase):
|
||||
response = self.client.get(reverse('web'))
|
||||
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."""
|
||||
# multi-site setup with trusted origins
|
||||
@@ -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',
|
||||
|
||||
@@ -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.5'
|
||||
INVENTREE_SW_VERSION = '1.0.7'
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -13,7 +13,34 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">If you see this text there might be an Issue with your update - 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="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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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' />
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -460,6 +460,7 @@ export default function PurchaseOrderDetail() {
|
||||
<PrintingActions
|
||||
modelType={ModelType.purchaseorder}
|
||||
items={[order.pk]}
|
||||
enableLabels
|
||||
enableReports
|
||||
/>,
|
||||
<OptionsActionDropdown
|
||||
|
||||
@@ -458,6 +458,7 @@ export default function ReturnOrderDetail() {
|
||||
modelType={ModelType.returnorder}
|
||||
items={[order.pk]}
|
||||
enableReports
|
||||
enableLabels
|
||||
/>,
|
||||
<OptionsActionDropdown
|
||||
tooltip={t`Order Actions`}
|
||||
|
||||
@@ -516,6 +516,7 @@ export default function SalesOrderDetail() {
|
||||
modelType={ModelType.salesorder}
|
||||
items={[order.pk]}
|
||||
enableReports
|
||||
enableLabels
|
||||
/>,
|
||||
<OptionsActionDropdown
|
||||
tooltip={t`Order Actions`}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -248,6 +248,7 @@ export function BuildOrderTable({
|
||||
modelType: ModelType.build,
|
||||
enableSelection: true,
|
||||
enableReports: true,
|
||||
enableLabels: true,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -190,7 +190,8 @@ export function PurchaseOrderTable({
|
||||
modelType: ModelType.purchaseorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
enableReports: true,
|
||||
enableLabels: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -189,7 +189,8 @@ export function ReturnOrderTable({
|
||||
modelType: ModelType.returnorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
enableReports: true,
|
||||
enableLabels: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -201,7 +201,8 @@ export function SalesOrderTable({
|
||||
modelType: ModelType.salesorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
enableReports: true,
|
||||
enableLabels: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
4
tasks.py
4
tasks.py
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user