2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-25 10:27:39 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into make-fields-filterable

This commit is contained in:
Matthias Mair
2025-10-13 08:40:04 +02:00
21 changed files with 191 additions and 76 deletions

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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