mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-24 09:57:40 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into make-fields-filterable
This commit is contained in:
@@ -12,10 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added `order_queryset` report helper function in [#10439](https://github.com/inventree/InvenTree/pull/10439)
|
||||
- Added much more detailed status information for machines to the API endpoint (including backend and frontend changes) in [#10381](https://github.com/inventree/InvenTree/pull/10381)
|
||||
- Added ability to partially complete and partially scrap build outputs in [#10499](https://github.com/inventree/InvenTree/pull/10499)
|
||||
- Added support for Redis ACL user-based authentication in [#10551](https://github.com/inventree/InvenTree/pull/10551)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed site URL check to allow protocol mismatches if `INVENTREE_SITE_LAX_PROTOCOL` is set to `True` (default) in [#10454](https://github.com/inventree/InvenTree/pull/10454)
|
||||
- Changed call signature of `get_global_setting` to use `environment_key` instead of `enviroment_key` in [#10557](https://github.com/inventree/InvenTree/pull/10557)
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -309,6 +309,7 @@ The following cache settings are available:
|
||||
| INVENTREE_CACHE_HOST | cache.host | Cache server host | *Not specified* |
|
||||
| INVENTREE_CACHE_PORT | cache.port | Cache server port | 6379 |
|
||||
| INVENTREE_CACHE_PASSWORD | cache.password | Cache server password | none |
|
||||
| INVENTREE_CACHE_USER | cache.user | Cache server username | none |
|
||||
| INVENTREE_CACHE_CONNECT_TIMEOUT | cache.connect_timeout | Cache connection timeout (seconds) | 3 |
|
||||
| INVENTREE_CACHE_TIMEOUT | cache.timeout | Cache timeout (seconds) | 3 |
|
||||
| INVENTREE_CACHE_TCP_KEEPALIVE | cache.tcp_keepalive | Cache TCP keepalive | True |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 407
|
||||
INVENTREE_API_VERSION = 408
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v408 -> 2025-10-13: https://github.com/inventree/InvenTree/pull/10561
|
||||
- Allow search of assembly fields in BOM API endpoint
|
||||
|
||||
v407 -> 2025-10-09: https://github.com/inventree/InvenTree/pull/10538
|
||||
- Breaking: Set error status code for plugin action call instead of just returning error data
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ def cache_password():
|
||||
return cache_setting('password', None)
|
||||
|
||||
|
||||
def cache_user():
|
||||
"""Return the cash username."""
|
||||
return cache_setting('user', None)
|
||||
|
||||
|
||||
def is_global_cache_enabled() -> bool:
|
||||
"""Check if the global cache is enabled.
|
||||
|
||||
@@ -85,9 +90,10 @@ def get_cache_config(global_cache: bool) -> dict:
|
||||
if global_cache:
|
||||
# Build Redis URL with optional password
|
||||
password = cache_password()
|
||||
user = cache_user() or ''
|
||||
|
||||
if password:
|
||||
redis_url = f'redis://:{password}@{cache_host()}:{cache_port()}/0'
|
||||
redis_url = f'redis://{user}:{password}@{cache_host()}:{cache_port()}/0'
|
||||
else:
|
||||
redis_url = f'redis://{cache_host()}:{cache_port()}/0'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
@@ -80,11 +79,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!
|
||||
|
||||
@@ -670,6 +670,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."""
|
||||
@@ -720,6 +725,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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -298,7 +298,7 @@ def inventree_identifier(override_announce: bool = False):
|
||||
from common.settings import get_global_setting
|
||||
|
||||
if override_announce or get_global_setting(
|
||||
'INVENTREE_ANNOUNCE_ID', enviroment_key='INVENTREE_ANNOUNCE_ID'
|
||||
'INVENTREE_ANNOUNCE_ID', environment_key='INVENTREE_ANNOUNCE_ID'
|
||||
):
|
||||
return get_global_setting('INVENTREE_INSTANCE_ID', default='')
|
||||
return None
|
||||
|
||||
@@ -48,7 +48,7 @@ def currency_codes() -> list:
|
||||
from common.settings import get_global_setting
|
||||
|
||||
codes = get_global_setting(
|
||||
'CURRENCY_CODES', create=False, enviroment_key='INVENTREE_CURRENCY_CODES'
|
||||
'CURRENCY_CODES', create=False, environment_key='INVENTREE_CURRENCY_CODES'
|
||||
).strip()
|
||||
|
||||
if not codes:
|
||||
|
||||
@@ -26,12 +26,12 @@ def global_setting_overrides() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def get_global_setting(key, backup_value=None, enviroment_key=None, **kwargs):
|
||||
def get_global_setting(key, backup_value=None, environment_key=None, **kwargs):
|
||||
"""Return the value of a global setting using the provided key."""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
if enviroment_key:
|
||||
value = environ.get(enviroment_key)
|
||||
if environment_key:
|
||||
value = environ.get(environment_key)
|
||||
if value:
|
||||
return value
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1639,6 +1639,11 @@ class BomList(
|
||||
|
||||
search_fields = [
|
||||
'reference',
|
||||
'part__name',
|
||||
'part__description',
|
||||
'part__IPN',
|
||||
'part__revision',
|
||||
'part__keywords',
|
||||
'sub_part__name',
|
||||
'sub_part__description',
|
||||
'sub_part__IPN',
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -504,7 +504,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();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user