Compare commits
72 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0dcb706f4c | |
|
|
f8bcc3ec17 | |
|
|
bc42450e0a | |
|
|
fa698e7e2b | |
|
|
c59fd55a00 | |
|
|
486e338b0b | |
|
|
eb32546824 | |
|
|
cc508a544c | |
|
|
c471a1cd38 | |
|
|
1249ae3a84 | |
|
|
4fe949811d | |
|
|
afdb4090bf | |
|
|
ae2cf931a5 | |
|
|
6fa54c0e0e | |
|
|
ff79ab87e5 | |
|
|
9fe290b01f | |
|
|
a63fe64aa2 | |
|
|
b6acbf0a48 | |
|
|
4a83b98cd9 | |
|
|
fe1cc56e94 | |
|
|
ef35591110 | |
|
|
df7817735d | |
|
|
c3ea3a5566 | |
|
|
be3e2b4e01 | |
|
|
3fc2db6c3a | |
|
|
79325c96d0 | |
|
|
62425f1f3b | |
|
|
200c1dfe06 | |
|
|
8c0b35ba69 | |
|
|
b1d0f3cb9e | |
|
|
555b72305c | |
|
|
9819442697 | |
|
|
f901eec771 | |
|
|
3044cee011 | |
|
|
17be1fbb43 | |
|
|
d2aa15257b | |
|
|
6ff28da48f | |
|
|
bd62363b9a | |
|
|
acc85672e1 | |
|
|
3e9fc94d63 | |
|
|
1d2700fa6f | |
|
|
9c170e6ed3 | |
|
|
542a75ce58 | |
|
|
06750358d6 | |
|
|
6ccc4544be | |
|
|
ac324cff14 | |
|
|
98b1678402 | |
|
|
da3ebacf6b | |
|
|
24b2401f6a | |
|
|
d12c335032 | |
|
|
eae580deaf | |
|
|
6207aa6c32 | |
|
|
5ff7ce4703 | |
|
|
0350623866 | |
|
|
bb576b16d8 | |
|
|
9705ea90c2 | |
|
|
39dc2b17fd | |
|
|
00646b0891 | |
|
|
2b7627a940 | |
|
|
048ece4797 | |
|
|
0feb542899 | |
|
|
4f9c42cbc2 | |
|
|
f8310c268c | |
|
|
91c134f752 | |
|
|
a57676faef | |
|
|
0de265d954 | |
|
|
f4fdad5d48 | |
|
|
73d6da8c13 | |
|
|
c108fd016d | |
|
|
2c95c3db29 | |
|
|
8571a42981 | |
|
|
a916e501c3 |
|
|
@ -10,6 +10,7 @@ tagged branch:
|
|||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
|
|
@ -23,7 +24,93 @@ REPO = os.getenv('GITHUB_REPOSITORY', 'inventree/inventree')
|
|||
GITHUB_API_URL = os.getenv('GITHUB_API_URL', 'https://api.github.com')
|
||||
|
||||
|
||||
def get_existing_release_tags(include_prerelease=True):
|
||||
def get_src_dir() -> Path:
|
||||
"""Return the path to the InvenTree source directory."""
|
||||
here = Path(__file__).parent.absolute()
|
||||
src_dir = here.joinpath('..', '..', 'src', 'backend', 'InvenTree', 'InvenTree')
|
||||
|
||||
if not src_dir.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Could not find InvenTree source directory: '{src_dir}'"
|
||||
)
|
||||
|
||||
return src_dir
|
||||
|
||||
|
||||
def get_inventree_version() -> str:
|
||||
"""Return the InvenTree version string."""
|
||||
src_dir = get_src_dir()
|
||||
version_file = src_dir.joinpath('version.py')
|
||||
|
||||
if not version_file.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Could not find InvenTree version file: '{version_file}'"
|
||||
)
|
||||
|
||||
with open(version_file, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
# Extract the InvenTree software version
|
||||
results = re.findall(r"""INVENTREE_SW_VERSION = '(.*)'""", text)
|
||||
|
||||
if len(results) != 1:
|
||||
raise ValueError(f'Could not find INVENTREE_SW_VERSION in {version_file}')
|
||||
|
||||
return results[0]
|
||||
|
||||
|
||||
def get_api_version() -> str:
|
||||
"""Return the InvenTree API version string."""
|
||||
src_dir = get_src_dir()
|
||||
api_version_file = src_dir.joinpath('api_version.py')
|
||||
|
||||
if not api_version_file.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Could not find InvenTree API version file: '{api_version_file}'"
|
||||
)
|
||||
|
||||
with open(api_version_file, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
# Extract the InvenTree software version
|
||||
results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text)
|
||||
|
||||
if len(results) != 1:
|
||||
raise ValueError(
|
||||
f'Could not find INVENTREE_API_VERSION in {api_version_file}'
|
||||
)
|
||||
|
||||
return results[0].strip().strip('"').strip("'")
|
||||
|
||||
|
||||
def version_number_to_tuple(version_string: str) -> tuple[int, int, int, str]:
|
||||
"""Validate a version number string, and convert to a tuple of integers.
|
||||
|
||||
e.g. 1.1.0
|
||||
e.g. 1.1.0 dev
|
||||
e.g. 1.2.3-rc2
|
||||
"""
|
||||
pattern = r'^(\d+)\.(\d+)\.(\d+)[\s-]?(.*)?$'
|
||||
|
||||
match = re.match(pattern, version_string)
|
||||
|
||||
if not match or len(match.groups()) < 3:
|
||||
raise ValueError(
|
||||
f"Version string '{version_string}' did not match required pattern"
|
||||
)
|
||||
|
||||
result = tuple(int(x) for x in match.groups()[:3])
|
||||
|
||||
# Add optional prerelease tag
|
||||
if len(match.groups()) > 3:
|
||||
result += (match.groups()[3] or '',)
|
||||
else:
|
||||
result += ('',)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_existing_release_tags(include_prerelease: bool = True):
|
||||
"""Request information on existing releases via the GitHub API."""
|
||||
# Check for github token
|
||||
token = os.getenv('GITHUB_TOKEN', None)
|
||||
|
|
@ -46,16 +133,16 @@ def get_existing_release_tags(include_prerelease=True):
|
|||
|
||||
for release in data:
|
||||
tag = release['tag_name'].strip()
|
||||
match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag)
|
||||
|
||||
if len(match.groups()) != 3:
|
||||
print(f"Version '{tag}' did not match expected pattern")
|
||||
continue
|
||||
version_tuple = version_number_to_tuple(tag)
|
||||
|
||||
if not include_prerelease and release['prerelease']:
|
||||
continue
|
||||
if len(version_tuple) >= 4 and version_tuple[3]:
|
||||
# Skip prerelease tags
|
||||
if not include_prerelease:
|
||||
print('-- skipping prerelease tag:', tag)
|
||||
continue
|
||||
|
||||
tags.append([int(x) for x in match.groups()])
|
||||
tags.append(tag)
|
||||
|
||||
return tags
|
||||
|
||||
|
|
@ -67,15 +154,7 @@ def check_version_number(version_string, allow_duplicate=False):
|
|||
"""
|
||||
print(f"Checking version '{version_string}'")
|
||||
|
||||
# Check that the version string matches the required format
|
||||
match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?: dev)?$', version_string)
|
||||
|
||||
if not match or len(match.groups()) != 3:
|
||||
raise ValueError(
|
||||
f"Version string '{version_string}' did not match required pattern"
|
||||
)
|
||||
|
||||
version_tuple = [int(x) for x in match.groups()]
|
||||
version_tuple = version_number_to_tuple(version_string)
|
||||
|
||||
# Look through the existing releases
|
||||
existing = get_existing_release_tags(include_prerelease=False)
|
||||
|
|
@ -83,35 +162,67 @@ def check_version_number(version_string, allow_duplicate=False):
|
|||
# Assume that this is the highest release, unless told otherwise
|
||||
highest_release = True
|
||||
|
||||
# A non-standard tag cannot be the 'highest' release
|
||||
if len(version_tuple) >= 4 and version_tuple[3]:
|
||||
highest_release = False
|
||||
print(f"-- Version tag '{version_string}' cannot be the highest release")
|
||||
|
||||
for release in existing:
|
||||
if release == version_tuple and not allow_duplicate:
|
||||
if version_string == release and not allow_duplicate:
|
||||
raise ValueError(f"Duplicate release '{version_string}' exists!")
|
||||
|
||||
if release > version_tuple:
|
||||
release_tuple = version_number_to_tuple(release)
|
||||
|
||||
if release_tuple > version_tuple:
|
||||
highest_release = False
|
||||
print(f'Found newer release: {release!s}')
|
||||
|
||||
if highest_release:
|
||||
print(f"-- Version '{version_string}' is the highest release")
|
||||
|
||||
return highest_release
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='InvenTree Version Check')
|
||||
parser.add_argument(
|
||||
'--show-version',
|
||||
action='store_true',
|
||||
help='Print the InvenTree version and exit',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--show-api-version',
|
||||
action='store_true',
|
||||
help='Print the InvenTree API version and exit',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--decrement-api',
|
||||
type=str,
|
||||
default='false',
|
||||
help='Decrement the API version by 1 and print',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
inventree_version = get_inventree_version()
|
||||
inventree_api_version = int(get_api_version())
|
||||
|
||||
if args.show_version:
|
||||
print(inventree_version)
|
||||
sys.exit(0)
|
||||
|
||||
if args.show_api_version:
|
||||
if str(args.decrement_api).strip().lower() == 'true':
|
||||
inventree_api_version -= 1
|
||||
print(inventree_api_version)
|
||||
sys.exit(0)
|
||||
|
||||
# Ensure that we are running in GH Actions
|
||||
if os.environ.get('GITHUB_ACTIONS', '') != 'true':
|
||||
print('This script is intended to be run within a GitHub Action!')
|
||||
sys.exit(1)
|
||||
|
||||
if 'only_version' in sys.argv:
|
||||
here = Path(__file__).parent.absolute()
|
||||
version_file = here.joinpath(
|
||||
'..', '..', 'src', 'backend', 'InvenTree', 'InvenTree', 'api_version.py'
|
||||
)
|
||||
text = version_file.read_text()
|
||||
results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text)
|
||||
# If 2. args is true lower the version number by 1
|
||||
if len(sys.argv) > 2 and sys.argv[2] == 'true':
|
||||
results[0] = str(int(results[0]) - 1)
|
||||
print(results[0])
|
||||
exit(0)
|
||||
print('Running InvenTree version check...')
|
||||
|
||||
# GITHUB_REF_TYPE may be either 'branch' or 'tag'
|
||||
GITHUB_REF_TYPE = os.environ['GITHUB_REF_TYPE']
|
||||
|
|
@ -127,26 +238,10 @@ if __name__ == '__main__':
|
|||
print(f'GITHUB_REF_TYPE: {GITHUB_REF_TYPE}')
|
||||
print(f'GITHUB_BASE_REF: {GITHUB_BASE_REF}')
|
||||
|
||||
here = Path(__file__).parent.absolute()
|
||||
version_file = here.joinpath(
|
||||
'..', '..', 'src', 'backend', 'InvenTree', 'InvenTree', 'version.py'
|
||||
print(
|
||||
f"InvenTree Version: '{inventree_version}' - {version_number_to_tuple(inventree_version)}"
|
||||
)
|
||||
|
||||
version = None
|
||||
|
||||
with open(version_file, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
# Extract the InvenTree software version
|
||||
results = re.findall(r"""INVENTREE_SW_VERSION = '(.*)'""", text)
|
||||
|
||||
if len(results) != 1:
|
||||
print(f'Could not find INVENTREE_SW_VERSION in {version_file}')
|
||||
sys.exit(1)
|
||||
|
||||
version = results[0]
|
||||
|
||||
print(f"InvenTree Version: '{version}'")
|
||||
print(f"InvenTree API Version: '{inventree_api_version}'")
|
||||
|
||||
# Check version number and look for existing versions
|
||||
# If a release is found which matches the current tag, throw an error
|
||||
|
|
@ -161,7 +256,9 @@ if __name__ == '__main__':
|
|||
if GITHUB_BASE_REF == 'stable':
|
||||
allow_duplicate = True
|
||||
|
||||
highest_release = check_version_number(version, allow_duplicate=allow_duplicate)
|
||||
highest_release = check_version_number(
|
||||
inventree_version, allow_duplicate=allow_duplicate
|
||||
)
|
||||
|
||||
# Determine which docker tag we are going to use
|
||||
docker_tags = None
|
||||
|
|
@ -171,8 +268,10 @@ if __name__ == '__main__':
|
|||
version_tag = GITHUB_REF.split('/')[-1]
|
||||
print(f"Checking requirements for tagged release - '{version_tag}':")
|
||||
|
||||
if version_tag != version:
|
||||
print(f"Version number '{version}' does not match tag '{version_tag}'")
|
||||
if version_tag != inventree_version:
|
||||
print(
|
||||
f"Version number '{inventree_version}' does not match tag '{version_tag}'"
|
||||
)
|
||||
sys.exit
|
||||
|
||||
docker_tags = [version_tag, 'stable'] if highest_release else [version_tag]
|
||||
|
|
@ -180,10 +279,11 @@ if __name__ == '__main__':
|
|||
elif GITHUB_REF_TYPE == 'branch':
|
||||
# Otherwise we know we are targeting the 'master' branch
|
||||
docker_tags = ['latest']
|
||||
highest_release = False
|
||||
|
||||
else:
|
||||
print('Unsupported branch / version combination:')
|
||||
print(f'InvenTree Version: {version}')
|
||||
print(f'InvenTree Version: {inventree_version}')
|
||||
print('GITHUB_REF_TYPE:', GITHUB_REF_TYPE)
|
||||
print('GITHUB_BASE_REF:', GITHUB_BASE_REF)
|
||||
print('GITHUB_REF:', GITHUB_REF)
|
||||
|
|
@ -193,7 +293,7 @@ if __name__ == '__main__':
|
|||
print('Docker tags could not be determined')
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Version check passed for '{version}'!")
|
||||
print(f"Version check passed for '{inventree_version}'!")
|
||||
print(f"Docker tags: '{docker_tags}'")
|
||||
|
||||
target_repos = [REPO.lower(), f'ghcr.io/{REPO.lower()}']
|
||||
|
|
|
|||
|
|
@ -164,8 +164,8 @@ jobs:
|
|||
API: ${{ needs.paths-filter.outputs.api }}
|
||||
run: |
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
|
||||
version="$(python3 .github/scripts/version_check.py only_version ${API} 2>&1)"
|
||||
echo "Version: $version"
|
||||
version="$(python3 .github/scripts/version_check.py --show-api-version --decrement-api=${API} 2>&1)"
|
||||
echo "API Version: $version"
|
||||
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
|
||||
echo "URL: $url"
|
||||
code=$(curl -s -o api.yaml $url --write-out '%{http_code}' --silent)
|
||||
|
|
@ -198,8 +198,8 @@ jobs:
|
|||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
|
||||
run: |
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
|
||||
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
|
||||
echo "Version: $version"
|
||||
version="$(python3 .github/scripts/version_check.py --show-api-version 2>&1)"
|
||||
echo "API Version: $version"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Extract settings / tags
|
||||
run: invoke int.export-definitions --basedir docs
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### 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)
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ INVENTREE_SITE_URL="http://inventree.localhost"
|
|||
#INVENTREE_SITE_URL="http://192.168.1.2" # You can specify a local IP address here
|
||||
#INVENTREE_SITE_URL="https://inventree.my-domain.com" # Or a public domain name (which you control)
|
||||
|
||||
# InvenTree proxy forwarding settings
|
||||
INVENTREE_USE_X_FORWARDED_HOST=True
|
||||
INVENTREE_USE_X_FORWARDED_PORT=True
|
||||
INVENTREE_USE_X_FORWARDED_PROTO=True
|
||||
|
||||
# Specify the location of the external data volume
|
||||
# By default, placed in local directory 'inventree-data'
|
||||
INVENTREE_EXT_VOLUME=./inventree-data
|
||||
|
|
@ -23,7 +28,8 @@ INVENTREE_LOG_LEVEL=WARNING
|
|||
# Enable custom plugins?
|
||||
INVENTREE_PLUGINS_ENABLED=True
|
||||
|
||||
# Run migrations automatically?
|
||||
# Run database migrations automatically?
|
||||
# Note: This does not negate the need to run "invoke update"
|
||||
INVENTREE_AUTO_UPDATE=True
|
||||
|
||||
# InvenTree superuser account details
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile contrib/container/requirements.in -o contrib/container/requirements.txt --python-version=3.11 --no-strip-extras --generate-hashes -b src/backend/requirements.txt
|
||||
asgiref==3.9.1 \
|
||||
--hash=sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142 \
|
||||
--hash=sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c
|
||||
asgiref==3.9.2 \
|
||||
--hash=sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960 \
|
||||
--hash=sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1
|
||||
# via django
|
||||
django==4.2.23 \
|
||||
--hash=sha256:42fdeaba6e6449d88d4f66de47871015097dc6f1b87910db00a91946295cfae4 \
|
||||
--hash=sha256:dafbfaf52c2f289bd65f4ab935791cb4fb9a198f2a5ba9faf35d7338a77e9803
|
||||
django==4.2.25 \
|
||||
--hash=sha256:2391ab3d78191caaae2c963c19fd70b99e9751008da22a0adcc667c5a4f8d311 \
|
||||
--hash=sha256:9584cf26b174b35620e53c2558b09d7eb180a655a3470474f513ff9acb494f8c
|
||||
# via
|
||||
# -r contrib/container/requirements.in
|
||||
# django-auth-ldap
|
||||
|
|
@ -51,76 +51,73 @@ packaging==25.0 \
|
|||
# via
|
||||
# gunicorn
|
||||
# mariadb
|
||||
psycopg[binary, pool]==3.2.9 \
|
||||
--hash=sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6 \
|
||||
--hash=sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700
|
||||
psycopg[binary, pool]==3.2.10 \
|
||||
--hash=sha256:0bce99269d16ed18401683a8569b2c5abd94f72f8364856d56c0389bcd50972a \
|
||||
--hash=sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3
|
||||
# via -r contrib/container/requirements.in
|
||||
psycopg-binary==3.2.9 \
|
||||
--hash=sha256:001e986656f7e06c273dd4104e27f4b4e0614092e544d950c7c938d822b1a894 \
|
||||
--hash=sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795 \
|
||||
--hash=sha256:093a0c079dd6228a7f3c3d82b906b41964eaa062a9a8c19f45ab4984bf4e872b \
|
||||
--hash=sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab \
|
||||
--hash=sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4 \
|
||||
--hash=sha256:166acc57af5d2ff0c0c342aed02e69a0cd5ff216cae8820c1059a6f3b7cf5f78 \
|
||||
--hash=sha256:18ac08475c9b971237fcc395b0a6ee4e8580bb5cf6247bc9b8461644bef5d9f4 \
|
||||
--hash=sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a \
|
||||
--hash=sha256:1ed2bab85b505d13e66a914d0f8cdfa9475c16d3491cf81394e0748b77729af2 \
|
||||
--hash=sha256:1f1736d5b21f69feefeef8a75e8d3bf1f0a1e17c165a7488c3111af9d6936e91 \
|
||||
--hash=sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb \
|
||||
--hash=sha256:24ddb03c1ccfe12d000d950c9aba93a7297993c4e3905d9f2c9795bb0764d523 \
|
||||
--hash=sha256:2504e9fd94eabe545d20cddcc2ff0da86ee55d76329e1ab92ecfcc6c0a8156c4 \
|
||||
--hash=sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351 \
|
||||
--hash=sha256:354dea21137a316b6868ee41c2ae7cce001e104760cf4eab3ec85627aed9b6cd \
|
||||
--hash=sha256:387c87b51d72442708e7a853e7e7642717e704d59571da2f3b29e748be58c78a \
|
||||
--hash=sha256:39a127e0cf9b55bd4734a8008adf3e01d1fd1cb36339c6a9e2b2cbb6007c50ee \
|
||||
--hash=sha256:3db3ba3c470801e94836ad78bf11fd5fab22e71b0c77343a1ee95d693879937a \
|
||||
--hash=sha256:413f9e46259fe26d99461af8e1a2b4795a4e27cc8ac6f7919ec19bcee8945074 \
|
||||
--hash=sha256:418f52b77b715b42e8ec43ee61ca74abc6765a20db11e8576e7f6586488a266f \
|
||||
--hash=sha256:4bfec4a73e8447d8fe8854886ffa78df2b1c279a7592241c2eb393d4499a17e2 \
|
||||
--hash=sha256:4c1ab25e3134774f1e476d4bb9050cdec25f10802e63e92153906ae934578734 \
|
||||
--hash=sha256:4df22ec17390ec5ccb38d211fb251d138d37a43344492858cea24de8efa15003 \
|
||||
--hash=sha256:528239bbf55728ba0eacbd20632342867590273a9bacedac7538ebff890f1093 \
|
||||
--hash=sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40 \
|
||||
--hash=sha256:587a3f19954d687a14e0c8202628844db692dbf00bba0e6d006659bf1ca91cbe \
|
||||
--hash=sha256:5918c0fab50df764812f3ca287f0d716c5c10bedde93d4da2cefc9d40d03f3aa \
|
||||
--hash=sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2 \
|
||||
--hash=sha256:5d2c9fe14fe42b3575a0b4e09b081713e83b762c8dc38a3771dd3265f8f110e7 \
|
||||
--hash=sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724 \
|
||||
--hash=sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0 \
|
||||
--hash=sha256:6afb3e62f2a3456f2180a4eef6b03177788df7ce938036ff7f09b696d418d186 \
|
||||
--hash=sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5 \
|
||||
--hash=sha256:72fdbda5b4c2a6a72320857ef503a6589f56d46821592d4377c8c8604810342b \
|
||||
--hash=sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6 \
|
||||
--hash=sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9 \
|
||||
--hash=sha256:791759138380df21d356ff991265fde7fe5997b0c924a502847a9f9141e68786 \
|
||||
--hash=sha256:799fa1179ab8a58d1557a95df28b492874c8f4135101b55133ec9c55fc9ae9d7 \
|
||||
--hash=sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21 \
|
||||
--hash=sha256:7b617b81f08ad8def5edd110de44fd6d326f969240cc940c6f6b3ef21fe9c59f \
|
||||
--hash=sha256:7e4660fad2807612bb200de7262c88773c3483e85d981324b3c647176e41fdc8 \
|
||||
--hash=sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87 \
|
||||
--hash=sha256:95315b8c8ddfa2fdcb7fe3ddea8a595c1364524f512160c604e3be368be9dd07 \
|
||||
--hash=sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff \
|
||||
--hash=sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e \
|
||||
--hash=sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f \
|
||||
--hash=sha256:a3e0f89fe35cb03ff1646ab663dabf496477bab2a072315192dbaa6928862891 \
|
||||
--hash=sha256:a4d76e28df27ce25dc19583407f5c6c6c2ba33b443329331ab29b6ef94c8736d \
|
||||
--hash=sha256:ac2c04b6345e215e65ca6aef5c05cc689a960b16674eaa1f90a8f86dfaee8c04 \
|
||||
--hash=sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d \
|
||||
--hash=sha256:b2d7a6646d41228e9049978be1f3f838b557a1bde500b919906d54c4390f5086 \
|
||||
--hash=sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748 \
|
||||
--hash=sha256:bb37ac3955d19e4996c3534abfa4f23181333974963826db9e0f00731274b695 \
|
||||
--hash=sha256:bc75f63653ce4ec764c8f8c8b0ad9423e23021e1c34a84eb5f4ecac8538a4a4a \
|
||||
--hash=sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e \
|
||||
--hash=sha256:cc19ed5c7afca3f6b298bfc35a6baa27adb2019670d15c32d0bb8f780f7d560d \
|
||||
--hash=sha256:cf789be42aea5752ee396d58de0538d5fcb76795c85fb03ab23620293fb81b6f \
|
||||
--hash=sha256:d9ac10a2ebe93a102a326415b330fff7512f01a9401406896e78a81d75d6eddc \
|
||||
--hash=sha256:e0f05b9dafa5670a7503abc715af081dbbb176a8e6770de77bccaeb9024206c5 \
|
||||
--hash=sha256:e4978c01ca4c208c9d6376bd585e2c0771986b76ff7ea518f6d2b51faece75e8 \
|
||||
--hash=sha256:eac3a6e926421e976c1c2653624e1294f162dc67ac55f9addbe8f7b8d08ce603 \
|
||||
--hash=sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b \
|
||||
--hash=sha256:f34e88940833d46108f949fdc1fcfb74d6b5ae076550cd67ab59ef47555dba95 \
|
||||
--hash=sha256:fa5c80d8b4cbf23f338db88a7251cef8bb4b68e0f91cf8b6ddfa93884fdbb0c1 \
|
||||
--hash=sha256:fb7599e436b586e265bea956751453ad32eb98be6a6e694252f4691c31b16edb
|
||||
psycopg-binary==3.2.10 \
|
||||
--hash=sha256:037dc92fc7d3f2adae7680e17216934c15b919d6528b908ac2eb52aecc0addcf \
|
||||
--hash=sha256:0738320a8d405f98743227ff70ed8fac9670870289435f4861dc640cef4a61d3 \
|
||||
--hash=sha256:0c23e88e048bbc33f32f5a35981707c9418723d469552dd5ac4e956366e58492 \
|
||||
--hash=sha256:0c2b95e83fda70ed2b0b4fadd8538572e4a4d987b721823981862d1ab56cc760 \
|
||||
--hash=sha256:14bcbcac0cab465d88b2581e43ec01af4b01c9833e663f1352e05cb41be19e44 \
|
||||
--hash=sha256:183a59cbdcd7e156669577fd73a9e917b1ee664e620f1e31ae138d24c7714693 \
|
||||
--hash=sha256:1b29285474e3339d0840e1b5079fdb0481914108f92ec62de0c87ae333c60b24 \
|
||||
--hash=sha256:1dee2f4d2adc9adacbfecf8254bd82f6ac95cff707e1b9b99aa721cd1ef16b47 \
|
||||
--hash=sha256:1f6982609b8ff8fcd67299b67cd5787da1876f3bb28fedd547262cfa8ddedf94 \
|
||||
--hash=sha256:2028073fc12cd70ba003309d1439c0c4afab4a7eee7653b8c91213064fffe12b \
|
||||
--hash=sha256:20384985fbc650c09a547a13c6d7f91bb42020d38ceafd2b68b7fc4a48a1f160 \
|
||||
--hash=sha256:299834cce3eec0c48aae5a5207fc8f0c558fd65f2ceab1a36693329847da956b \
|
||||
--hash=sha256:29b6bb87959515bc8b6abef10d8d23a9a681f03e48e9f0c8adb4b9fb7fa73f11 \
|
||||
--hash=sha256:3234605839e7d7584bd0a20716395eba34d368a5099dafe7896c943facac98fc \
|
||||
--hash=sha256:37b42b2f5f58df1f07a5df1b0c2bcc9bd3b9c105e2e988923bfa47aa4ae967da \
|
||||
--hash=sha256:3bb4046973264ebc8cb7e20a83882d68577c1f26a6f8ad4fe52e4468cd9a8eee \
|
||||
--hash=sha256:3e115930af2f38f4bbb5f1b61b598ceb802f091c1592c0fe0571c796b714b89a \
|
||||
--hash=sha256:42ee399c2613b470a87084ed79b06d9d277f19b0457c10e03a4aef7059097abc \
|
||||
--hash=sha256:43d803fb4e108a67c78ba58f3e6855437ca25d56504cae7ebbfbd8fce9b59247 \
|
||||
--hash=sha256:447afc326cbc95ed67c0cd27606c0f81fa933b830061e096dbd37e08501cb3de \
|
||||
--hash=sha256:470594d303928ab72a1ffd179c9c7bde9d00f76711d6b0c28f8a46ddf56d9807 \
|
||||
--hash=sha256:484d2b1659afe0f8f1cef5ea960bb640e96fa864faf917086f9f833f5c7a8034 \
|
||||
--hash=sha256:50130c0d1a2a01ec3d41631df86b6c1646c76718be000600a399dc1aad80b813 \
|
||||
--hash=sha256:5334a61a00ccb722f0b28789e265c7a273cfd10d5a1ed6bf062686fbb71e7032 \
|
||||
--hash=sha256:5369202e0e764193eac311b5a337d8cd58b1e23b822ddb7a559ed9f683d97623 \
|
||||
--hash=sha256:55b14f2402be027fe1568bc6c4d75ac34628ff5442a70f74137dadf99f738e3b \
|
||||
--hash=sha256:6220d6efd6e2df7b67d70ed60d653106cd3b70c5cb8cbe4e9f0a142a5db14015 \
|
||||
--hash=sha256:62590dd113d10cd9c08251cb80b32e2e8aaf01ece04a700322e776b1d216959f \
|
||||
--hash=sha256:646048f46192c8d23786cc6ef19f35b7488d4110396391e407eca695fdfe9dcd \
|
||||
--hash=sha256:6fe450a98a0788b721b1b8302f0ba9be6eca82faf74bf7a86d794cd6484c7e27 \
|
||||
--hash=sha256:70bb7f665587dfd79e69f48b34efe226149454d7aab138ed22d5431d703de2f6 \
|
||||
--hash=sha256:725843fd444075cc6c9989f5b25ca83ac68d8d70b58e1f476fbb4096975e43cc \
|
||||
--hash=sha256:764a5b9b40ad371c55dfdf95374d89e44a82fd62272d4fceebea0adb8930e2fb \
|
||||
--hash=sha256:7950ff79df7a453ac8a7d7a74694055b6c15905b0a2b6e3c99eb59c51a3f9bf7 \
|
||||
--hash=sha256:7fa1626225a162924d2da0ff4ef77869f7a8501d320355d2732be5bf2dda6138 \
|
||||
--hash=sha256:810f65b9ef1fe9dddb5c05937884ea9563aaf4e1a2c3d138205231ed5f439511 \
|
||||
--hash=sha256:8390db6d2010ffcaf7f2b42339a2da620a7125d37029c1f9b72dfb04a8e7be6f \
|
||||
--hash=sha256:84f7e8c5e5031db342ae697c2e8fb48cd708ba56990573b33e53ce626445371d \
|
||||
--hash=sha256:8923487c3898c65e1450847e15d734bb2e6adbd2e79d2d1dd5ad829a1306bdc0 \
|
||||
--hash=sha256:89440355d1b163b11dc661ae64a5667578aab1b80bbf71ced90693d88e9863e1 \
|
||||
--hash=sha256:8b45e65383da9c4a42a56f817973e521e893f4faae897fe9f1a971f9fe799742 \
|
||||
--hash=sha256:8f4ae059c6c9e491cdc3f39f9fc4f09373ef281c6cc381499269dcff21abafc9 \
|
||||
--hash=sha256:8fa2efaf5e2f8c289a185c91c80a624a8f97aa17fbedcbc68f373d089b332afd \
|
||||
--hash=sha256:901729188b3fd5625970650ca1167786847dee0b92930c2858724d1a5e25dee1 \
|
||||
--hash=sha256:9c9f2728488ac5848acdbf14bb4fde50f8ba783cbf3c19e9abd506741389fa7f \
|
||||
--hash=sha256:a024b3ee539a475cbc59df877c8ecdd6f8552a1b522b69196935bc26dc6152fb \
|
||||
--hash=sha256:a1d4e4d309049e3cb61269652a3ca56cb598da30ecd7eb8cea561e0d18bc1a43 \
|
||||
--hash=sha256:a28f24a7b68456bd31209b027a5b04304d37eb1d622ef847bf8c47933218a738 \
|
||||
--hash=sha256:a5a81104d88780018005fe17c37fa55b4afbb6dd3c205963cc56c025d5f1cc32 \
|
||||
--hash=sha256:a92ff1c2cd79b3966d6a87e26ceb222ecd5581b5ae4b58961f126af806a861ed \
|
||||
--hash=sha256:ab1c6d761c4ee581016823dcc02f29b16ad69177fcbba88a9074c924fc31813e \
|
||||
--hash=sha256:ac0365398947879c9827b319217096be727da16c94422e0eb3cf98c930643162 \
|
||||
--hash=sha256:b34c278a58aa79562afe7f45e0455b1f4cad5974fc3d5674cc5f1f9f57e97fc5 \
|
||||
--hash=sha256:bd3676a04970cf825d2c771b0c147f91182c5a3653e0dbe958e12383668d0f79 \
|
||||
--hash=sha256:bf30dcf6aaaa8d4779a20d2158bdf81cc8e84ce8eee595d748a7671c70c7b890 \
|
||||
--hash=sha256:d2fe9eaa367f6171ab1a21a7dcb335eb2398be7f8bb7e04a20e2260aedc6f782 \
|
||||
--hash=sha256:d557a94cd6d2e775b3af6cc0bd0ff0d9d641820b5cc3060ccf1f5ca2bf971217 \
|
||||
--hash=sha256:d5c6a66a76022af41970bf19f51bc6bf87bd10165783dd1d40484bfd87d6b382 \
|
||||
--hash=sha256:d7d05174276bb403b8a57e01b857d96b0ac2a6879c5ce06a5cac2d1115763081 \
|
||||
--hash=sha256:d922fdd49ed17c558b6b2f9ae2054c3d0cced2a34e079ce5a41c86904d0203f7 \
|
||||
--hash=sha256:db0eb06a19e4c64a08db0db80875ede44939af6a2afc281762c338fad5d6e547 \
|
||||
--hash=sha256:e037aac8dc894d147ef33056fc826ee5072977107a3fdf06122224353a057598
|
||||
# via psycopg
|
||||
psycopg-pool==3.2.6 \
|
||||
--hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \
|
||||
|
|
@ -141,60 +138,80 @@ python-ldap==3.4.4 \
|
|||
# via
|
||||
# -r contrib/container/requirements.in
|
||||
# django-auth-ldap
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via -r contrib/container/requirements.in
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
|
|
@ -204,32 +221,32 @@ sqlparse==0.5.3 \
|
|||
--hash=sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272 \
|
||||
--hash=sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca
|
||||
# via django
|
||||
typing-extensions==4.14.1 \
|
||||
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
|
||||
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
|
||||
typing-extensions==4.15.0 \
|
||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via
|
||||
# psycopg
|
||||
# psycopg-pool
|
||||
uv==0.8.6 \
|
||||
--hash=sha256:0b524de39f317bd8733c38cf100b6f8091d44e06b23f7752523ad1ad1454ede3 \
|
||||
--hash=sha256:132e73f1e9fe05edc6c06c00416f7c721c48298786fd7293be6c584793170bbc \
|
||||
--hash=sha256:1913f5627c57076c88dd38b0173bdb006ae9b8dbd92b1798a1acc9d744c1a7cc \
|
||||
--hash=sha256:2ac28509db2e52613a59264bdb150d13274ed13e5b305f7e274da8cd83033985 \
|
||||
--hash=sha256:37227aaf1e41c7eda3d7f0028e747a2a2eed3f3506b0adc121a4366e8281115b \
|
||||
--hash=sha256:4d4e042f6bd9f143094051a05de758684028f451e563846cbc0c6f505b530cca \
|
||||
--hash=sha256:4e81380549151e34ae96d56499438444ba58591ca9f2fc6ba0a867152601849e \
|
||||
--hash=sha256:57a98367bfad38e870e1a8a6626464796ffcee6e937d429fbd7b25ddf46bb36f \
|
||||
--hash=sha256:5b201ebc1c5c76c3a415fa4edcb25a0e06263d2255319d6d52275c775e926e23 \
|
||||
--hash=sha256:6acdc77099906ba64bc1b725bef973c10905d7e9596d1b25f271db772bc9e8e4 \
|
||||
--hash=sha256:7796acc3c5b84d5ee5e10cc6cf92eb61c19f6551855d0aa89ef5925e4a371fbf \
|
||||
--hash=sha256:7c1f48279ff61940143c78b969094e13324988eabcfcd4799f4350d9d36c1d48 \
|
||||
--hash=sha256:993af2c295856c5ca053678a8dadc11ce2f85485513ed1568c16e98d5dfa88bf \
|
||||
--hash=sha256:c9de4adac36a62e4bddd959ce65fb4bb09b0cbfd95946d50390f2a9c186ecb9c \
|
||||
--hash=sha256:d96ff3a1d06a6a00ed94dfb2996228153b3b5bfc892174b7556216ab872a91b1 \
|
||||
--hash=sha256:deab2ce32d2dd7a1c0de459aa23470c60feb0ea24e67c9c5c5988d8bf4eb4a09 \
|
||||
--hash=sha256:e35cc1ef79d3dce2b6aeffbfb280d02d5ad741d4ca07874bdf0a4d85c841d9de \
|
||||
--hash=sha256:ee67acf1b211be2cfbeaec16cde13c8325810d32ff85963a9dedd1f9d7c61ef7 \
|
||||
--hash=sha256:fdceb1ef554df0ddc620bfe83fdcf740829e489c62f78ba1f089abd62c71c63e
|
||||
uv==0.8.22 \
|
||||
--hash=sha256:2a436b941b6e79fe1e1065b705a5689d72210f4367cbe885e19910cbcde2e4a1 \
|
||||
--hash=sha256:36c7aecdb0044caf15ace00da00af172759c49c832f0017b7433d80f46552cd3 \
|
||||
--hash=sha256:4a982bdd5d239dd6dd2b4219165e209c75af1e1819730454ee46d65b3ccf77a3 \
|
||||
--hash=sha256:58b6fb191a04b922dc3c8fea6660f58545a651843d7d0efa9ae69164fca9e05d \
|
||||
--hash=sha256:6706b782ad75662df794e186d16b9ffa4946d57c88f21d0eadfd43425794d1b0 \
|
||||
--hash=sha256:7350c5f82d9c38944e6466933edcf96a90e0cb85eae5c0e53a5bc716d6f62332 \
|
||||
--hash=sha256:7378127cbd6ebce8ba6d9bdb88aa8ea995b579824abb5ec381c63b3a123a43be \
|
||||
--hash=sha256:7e761ca7df8a0059b3fae6bc2c1db24583fa00b016e35bd22a5599d7084471a7 \
|
||||
--hash=sha256:89944e99b04cc8542cb5931306f1c593f00c9d6f2b652fffc4d84d12b915f911 \
|
||||
--hash=sha256:8ea724ae9f15c0cb4964e9e2e1b21df65c56ae02a54dc1d8a6ea44a52d819268 \
|
||||
--hash=sha256:8efec4ef5acddc35f0867998c44e0b15fc4dace1e4c26d01443871a2fbb04bf6 \
|
||||
--hash=sha256:9757f0b0c7d296f1e354db442ed0ce39721c06d11635ce4ee6638c5e809a9cb4 \
|
||||
--hash=sha256:9eb3b4abfa25e07d7e1bb4c9bb8dbbdd51878356a37c3c4a2ece3d68d4286f28 \
|
||||
--hash=sha256:aefa0cb27a86d2145ca9290a1e99c16a17ea26a4f14a89fb7336bc19388427cc \
|
||||
--hash=sha256:b1fdffc2e71892ce648b66317e478fe8884d0007e20cfa582fff3dcea588a450 \
|
||||
--hash=sha256:cda349c9ea53644d8d9ceae30db71616b733eb5330375ab4259765aef494b74e \
|
||||
--hash=sha256:d6a33bd5309f8fb77d9fc249bb17f77a23426e6153e43b03ca1cd6640f0a423d \
|
||||
--hash=sha256:e6e1289c411d43e0ca245f46e76457f3807de646d90b656591b6cf46348bed5c \
|
||||
--hash=sha256:f6ded9bacb31441d788afca397b8b884ebc2e70f903bea0a38806194be4b249c
|
||||
# via -r contrib/container/requirements.in
|
||||
wheel==0.45.1 \
|
||||
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
|
||||
|
|
|
|||
|
|
@ -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 "####################################################################################"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ Date and time values are stored in the database in UTC format, and are converted
|
|||
|
||||
By default, the InvenTree server will not automatically apply database migrations. When the InvenTree installation is updated (*or a plugin is installed which requires database migrations*), database migrations must be applied manually by the system administrator.
|
||||
|
||||
With "auto update" enabled, the InvenTree server will automatically apply database migrations as required. To enable automatic database updates, set `INVENTREE_AUTO_UPDATE` to `True`.
|
||||
With "auto update" enabled, the InvenTree server will automatically apply database migrations as required when plugins are changed. To enable automatic database updates, set `INVENTREE_AUTO_UPDATE` to `True`. However, this setting is not sufficient when updating your InvenTree installation - you must still ensure that you follow the required steps for updating InvenTree as per your installation method.
|
||||
|
||||
## Debugging and Logging Options
|
||||
|
||||
|
|
@ -135,6 +135,7 @@ Depending on how your InvenTree installation is configured, you will need to pay
|
|||
| INVENTREE_CORS_ORIGIN_WHITELIST | cors.whitelist | List of whitelisted CORS URLs. Refer to the [django-cors-headers documentation](https://github.com/adamchainz/django-cors-headers#cors_allowed_origins-sequencestr) | Uses the *INVENTREE_SITE_URL* parameter, if set. Otherwise, an empty list. |
|
||||
| INVENTREE_CORS_ORIGIN_REGEX | cors.regex | List of regular expressions for CORS whitelisted URL patterns | *Empty list* |
|
||||
| INVENTREE_CORS_ALLOW_CREDENTIALS | cors.allow_credentials | Allow cookies in cross-site requests | `True` |
|
||||
| INVENTREE_SITE_LAX_PROTOCOL | site_lax_protocol | Ignore protocol mismatches on INVE-E7 site checks | `True` |
|
||||
| INVENTREE_USE_X_FORWARDED_HOST | use_x_forwarded_host | Use forwarded host header | `False` |
|
||||
| INVENTREE_USE_X_FORWARDED_PORT | use_x_forwarded_port | Use forwarded port header | `False` |
|
||||
| INVENTREE_USE_X_FORWARDED_PROTO | use_x_forwarded_proto | Use forwarded protocol header | `False` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -305,8 +305,8 @@ class InfoView(APIView):
|
|||
'login_message': helpers.getCustomOption('login_message'),
|
||||
'navbar_message': helpers.getCustomOption('navbar_message'),
|
||||
},
|
||||
'active_plugins': plugins_info(),
|
||||
# Following fields are only available to staff users
|
||||
'active_plugins': plugins_info() if is_staff else None,
|
||||
'system_health': check_system_health() if is_staff else None,
|
||||
'database': InvenTree.version.inventreeDatabase() if is_staff else None,
|
||||
'platform': InvenTree.version.inventreePlatform() if is_staff else None,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 390
|
||||
INVENTREE_API_VERSION = 391
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v391 -> 2025-09-06 : https://github.com/inventree/InvenTree/pull/10279
|
||||
- Refactors 'exclude_tree', 'cascade', and 'location' filters in StockList API endpoint
|
||||
|
||||
v390 -> 2025-09-03 : https://github.com/inventree/InvenTree/pull/10257
|
||||
- Fixes limitation on adding virtual parts to a SalesOrder
|
||||
- Additional query filter options for BomItem API endpoint
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -234,27 +235,75 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin):
|
|||
if path in urls or any(path.startswith(p) for p in paths_ignore):
|
||||
return None
|
||||
|
||||
# Ensure that the settings are set correctly with the current request
|
||||
# treat the accessed scheme and host
|
||||
accessed_scheme = request._current_scheme_host
|
||||
if accessed_scheme and not accessed_scheme.startswith(settings.SITE_URL):
|
||||
msg = f'INVE-E7: The used path `{accessed_scheme}` does not match the SITE_URL `{settings.SITE_URL}`'
|
||||
logger.error(msg)
|
||||
return render(
|
||||
request, 'config_error.html', {'error_message': msg}, status=500
|
||||
referer = urlsplit(accessed_scheme)
|
||||
|
||||
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 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
|
||||
):
|
||||
# The used url might not be the primary url - next check determines if in a trusted origins
|
||||
pass
|
||||
else:
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
referer = urlsplit(accessed_scheme)
|
||||
if not any(
|
||||
is_same_domain(referer.netloc, host)
|
||||
for host in [
|
||||
urlsplit(origin).netloc.lstrip('*')
|
||||
for origin in settings.CSRF_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!
|
||||
|
|
@ -1052,6 +1047,9 @@ DATE_INPUT_FORMATS = ['%Y-%m-%d']
|
|||
|
||||
# Site URL can be specified statically, or via a run-time setting
|
||||
SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
|
||||
SITE_LAX_PROTOCOL_CHECK = get_boolean_setting(
|
||||
'INVENTREE_SITE_LAX_PROTOCOL', 'site_lax_protocol', True
|
||||
)
|
||||
|
||||
if SITE_URL:
|
||||
SITE_URL = str(SITE_URL).strip().rstrip('/')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -605,9 +605,7 @@ class GeneralApiTests(InvenTreeAPITestCase):
|
|||
response = self.get(url, max_query_count=20)
|
||||
data = response.json()
|
||||
self.assertEqual(data['database'], None)
|
||||
|
||||
# No active plugin info for anon user
|
||||
self.assertIsNone(data.get('active_plugins'))
|
||||
self.assertIsNotNone(data.get('active_plugins'))
|
||||
|
||||
# Staff
|
||||
response = self.get(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for middleware functions."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.urls import reverse
|
||||
|
||||
|
|
@ -87,36 +88,117 @@ class MiddlewareTests(InvenTreeTestCase):
|
|||
log_error('testpath')
|
||||
check(1)
|
||||
|
||||
def do_positive_test(self, response):
|
||||
"""Helper function to check for positive test results."""
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'INVE-E7')
|
||||
self.assertContains(response, 'window.INVENTREE_SETTINGS')
|
||||
|
||||
def test_site_url_checks(self):
|
||||
"""Test that the site URL check is correctly working."""
|
||||
# correctly set
|
||||
# simple setup
|
||||
with self.settings(
|
||||
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
|
||||
):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'INVE-E7')
|
||||
self.assertContains(response, 'window.INVENTREE_SETTINGS')
|
||||
self.do_positive_test(response)
|
||||
|
||||
# wrongly set site URL
|
||||
with self.settings(SITE_URL='https://example.com'):
|
||||
# simple setup with wildcard
|
||||
with self.settings(
|
||||
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://*.testserver']
|
||||
):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertContains(
|
||||
response,
|
||||
'INVE-E7: The used path `http://testserver` does not match',
|
||||
status_code=500,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response, 'window.INVENTREE_SETTINGS', status_code=500
|
||||
)
|
||||
self.do_positive_test(response)
|
||||
|
||||
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']
|
||||
):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.do_positive_test(response)
|
||||
|
||||
# No worky if strong security
|
||||
with self.settings(
|
||||
SITE_URL='https://testserver',
|
||||
CSRF_TRUSTED_ORIGINS=['https://testserver'],
|
||||
SITE_LAX_PROTOCOL_CHECK=False,
|
||||
):
|
||||
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
|
||||
with self.settings(
|
||||
SITE_URL='https://testserver.example.com',
|
||||
CSRF_TRUSTED_ORIGINS=[
|
||||
'http://testserver',
|
||||
'https://testserver.example.com',
|
||||
],
|
||||
):
|
||||
# this will run with testserver as host by default
|
||||
response = self.client.get(reverse('web'))
|
||||
self.do_positive_test(response)
|
||||
|
||||
# Now test with the "outside" url name
|
||||
response = self.client.get(
|
||||
'https://testserver.example.com/web/',
|
||||
SERVER_NAME='testserver.example.com',
|
||||
)
|
||||
self.do_positive_test(response)
|
||||
|
||||
# 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',
|
||||
)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
# Even if it is a subdomain
|
||||
response = self.client.get(
|
||||
'https://not-my.testserver.example.com/web/',
|
||||
SERVER_NAME='not-my.testserver.example.com',
|
||||
)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
def test_site_url_checks_fails(self):
|
||||
"""Test that the site URL check is correctly failing.
|
||||
|
||||
Important for security.
|
||||
"""
|
||||
# wrongly set but in debug -> is ignored
|
||||
with self.settings(SITE_URL='https://example.com', DEBUG=True):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'INVE-E7')
|
||||
self.assertContains(response, 'window.INVENTREE_SETTINGS')
|
||||
self.do_positive_test(response)
|
||||
|
||||
# wrongly set cors
|
||||
with self.settings(
|
||||
|
|
@ -125,7 +207,6 @@ class MiddlewareTests(InvenTreeTestCase):
|
|||
CSRF_TRUSTED_ORIGINS=['https://example.com'],
|
||||
):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertContains(
|
||||
response, 'is not in the TRUSTED_ORIGINS', status_code=500
|
||||
)
|
||||
|
|
@ -133,10 +214,33 @@ class MiddlewareTests(InvenTreeTestCase):
|
|||
response, 'window.INVENTREE_SETTINGS', status_code=500
|
||||
)
|
||||
|
||||
# wrongly set site URL
|
||||
with self.settings(
|
||||
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://*.testserver']
|
||||
SITE_URL='https://example.com',
|
||||
CSRF_TRUSTED_ORIGINS=['http://localhost:8000'],
|
||||
):
|
||||
response = self.client.get(reverse('web'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, 'INVE-E7')
|
||||
self.assertContains(response, 'window.INVENTREE_SETTINGS')
|
||||
self.assertContains(
|
||||
response,
|
||||
'INVE-E7: The visited path `http://testserver` ',
|
||||
status_code=500,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response, 'window.INVENTREE_SETTINGS', status_code=500
|
||||
)
|
||||
|
||||
# Log stuff # TODO remove
|
||||
print(
|
||||
'###DBG-TST###',
|
||||
'site',
|
||||
settings.SITE_URL,
|
||||
'trusted',
|
||||
settings.CSRF_TRUSTED_ORIGINS,
|
||||
)
|
||||
|
||||
# Check that the correct step triggers the error message
|
||||
self.assertContains(
|
||||
response,
|
||||
'INVE-E7: The visited path `http://testserver` does not match',
|
||||
status_code=500,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from django.conf import settings
|
|||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = '0.18.0 dev'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -480,8 +480,8 @@ class BuildLineFilter(rest_filters.FilterSet):
|
|||
def filter_allocated(self, queryset, name, value):
|
||||
"""Filter by whether each BuildLine is fully allocated."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(allocated__gte=F('quantity'))
|
||||
return queryset.filter(allocated__lt=F('quantity'))
|
||||
return queryset.filter(allocated__gte=F('quantity') - F('consumed'))
|
||||
return queryset.filter(allocated__lt=F('quantity') - F('consumed'))
|
||||
|
||||
consumed = rest_filters.BooleanFilter(label=_('Consumed'), method='filter_consumed')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
"""Queryset filtering helper functions for the Build app."""
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import DecimalField, ExpressionWrapper, F, Max, Sum
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
|
||||
|
||||
def annotate_allocated_quantity(queryset: Q) -> Q:
|
||||
"""Annotate the 'allocated' quantity for each build item in the queryset.
|
||||
|
||||
Arguments:
|
||||
queryset: The BuildLine queryset to annotate
|
||||
|
||||
"""
|
||||
queryset = queryset.prefetch_related('allocations')
|
||||
|
||||
return queryset.annotate(
|
||||
allocated=Coalesce(
|
||||
Sum('allocations__quantity'), 0, output_field=models.DecimalField()
|
||||
)
|
||||
def annotate_required_quantity():
|
||||
"""Annotate the 'required' quantity for each build item in the queryset."""
|
||||
# Note: The use of Max() here is intentional, to avoid aggregation issues in MySQL
|
||||
# Ref: https://github.com/inventree/InvenTree/pull/10398
|
||||
return Greatest(
|
||||
ExpressionWrapper(
|
||||
Max(F('quantity')) - Max(F('consumed')), output_field=DecimalField()
|
||||
),
|
||||
0,
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
|
||||
|
||||
def annotate_allocated_quantity():
|
||||
"""Annotate the 'allocated' quantity for each build item in the queryset."""
|
||||
return Coalesce(Sum('allocations__quantity'), 0, output_field=DecimalField())
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import report.mixins
|
|||
import stock.models
|
||||
import users.models
|
||||
from build.events import BuildEvents
|
||||
from build.filters import annotate_allocated_quantity
|
||||
from build.filters import annotate_allocated_quantity, annotate_required_quantity
|
||||
from build.status_codes import BuildStatus, BuildStatusGroups
|
||||
from build.validators import (
|
||||
generate_next_build_reference,
|
||||
|
|
@ -1062,7 +1062,7 @@ class Build(
|
|||
|
||||
lines = self.untracked_line_items.all()
|
||||
lines = lines.exclude(bom_item__consumable=True)
|
||||
lines = annotate_allocated_quantity(lines)
|
||||
lines = lines.annotate(allocated=annotate_allocated_quantity())
|
||||
|
||||
for build_line in lines:
|
||||
reduce_by = build_line.allocated - build_line.quantity
|
||||
|
|
@ -1381,10 +1381,12 @@ class Build(
|
|||
elif tracked is False:
|
||||
lines = lines.filter(bom_item__sub_part__trackable=False)
|
||||
|
||||
lines = annotate_allocated_quantity(lines)
|
||||
lines = lines.prefetch_related('allocations')
|
||||
|
||||
# Filter out any lines which have been fully allocated
|
||||
lines = lines.filter(allocated__lt=F('quantity'))
|
||||
lines = lines.annotate(
|
||||
allocated=annotate_allocated_quantity(),
|
||||
required=annotate_required_quantity(),
|
||||
).filter(allocated__lt=F('required'))
|
||||
|
||||
return lines
|
||||
|
||||
|
|
@ -1436,10 +1438,14 @@ class Build(
|
|||
True if any BuildLine has been over-allocated.
|
||||
"""
|
||||
lines = self.build_lines.all().exclude(bom_item__consumable=True)
|
||||
lines = annotate_allocated_quantity(lines)
|
||||
|
||||
lines = lines.prefetch_related('allocations')
|
||||
|
||||
# Find any lines which have been over-allocated
|
||||
lines = lines.filter(allocated__gt=F('quantity'))
|
||||
lines = lines.annotate(
|
||||
allocated=annotate_allocated_quantity(),
|
||||
required=annotate_required_quantity(),
|
||||
).filter(allocated__gt=F('required'))
|
||||
|
||||
return lines.count() > 0
|
||||
|
||||
|
|
@ -1644,19 +1650,30 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
|
|||
return allocated['q']
|
||||
|
||||
def unallocated_quantity(self):
|
||||
"""Return the unallocated quantity for this BuildLine."""
|
||||
return max(self.quantity - self.allocated_quantity(), 0)
|
||||
"""Return the unallocated quantity for this BuildLine.
|
||||
|
||||
- Start with the required quantity
|
||||
- Subtract the consumed quantity
|
||||
- Subtract the allocated quantity
|
||||
|
||||
Return the remaining quantity (or zero if negative)
|
||||
"""
|
||||
return max(self.quantity - self.consumed - self.allocated_quantity(), 0)
|
||||
|
||||
def is_fully_allocated(self):
|
||||
"""Return True if this BuildLine is fully allocated."""
|
||||
if self.bom_item.consumable:
|
||||
return True
|
||||
|
||||
return self.allocated_quantity() >= self.quantity
|
||||
required = max(0, self.quantity - self.consumed)
|
||||
|
||||
return self.allocated_quantity() >= required
|
||||
|
||||
def is_overallocated(self):
|
||||
"""Return True if this BuildLine is over-allocated."""
|
||||
return self.allocated_quantity() > self.quantity
|
||||
required = max(0, self.quantity - self.consumed)
|
||||
|
||||
return self.allocated_quantity() > required
|
||||
|
||||
def is_fully_consumed(self):
|
||||
"""Return True if this BuildLine is fully consumed."""
|
||||
|
|
|
|||
|
|
@ -853,6 +853,78 @@ class AutoAllocationTests(BuildTestBase):
|
|||
self.assertEqual(self.line_1.unallocated_quantity(), 0)
|
||||
self.assertEqual(self.line_2.unallocated_quantity(), 0)
|
||||
|
||||
def test_allocate_consumed(self):
|
||||
"""Test for auto-allocation against a build which has been fully consumed.
|
||||
|
||||
Steps:
|
||||
1. Fully allocate the build (using the auto-allocate function)
|
||||
2. Consume allocated stock
|
||||
3. Ensure that all allocations are removed
|
||||
4. Re-run the auto-allocate function
|
||||
5. Check that no new allocations have been made
|
||||
"""
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
self.assertFalse(self.build.is_fully_allocated(tracked=False))
|
||||
|
||||
# Auto allocate stock against the build order
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True, substitutes=True, optional_items=True
|
||||
)
|
||||
|
||||
self.assertEqual(self.line_1.allocated_quantity(), 50)
|
||||
self.assertEqual(self.line_2.allocated_quantity(), 30)
|
||||
|
||||
self.assertEqual(self.line_1.unallocated_quantity(), 0)
|
||||
self.assertEqual(self.line_2.unallocated_quantity(), 0)
|
||||
|
||||
self.assertTrue(self.line_1.is_fully_allocated())
|
||||
self.assertTrue(self.line_2.is_fully_allocated())
|
||||
|
||||
self.assertFalse(self.line_1.is_overallocated())
|
||||
self.assertFalse(self.line_2.is_overallocated())
|
||||
|
||||
N = self.build.allocated_stock.count()
|
||||
|
||||
self.assertEqual(self.line_1.allocations.count(), 2)
|
||||
self.assertEqual(self.line_2.allocations.count(), 6)
|
||||
|
||||
for item in self.line_1.allocations.all():
|
||||
item.complete_allocation()
|
||||
|
||||
for item in self.line_2.allocations.all():
|
||||
item.complete_allocation()
|
||||
|
||||
self.line_1.refresh_from_db()
|
||||
self.line_2.refresh_from_db()
|
||||
|
||||
self.assertTrue(self.line_1.is_fully_allocated())
|
||||
self.assertTrue(self.line_2.is_fully_allocated())
|
||||
self.assertFalse(self.line_1.is_overallocated())
|
||||
self.assertFalse(self.line_2.is_overallocated())
|
||||
|
||||
self.assertEqual(self.line_1.allocations.count(), 0)
|
||||
self.assertEqual(self.line_2.allocations.count(), 0)
|
||||
|
||||
self.assertEqual(self.line_1.quantity, self.line_1.consumed)
|
||||
self.assertEqual(self.line_2.quantity, self.line_2.consumed)
|
||||
|
||||
# Check that the "allocations" have been removed
|
||||
self.assertEqual(self.build.allocated_stock.count(), N - 8)
|
||||
|
||||
# Now, try to auto-allocate again
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True, substitutes=True, optional_items=True
|
||||
)
|
||||
|
||||
# Ensure that there are no "new" allocations (there should be none!)
|
||||
self.assertEqual(self.line_1.allocated_quantity(), 0)
|
||||
self.assertEqual(self.line_2.allocated_quantity(), 0)
|
||||
|
||||
self.assertEqual(self.line_1.unallocated_quantity(), 0)
|
||||
self.assertEqual(self.line_2.unallocated_quantity(), 0)
|
||||
|
||||
self.assertEqual(self.build.allocated_stock.count(), N - 8)
|
||||
|
||||
|
||||
class ExternalBuildTest(InvenTreeAPITestCase):
|
||||
"""Unit tests for external build order functionality."""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import structlog
|
|||
from moneyed import CURRENCIES
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.ready
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import InvenTree.validators
|
||||
import common.currency
|
||||
import common.settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -630,6 +630,38 @@ class DataImportRow(models.Model):
|
|||
if value is None and field in default_values:
|
||||
value = default_values[field]
|
||||
|
||||
# If the field provides a set of valid 'choices', use that as a lookup
|
||||
if field_type == 'choice' and 'choices' in field_def:
|
||||
choices = field_def.get('choices', None)
|
||||
|
||||
if callable(choices):
|
||||
choices = choices()
|
||||
|
||||
# Try to match the provided value against the available choices
|
||||
choice_value = None
|
||||
|
||||
for choice in choices:
|
||||
primary_value = choice['value']
|
||||
display_value = choice['display_name']
|
||||
|
||||
if primary_value == value:
|
||||
choice_value = primary_value
|
||||
# Break on first match against a primary choice value
|
||||
break
|
||||
|
||||
if display_value == value:
|
||||
choice_value = primary_value
|
||||
|
||||
elif (
|
||||
str(display_value).lower().strip() == str(value).lower().strip()
|
||||
and choice_value is None
|
||||
):
|
||||
# Case-insensitive match against display value
|
||||
choice_value = primary_value
|
||||
|
||||
if choice_value is not None:
|
||||
value = choice_value
|
||||
|
||||
data[field] = value
|
||||
|
||||
self.data = data
|
||||
|
|
@ -693,9 +725,20 @@ class DataImportRow(models.Model):
|
|||
try:
|
||||
instance = self.session.model_class.objects.get(pk=instance_id)
|
||||
except self.session.model_class.DoesNotExist:
|
||||
raise DjangoValidationError(_('No record found with the provided ID.'))
|
||||
self.errors = {
|
||||
'non_field_errors': _('No record found with the provided ID')
|
||||
+ f': {instance_id}'
|
||||
}
|
||||
return False
|
||||
except ValueError:
|
||||
raise DjangoValidationError(_('Invalid ID format provided.'))
|
||||
self.errors = {
|
||||
'non_field_errors': _('Invalid ID format provided')
|
||||
+ f': {instance_id}'
|
||||
}
|
||||
return False
|
||||
except Exception as e:
|
||||
self.errors = {'non_field_errors': str(e)}
|
||||
return False
|
||||
|
||||
serializer = self.construct_serializer(instance=instance, request=request)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import djmoney.models.validators
|
|||
|
||||
import InvenTree.fields
|
||||
import common.currency
|
||||
import common.settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@ class StaleStockNotificationTests(InvenTreeTestCase):
|
|||
def test_check_stale_stock_no_stale_items(self):
|
||||
"""Test check_stale_stock when no stale items exist."""
|
||||
# Clear all existing stock items
|
||||
stock.models.StockItem.objects.update(parent=None)
|
||||
stock.models.StockItem.objects.all().delete()
|
||||
|
||||
# Create only future expiry items
|
||||
today = helpers.current_date()
|
||||
stock.models.StockItem.objects.create(
|
||||
|
|
@ -194,6 +194,7 @@ class StaleStockNotificationTests(InvenTreeTestCase):
|
|||
def test_check_stale_stock_with_stale_items(self, mock_offload):
|
||||
"""Test check_stale_stock when stale items exist."""
|
||||
# Clear existing stock items
|
||||
stock.models.StockItem.objects.update(parent=None)
|
||||
stock.models.StockItem.objects.all().delete()
|
||||
|
||||
self.create_stock_items_with_expiry()
|
||||
|
|
@ -229,6 +230,7 @@ class StaleStockNotificationTests(InvenTreeTestCase):
|
|||
def test_check_stale_stock_filtering(self):
|
||||
"""Test that check_stale_stock properly filters stock items."""
|
||||
# Clear all existing stock items first
|
||||
stock.models.StockItem.objects.update(parent=None)
|
||||
stock.models.StockItem.objects.all().delete()
|
||||
|
||||
today = helpers.current_date()
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
|||
# Request custom panel information for a part instance
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should be 4 active panels for the part by default
|
||||
# There should be 3 active panels for the part by default
|
||||
self.assertEqual(3, len(response.data))
|
||||
|
||||
_part.active = False
|
||||
|
|
@ -119,8 +119,8 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
|||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# As the part is not active, only 3 panels left
|
||||
self.assertEqual(3, len(response.data))
|
||||
# As the part is not active, only 2 panels left
|
||||
self.assertEqual(2, len(response.data))
|
||||
|
||||
# Disable the "ENABLE_PART_PANELS" setting, and try again
|
||||
plugin.set_setting('ENABLE_PART_PANELS', False)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -93,13 +93,17 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug
|
|||
except (Part.DoesNotExist, ValueError):
|
||||
part = None
|
||||
|
||||
panels.append({
|
||||
'key': 'part-panel',
|
||||
'title': _('Part Panel'),
|
||||
'source': self.plugin_static_file('sample_panel.js:renderPartPanel'),
|
||||
'icon': 'ti:package_outline',
|
||||
'context': {'part_name': part.name if part else ''},
|
||||
})
|
||||
# Only display this panel for "active" parts
|
||||
if part and part.active:
|
||||
panels.append({
|
||||
'key': 'part-panel',
|
||||
'title': _('Part Panel'),
|
||||
'source': self.plugin_static_file(
|
||||
'sample_panel.js:renderPartPanel'
|
||||
),
|
||||
'icon': 'ti:package:outline',
|
||||
'context': {'part_name': part.name if part else ''},
|
||||
})
|
||||
|
||||
# Next, add a custom panel which will appear on the 'purchaseorder' page
|
||||
if target_model == 'purchaseorder' and self.get_setting(
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ class VersionPlugin(InvenTreePlugin):
|
|||
NAME = 'Sample Version Plugin'
|
||||
DESCRIPTION = 'A simple plugin which shows how to use the version limits'
|
||||
MIN_VERSION = '0.1.0'
|
||||
MAX_VERSION = '1.0.0'
|
||||
MAX_VERSION = '2.0.0'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import os
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.core.exceptions import AppRegistryNotReady, ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
||||
|
|
@ -52,6 +52,8 @@ class ReportConfig(AppConfig):
|
|||
try:
|
||||
self.create_default_labels()
|
||||
self.create_default_reports()
|
||||
except ValidationError:
|
||||
logger.warning('Validation error when creating default templates')
|
||||
except (
|
||||
AppRegistryNotReady,
|
||||
IntegrityError,
|
||||
|
|
@ -162,6 +164,10 @@ class ReportConfig(AppConfig):
|
|||
**template, template=self.file_from_template('label', filename)
|
||||
)
|
||||
logger.info("Creating new label template: '%s'", template['name'])
|
||||
except ValidationError:
|
||||
logger.warning(
|
||||
"Could not create label template: '%s'", template['name']
|
||||
)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('create_default_labels', scope='init')
|
||||
|
||||
|
|
@ -261,5 +267,9 @@ class ReportConfig(AppConfig):
|
|||
**template, template=self.file_from_template('report', filename)
|
||||
)
|
||||
logger.info("Created new report template: '%s'", template['name'])
|
||||
except ValidationError:
|
||||
logger.warning(
|
||||
"Could not create report template: '%s'", template['name']
|
||||
)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('create_default_reports', scope='init')
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ from InvenTree.filters import (
|
|||
SEARCH_ORDER_FILTER,
|
||||
SEARCH_ORDER_FILTER_ALIAS,
|
||||
InvenTreeDateFilter,
|
||||
NumberOrNullFilter,
|
||||
)
|
||||
from InvenTree.helpers import extract_serial_numbers, generateTestKey, isNull, str2bool
|
||||
from InvenTree.helpers import extract_serial_numbers, generateTestKey, str2bool
|
||||
from InvenTree.mixins import (
|
||||
CreateAPI,
|
||||
CustomRetrieveUpdateDestroyAPI,
|
||||
|
|
@ -933,6 +934,66 @@ class StockFilter(rest_filters.FilterSet):
|
|||
else:
|
||||
return queryset.exclude(stale_filter)
|
||||
|
||||
exclude_tree = rest_filters.NumberFilter(
|
||||
method='filter_exclude_tree',
|
||||
label=_('Exclude Tree'),
|
||||
help_text=_(
|
||||
'Provide a StockItem PK to exclude that item and all its descendants'
|
||||
),
|
||||
)
|
||||
|
||||
def filter_exclude_tree(self, queryset, name, value):
|
||||
"""Exclude a StockItem and all of its descendants from the queryset."""
|
||||
try:
|
||||
root = StockItem.objects.get(pk=value)
|
||||
pks_to_exclude = [
|
||||
item.pk for item in root.get_descendants(include_self=True)
|
||||
]
|
||||
return queryset.exclude(pk__in=pks_to_exclude)
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
# If the value is invalid or the object doesn't exist, do nothing.
|
||||
return queryset
|
||||
|
||||
cascade = rest_filters.BooleanFilter(
|
||||
method='filter_cascade',
|
||||
label=_('Cascade Locations'),
|
||||
help_text=_('If true, include items in child locations of the given location'),
|
||||
)
|
||||
|
||||
location = NumberOrNullFilter(
|
||||
method='filter_location',
|
||||
label=_('Location'),
|
||||
help_text=_("Filter by numeric Location ID or the literal 'null'"),
|
||||
)
|
||||
|
||||
def filter_cascade(self, queryset, name, value):
|
||||
"""Dummy filter method for 'cascade'.
|
||||
|
||||
- Ensures 'cascade' appears in API documentation
|
||||
- Does NOT actually filter the queryset directly
|
||||
"""
|
||||
return queryset
|
||||
|
||||
def filter_location(self, queryset, name, value):
|
||||
"""Filter for location that also applies cascade logic."""
|
||||
cascade = str2bool(self.data.get('cascade', True))
|
||||
|
||||
if value == 'null':
|
||||
if not cascade:
|
||||
return queryset.filter(location=None)
|
||||
return queryset
|
||||
|
||||
if not cascade:
|
||||
return queryset.filter(location=value)
|
||||
|
||||
try:
|
||||
loc_obj = StockLocation.objects.get(pk=value)
|
||||
except StockLocation.DoesNotExist:
|
||||
return queryset
|
||||
|
||||
children = loc_obj.getUniqueChildren()
|
||||
return queryset.filter(location__in=children)
|
||||
|
||||
|
||||
class StockApiMixin:
|
||||
"""Mixin class for StockItem API endpoints."""
|
||||
|
|
@ -1191,52 +1252,6 @@ class StockList(DataExportViewMixin, StockApiMixin, ListCreateDestroyAPIView):
|
|||
headers=self.get_success_headers(serializer.data),
|
||||
)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Custom filtering for the StockItem queryset."""
|
||||
params = self.request.query_params
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Exclude stock item tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
try:
|
||||
item = StockItem.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[it.pk for it in item.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, StockItem.DoesNotExist): # pragma: no cover
|
||||
pass
|
||||
|
||||
# Does the client wish to filter by stock location?
|
||||
loc_id = params.get('location', None)
|
||||
|
||||
cascade = str2bool(params.get('cascade', True))
|
||||
|
||||
if loc_id is not None:
|
||||
# Filter by 'null' location (i.e. top-level items)
|
||||
if isNull(loc_id):
|
||||
if not cascade:
|
||||
queryset = queryset.filter(location=None)
|
||||
else:
|
||||
try:
|
||||
# If '?cascade=true' then include items which exist in sub-locations
|
||||
if cascade:
|
||||
location = StockLocation.objects.get(pk=loc_id)
|
||||
queryset = queryset.filter(
|
||||
location__in=location.getUniqueChildren()
|
||||
)
|
||||
else:
|
||||
queryset = queryset.filter(location=loc_id)
|
||||
|
||||
except (ValueError, StockLocation.DoesNotExist): # pragma: no cover
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
||||
ordering_field_aliases = {
|
||||
|
|
|
|||
|
|
@ -274,18 +274,19 @@
|
|||
tree_id: 9
|
||||
level: 0
|
||||
lft: 1
|
||||
rght: 2
|
||||
rght: 4
|
||||
|
||||
- model: stock.stockitem
|
||||
pk: 1001
|
||||
fields:
|
||||
part: 100
|
||||
parent: 1000
|
||||
location: 1
|
||||
quantity: 11
|
||||
tree_id: 14
|
||||
level: 0
|
||||
lft: 1
|
||||
rght: 2
|
||||
tree_id: 9
|
||||
level: 1
|
||||
lft: 2
|
||||
rght: 3
|
||||
|
||||
- model: stock.stockitem
|
||||
pk: 1002
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ class StockItemSerializer(
|
|||
'supplier_part_detail.MPN',
|
||||
]
|
||||
|
||||
import_exclude_fields = ['use_pack_size', 'location_path']
|
||||
import_exclude_fields = ['location_path', 'serial_numbers', 'use_pack_size']
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
|
@ -490,7 +490,7 @@ class StockItemSerializer(
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
"""Custom update method to pass the user information through to the instance."""
|
||||
instance._user = self.context['user']
|
||||
instance._user = self.context.get('user', None)
|
||||
|
||||
status_custom_key = validated_data.pop('status_custom_key', None)
|
||||
status = validated_data.pop('status', None)
|
||||
|
|
|
|||
|
|
@ -621,6 +621,13 @@ class StockItemListTest(StockAPITestCase):
|
|||
response = self.get_stock(location=7)
|
||||
self.assertEqual(len(response), 18)
|
||||
|
||||
def test_filter_by_exclude_tree(self):
|
||||
"""Filter StockItem by excluding a StockItem tree."""
|
||||
response = self.get_stock(exclude_tree=1000)
|
||||
for item in response:
|
||||
self.assertNotEqual(item['pk'], 1000)
|
||||
self.assertNotEqual(item['parent'], 1000)
|
||||
|
||||
def test_filter_by_depleted(self):
|
||||
"""Filter StockItem by depleted status."""
|
||||
response = self.get_stock(depleted=1)
|
||||
|
|
@ -786,10 +793,10 @@ class StockItemListTest(StockAPITestCase):
|
|||
def test_filter_has_child_items(self):
|
||||
"""Filter StockItem by has_child_items."""
|
||||
response = self.get_stock(has_child_items=True)
|
||||
self.assertEqual(len(response), 0)
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
response = self.get_stock(has_child_items=False)
|
||||
self.assertEqual(len(response), 29) # TODO: adjust test dataset (belongs_to)
|
||||
self.assertEqual(len(response), 28) # TODO: adjust test dataset (belongs_to)
|
||||
|
||||
def test_filter_sent_to_customer(self):
|
||||
"""Filter StockItem by sent_to_customer."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -144,6 +144,14 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
|||
serializer_class = ExtendedUserSerializer
|
||||
permission_classes = [InvenTree.permissions.StaffRolePermissionOrReadOnly]
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Override destroy method to ensure sessions are deleted first."""
|
||||
# Remove all sessions for this user
|
||||
if sessions := instance.usersession_set.all():
|
||||
sessions.delete()
|
||||
# Normally delete the user
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class UserDetailSetPassword(UpdateAPI):
|
||||
"""Allows superusers to set the password for a user."""
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ if settings.LDAP_AUTH:
|
|||
user.save()
|
||||
|
||||
# if they got an email address from LDAP, create it now and make it the primary
|
||||
if user.email:
|
||||
if (
|
||||
user.email
|
||||
and not EmailAddress.objects.filter(user=user, email=user.email).exists()
|
||||
):
|
||||
EmailAddress.objects.create(user=user, email=user.email, primary=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Unit tests for the 'users' app."""
|
||||
|
||||
from time import sleep
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test import TestCase
|
||||
|
|
@ -349,12 +351,21 @@ class MFALoginTest(InvenTreeAPITestCase):
|
|||
response = self.post(login_url, auth_data, expected_code=200)
|
||||
self._helper_meta_val(response)
|
||||
|
||||
# Add MFA
|
||||
response = self.post(
|
||||
reverse('browser:mfa:manage_totp'),
|
||||
{'code': self.get_topt()},
|
||||
expected_code=200,
|
||||
)
|
||||
# Add MFA - trying in a limited loop in case of timing issues
|
||||
success: bool = False
|
||||
for _ in range(10):
|
||||
try:
|
||||
response = self.post(
|
||||
reverse('browser:mfa:manage_totp'),
|
||||
{'code': self.get_topt()},
|
||||
expected_code=200,
|
||||
)
|
||||
success = True
|
||||
break
|
||||
except AssertionError:
|
||||
sleep(0.8)
|
||||
self.assertTrue(success, 'Failed to add MFA device')
|
||||
|
||||
# There must be a TOTP device now - success
|
||||
self.get(reverse('browser:mfa:manage_totp'), expected_code=200)
|
||||
self.get(reverse('api-token'), expected_code=200)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -185,95 +185,111 @@ click==8.1.8 \
|
|||
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
|
||||
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
|
||||
# via pip-tools
|
||||
coverage[toml]==7.10.6 \
|
||||
--hash=sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930 \
|
||||
--hash=sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747 \
|
||||
--hash=sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65 \
|
||||
--hash=sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7 \
|
||||
--hash=sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27 \
|
||||
--hash=sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034 \
|
||||
--hash=sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd \
|
||||
--hash=sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b \
|
||||
--hash=sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e \
|
||||
--hash=sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c \
|
||||
--hash=sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5 \
|
||||
--hash=sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc \
|
||||
--hash=sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5 \
|
||||
--hash=sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5 \
|
||||
--hash=sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6 \
|
||||
--hash=sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634 \
|
||||
--hash=sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003 \
|
||||
--hash=sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b \
|
||||
--hash=sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7 \
|
||||
--hash=sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb \
|
||||
--hash=sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619 \
|
||||
--hash=sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b \
|
||||
--hash=sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea \
|
||||
--hash=sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb \
|
||||
--hash=sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e \
|
||||
--hash=sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc \
|
||||
--hash=sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32 \
|
||||
--hash=sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb \
|
||||
--hash=sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a \
|
||||
--hash=sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282 \
|
||||
--hash=sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5 \
|
||||
--hash=sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5 \
|
||||
--hash=sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6 \
|
||||
--hash=sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356 \
|
||||
--hash=sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a \
|
||||
--hash=sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e \
|
||||
--hash=sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144 \
|
||||
--hash=sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb \
|
||||
--hash=sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4 \
|
||||
--hash=sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629 \
|
||||
--hash=sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1 \
|
||||
--hash=sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612 \
|
||||
--hash=sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972 \
|
||||
--hash=sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393 \
|
||||
--hash=sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc \
|
||||
--hash=sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352 \
|
||||
--hash=sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6 \
|
||||
--hash=sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0 \
|
||||
--hash=sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3 \
|
||||
--hash=sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80 \
|
||||
--hash=sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9 \
|
||||
--hash=sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78 \
|
||||
--hash=sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0 \
|
||||
--hash=sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a \
|
||||
--hash=sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c \
|
||||
--hash=sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d \
|
||||
--hash=sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32 \
|
||||
--hash=sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0 \
|
||||
--hash=sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80 \
|
||||
--hash=sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713 \
|
||||
--hash=sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27 \
|
||||
--hash=sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153 \
|
||||
--hash=sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c \
|
||||
--hash=sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460 \
|
||||
--hash=sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1 \
|
||||
--hash=sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f \
|
||||
--hash=sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b \
|
||||
--hash=sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945 \
|
||||
--hash=sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b \
|
||||
--hash=sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a \
|
||||
--hash=sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df \
|
||||
--hash=sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d \
|
||||
--hash=sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21 \
|
||||
--hash=sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4 \
|
||||
--hash=sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2 \
|
||||
--hash=sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528 \
|
||||
--hash=sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba \
|
||||
--hash=sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301 \
|
||||
--hash=sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62 \
|
||||
--hash=sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2 \
|
||||
--hash=sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d \
|
||||
--hash=sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf \
|
||||
--hash=sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e \
|
||||
--hash=sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90 \
|
||||
--hash=sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e \
|
||||
--hash=sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862 \
|
||||
--hash=sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5 \
|
||||
--hash=sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6
|
||||
coverage[toml]==7.10.7 \
|
||||
--hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \
|
||||
--hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \
|
||||
--hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \
|
||||
--hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \
|
||||
--hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \
|
||||
--hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \
|
||||
--hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \
|
||||
--hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \
|
||||
--hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \
|
||||
--hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \
|
||||
--hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \
|
||||
--hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \
|
||||
--hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \
|
||||
--hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \
|
||||
--hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \
|
||||
--hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \
|
||||
--hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \
|
||||
--hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \
|
||||
--hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \
|
||||
--hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \
|
||||
--hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \
|
||||
--hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \
|
||||
--hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \
|
||||
--hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \
|
||||
--hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \
|
||||
--hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \
|
||||
--hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \
|
||||
--hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \
|
||||
--hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \
|
||||
--hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \
|
||||
--hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \
|
||||
--hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \
|
||||
--hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \
|
||||
--hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \
|
||||
--hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \
|
||||
--hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \
|
||||
--hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \
|
||||
--hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \
|
||||
--hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \
|
||||
--hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \
|
||||
--hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \
|
||||
--hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \
|
||||
--hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \
|
||||
--hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \
|
||||
--hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \
|
||||
--hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \
|
||||
--hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \
|
||||
--hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \
|
||||
--hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \
|
||||
--hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \
|
||||
--hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \
|
||||
--hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \
|
||||
--hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \
|
||||
--hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \
|
||||
--hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \
|
||||
--hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \
|
||||
--hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \
|
||||
--hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \
|
||||
--hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \
|
||||
--hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \
|
||||
--hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \
|
||||
--hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \
|
||||
--hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \
|
||||
--hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \
|
||||
--hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \
|
||||
--hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \
|
||||
--hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \
|
||||
--hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \
|
||||
--hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \
|
||||
--hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \
|
||||
--hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \
|
||||
--hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \
|
||||
--hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \
|
||||
--hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \
|
||||
--hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \
|
||||
--hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \
|
||||
--hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \
|
||||
--hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \
|
||||
--hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \
|
||||
--hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \
|
||||
--hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \
|
||||
--hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \
|
||||
--hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \
|
||||
--hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \
|
||||
--hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \
|
||||
--hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \
|
||||
--hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \
|
||||
--hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \
|
||||
--hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \
|
||||
--hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \
|
||||
--hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \
|
||||
--hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \
|
||||
--hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \
|
||||
--hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \
|
||||
--hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \
|
||||
--hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \
|
||||
--hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \
|
||||
--hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \
|
||||
--hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \
|
||||
--hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \
|
||||
--hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \
|
||||
--hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \
|
||||
--hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \
|
||||
--hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3
|
||||
# via -r src/backend/requirements-dev.in
|
||||
cryptography==44.0.3 \
|
||||
--hash=sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259 \
|
||||
|
|
@ -320,9 +336,9 @@ distlib==0.4.0 \
|
|||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
django==4.2.23 \
|
||||
--hash=sha256:42fdeaba6e6449d88d4f66de47871015097dc6f1b87910db00a91946295cfae4 \
|
||||
--hash=sha256:dafbfaf52c2f289bd65f4ab935791cb4fb9a198f2a5ba9faf35d7338a77e9803
|
||||
django==4.2.25 \
|
||||
--hash=sha256:2391ab3d78191caaae2c963c19fd70b99e9751008da22a0adcc667c5a4f8d311 \
|
||||
--hash=sha256:9584cf26b174b35620e53c2558b09d7eb180a655a3470474f513ff9acb494f8c
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# django-slowtests
|
||||
|
|
@ -336,13 +352,13 @@ django-test-migrations==1.4.0 \
|
|||
--hash=sha256:294dff98f6d43d020d4046b971bac5339e7c71458a35e9ad6450c388fe16ed6b \
|
||||
--hash=sha256:f0c9c92864ed27d0c9a582e92056637e91227f54bd868a50cb9a1726668c563e
|
||||
# via -r src/backend/requirements-dev.in
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
filelock==3.19.1 \
|
||||
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
|
||||
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
|
||||
# via virtualenv
|
||||
identify==2.6.12 \
|
||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
importlib-metadata==8.7.0 \
|
||||
--hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \
|
||||
|
|
@ -350,9 +366,10 @@ importlib-metadata==8.7.0 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# build
|
||||
isort==6.0.1 \
|
||||
--hash=sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450 \
|
||||
--hash=sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615
|
||||
# isort
|
||||
isort==6.1.0 \
|
||||
--hash=sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784 \
|
||||
--hash=sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481
|
||||
# via -r src/backend/requirements-dev.in
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
|
|
@ -372,9 +389,9 @@ pip==25.2 \
|
|||
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
||||
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
||||
# via pip-tools
|
||||
pip-tools==7.5.0 \
|
||||
--hash=sha256:30639f50961bb09f49d22f4389e8d7d990709677c094ce1114186b1f2e9b5821 \
|
||||
--hash=sha256:69758e4e5a65f160e315d74db46246fdbb30d549f1ed0c4236d057122c9b0f18
|
||||
pip-tools==7.5.1 \
|
||||
--hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \
|
||||
--hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a
|
||||
# via -r src/backend/requirements-dev.in
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
|
|
@ -513,9 +530,10 @@ typing-extensions==4.14.1 \
|
|||
# -c src/backend/requirements.txt
|
||||
# asgiref
|
||||
# django-test-migrations
|
||||
virtualenv==20.33.1 \
|
||||
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
||||
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
||||
# virtualenv
|
||||
virtualenv==20.34.0 \
|
||||
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
||||
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
||||
# via pre-commit
|
||||
wheel==0.45.1 \
|
||||
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
|
||||
|
|
|
|||
|
|
@ -391,9 +391,9 @@ defusedxml==0.7.1 \
|
|||
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||
# via python3-openid
|
||||
django==4.2.23 \
|
||||
--hash=sha256:42fdeaba6e6449d88d4f66de47871015097dc6f1b87910db00a91946295cfae4 \
|
||||
--hash=sha256:dafbfaf52c2f289bd65f4ab935791cb4fb9a198f2a5ba9faf35d7338a77e9803
|
||||
django==4.2.25 \
|
||||
--hash=sha256:2391ab3d78191caaae2c963c19fd70b99e9751008da22a0adcc667c5a4f8d311 \
|
||||
--hash=sha256:9584cf26b174b35620e53c2558b09d7eb180a655a3470474f513ff9acb494f8c
|
||||
# via
|
||||
# -r src/backend/requirements.in
|
||||
# django-allauth
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MantineSize } from '@mantine/core';
|
||||
import type { MantineRadius, MantineSize } from '@mantine/core';
|
||||
|
||||
export type UiSizeType = MantineSize | string | number;
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ export interface UserTheme {
|
|||
primaryColor: string;
|
||||
whiteColor: string;
|
||||
blackColor: string;
|
||||
radius: UiSizeType;
|
||||
radius: MantineRadius;
|
||||
loader: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ export function PrintingActions({
|
|||
id: reportId
|
||||
});
|
||||
|
||||
const [itemIdList, setItemIdList] = useState<number[]>([]);
|
||||
|
||||
// Fetch available printing fields via OPTIONS request
|
||||
const printingFields = useQuery({
|
||||
enabled: labelPrintingEnabled,
|
||||
|
|
@ -89,13 +91,13 @@ export function PrintingActions({
|
|||
filters: {
|
||||
enabled: true,
|
||||
model_type: modelType,
|
||||
items: items.join(',')
|
||||
items: itemIdList.join(',')
|
||||
}
|
||||
};
|
||||
|
||||
fields.items = {
|
||||
...fields.items,
|
||||
value: items,
|
||||
value: itemIdList,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
|
|
@ -115,7 +117,7 @@ export function PrintingActions({
|
|||
};
|
||||
|
||||
return fields;
|
||||
}, [defaultLabelPlugin, pluginKey, printingFields.data, items]);
|
||||
}, [defaultLabelPlugin, pluginKey, printingFields.data, itemIdList]);
|
||||
|
||||
const labelModal = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.label_print),
|
||||
|
|
@ -123,6 +125,9 @@ export function PrintingActions({
|
|||
modalId: 'print-labels',
|
||||
fields: labelFields,
|
||||
timeout: 5000,
|
||||
onOpen: () => {
|
||||
setItemIdList(items);
|
||||
},
|
||||
onClose: () => {
|
||||
setPluginKey('');
|
||||
},
|
||||
|
|
@ -134,24 +139,31 @@ export function PrintingActions({
|
|||
}
|
||||
});
|
||||
|
||||
const reportModal = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.report_print),
|
||||
title: t`Print Report`,
|
||||
modalId: 'print-reports',
|
||||
timeout: 5000,
|
||||
fields: {
|
||||
const reportFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
template: {
|
||||
autoFill: true,
|
||||
filters: {
|
||||
enabled: true,
|
||||
model_type: modelType,
|
||||
items: items.join(',')
|
||||
items: itemIdList.join(',')
|
||||
}
|
||||
},
|
||||
items: {
|
||||
hidden: true,
|
||||
value: items
|
||||
value: itemIdList
|
||||
}
|
||||
};
|
||||
}, [itemIdList, modelType]);
|
||||
|
||||
const reportModal = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.report_print),
|
||||
title: t`Print Report`,
|
||||
modalId: 'print-reports',
|
||||
timeout: 5000,
|
||||
fields: reportFields,
|
||||
onOpen: () => {
|
||||
setItemIdList(items);
|
||||
},
|
||||
submitText: t`Print`,
|
||||
successMessage: null,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
IconDownload,
|
||||
IconFilter
|
||||
} from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import type { CalendarState } from '../../hooks/UseCalendar';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
|
@ -59,6 +59,9 @@ export default function Calendar({
|
|||
|
||||
const [locale] = useLocalState(useShallow((s) => [s.language]));
|
||||
|
||||
// Ensure underscore is replaced with dash
|
||||
const calendarLocale = useMemo(() => locale.replace('_', '-'), [locale]);
|
||||
|
||||
const selectMonth = useCallback(
|
||||
(date: DateValue) => {
|
||||
state.selectMonth(date);
|
||||
|
|
@ -186,7 +189,7 @@ export default function Calendar({
|
|||
plugins={[dayGridPlugin, interactionPlugin]}
|
||||
initialView='dayGridMonth'
|
||||
locales={allLocales}
|
||||
locale={locale}
|
||||
locale={calendarLocale}
|
||||
headerToolbar={false}
|
||||
footerToolbar={false}
|
||||
{...calendarProps}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, Card, Center, Divider, Loader, Text } from '@mantine/core';
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Center,
|
||||
Divider,
|
||||
Loader,
|
||||
Space,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||
import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
|
@ -21,15 +29,23 @@ export default function DashboardLayout() {
|
|||
const [widgets, setWidgets] = useState<DashboardWidgetProps[]>([]);
|
||||
|
||||
// local/remote storage values for widget / layout
|
||||
const [remoteWidgets, setRemoteWidgets, remoteLayouts, setRemoteLayouts] =
|
||||
useLocalState(
|
||||
useShallow((state) => [
|
||||
state.widgets,
|
||||
state.setWidgets,
|
||||
state.layouts,
|
||||
state.setLayouts
|
||||
])
|
||||
);
|
||||
const [
|
||||
remoteWidgets,
|
||||
setRemoteWidgets,
|
||||
remoteLayouts,
|
||||
setRemoteLayouts,
|
||||
showSampleDashboard,
|
||||
setShowSampleDashboard
|
||||
] = useLocalState(
|
||||
useShallow((state) => [
|
||||
state.widgets,
|
||||
state.setWidgets,
|
||||
state.layouts,
|
||||
state.setLayouts,
|
||||
state.showSampleDashboard,
|
||||
state.setShowSampleDashboard
|
||||
])
|
||||
);
|
||||
|
||||
const [editing, setEditing] = useDisclosure(false);
|
||||
const [removing, setRemoving] = useDisclosure(false);
|
||||
|
|
@ -75,6 +91,9 @@ export default function DashboardLayout() {
|
|||
);
|
||||
|
||||
if (newWidget) {
|
||||
if (showSampleDashboard) {
|
||||
setShowSampleDashboard(false);
|
||||
}
|
||||
setWidgets([...widgets, newWidget]);
|
||||
}
|
||||
|
||||
|
|
@ -195,10 +214,48 @@ export default function DashboardLayout() {
|
|||
|
||||
// Clear all widgets from the dashboard
|
||||
const clearWidgets = useCallback(() => {
|
||||
if (showSampleDashboard) {
|
||||
setShowSampleDashboard(false);
|
||||
}
|
||||
setWidgets([]);
|
||||
setLayouts({});
|
||||
}, []);
|
||||
|
||||
const defaultLayouts = {
|
||||
lg: [
|
||||
{
|
||||
w: 6,
|
||||
h: 4,
|
||||
x: 0,
|
||||
y: 0,
|
||||
i: 'gstart',
|
||||
minW: 5,
|
||||
minH: 4,
|
||||
moved: false,
|
||||
static: false
|
||||
},
|
||||
{
|
||||
w: 6,
|
||||
h: 4,
|
||||
x: 6,
|
||||
y: 0,
|
||||
i: 'news',
|
||||
minW: 5,
|
||||
minH: 4,
|
||||
moved: false,
|
||||
static: false
|
||||
}
|
||||
]
|
||||
};
|
||||
const loadWigs = ['news', 'gstart'];
|
||||
const defaultWidgets = useMemo(() => {
|
||||
return loadWigs
|
||||
.map((lwid: string) =>
|
||||
availableWidgets.items.find((wid) => wid.label === lwid)
|
||||
)
|
||||
.filter((widget): widget is DashboardWidgetProps => widget !== undefined);
|
||||
}, [availableWidgets.items, defaultLayouts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardWidgetDrawer
|
||||
|
|
@ -228,43 +285,41 @@ export default function DashboardLayout() {
|
|||
{layouts && loaded && availableWidgets.loaded ? (
|
||||
<>
|
||||
{widgetLabels.length == 0 ? (
|
||||
<Center>
|
||||
<Card shadow='xs' padding='xl' style={{ width: '100%' }}>
|
||||
<Alert
|
||||
color='blue'
|
||||
title={t`No Widgets Selected`}
|
||||
icon={<IconInfoCircle />}
|
||||
>
|
||||
<Text>{t`Use the menu to add widgets to the dashboard`}</Text>
|
||||
</Alert>
|
||||
</Card>
|
||||
</Center>
|
||||
<>
|
||||
<Center>
|
||||
<Card shadow='xs' padding='xl' style={{ width: '100%' }}>
|
||||
<Alert
|
||||
color='blue'
|
||||
title={t`No Widgets Selected`}
|
||||
icon={<IconInfoCircle />}
|
||||
>
|
||||
<Text>{t`Use the menu to add widgets to the dashboard`}</Text>
|
||||
</Alert>
|
||||
</Card>
|
||||
</Center>
|
||||
{showSampleDashboard && (
|
||||
<>
|
||||
<Space h='lg' />
|
||||
{WidgetGrid(
|
||||
defaultLayouts,
|
||||
() => {},
|
||||
editing,
|
||||
defaultWidgets,
|
||||
removing,
|
||||
() => {}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ReactGridLayout
|
||||
className='dashboard-layout'
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
rowHeight={64}
|
||||
layouts={layouts}
|
||||
onLayoutChange={onLayoutChange}
|
||||
compactType={'vertical'}
|
||||
isDraggable={editing}
|
||||
isResizable={editing}
|
||||
margin={[10, 10]}
|
||||
containerPadding={[0, 0]}
|
||||
resizeHandles={['ne', 'se', 'sw', 'nw']}
|
||||
>
|
||||
{widgets.map((item: DashboardWidgetProps) => {
|
||||
return DashboardWidget({
|
||||
item: item,
|
||||
editing: editing,
|
||||
removing: removing,
|
||||
onRemove: () => {
|
||||
removeWidget(item.label);
|
||||
}
|
||||
});
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
WidgetGrid(
|
||||
layouts,
|
||||
onLayoutChange,
|
||||
editing,
|
||||
widgets,
|
||||
removing,
|
||||
removeWidget
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -275,3 +330,40 @@ export default function DashboardLayout() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WidgetGrid(
|
||||
layouts: {},
|
||||
onLayoutChange: (layout: any, newLayouts: any) => void,
|
||||
editing: boolean,
|
||||
widgets: DashboardWidgetProps[],
|
||||
removing: boolean,
|
||||
removeWidget: (widget: string) => void
|
||||
) {
|
||||
return (
|
||||
<ReactGridLayout
|
||||
className='dashboard-layout'
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
rowHeight={64}
|
||||
layouts={layouts}
|
||||
onLayoutChange={onLayoutChange}
|
||||
compactType={'vertical'}
|
||||
isDraggable={editing}
|
||||
isResizable={editing}
|
||||
margin={[10, 10]}
|
||||
containerPadding={[0, 0]}
|
||||
resizeHandles={['ne', 'se', 'sw', 'nw']}
|
||||
>
|
||||
{widgets.map((item: DashboardWidgetProps) => {
|
||||
return DashboardWidget({
|
||||
item: item,
|
||||
editing: editing,
|
||||
removing: removing,
|
||||
onRemove: () => {
|
||||
removeWidget(item.label);
|
||||
}
|
||||
});
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ApiFormFieldType } from '@lib/types/Forms';
|
||||
import { Select } from '@mantine/core';
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { FieldValues, UseControllerReturn } from 'react-hook-form';
|
||||
|
||||
/**
|
||||
|
|
@ -17,6 +17,7 @@ export function ChoiceField({
|
|||
fieldName: string;
|
||||
}>) {
|
||||
const fieldId = useId();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const {
|
||||
field,
|
||||
|
|
@ -70,6 +71,8 @@ export function ChoiceField({
|
|||
error={definition.error ?? error?.message}
|
||||
radius='sm'
|
||||
{...field}
|
||||
ref={inputRef}
|
||||
onDropdownOpen={() => inputRef?.current?.select()}
|
||||
onChange={onChange}
|
||||
data={choices}
|
||||
value={choiceValue}
|
||||
|
|
@ -81,6 +84,7 @@ export function ChoiceField({
|
|||
leftSection={definition.icon}
|
||||
comboboxProps={{ withinPortal: true }}
|
||||
searchable
|
||||
selectFirstOptionOnChange
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ export default function ImporterDataSelector({
|
|||
<IconCircleDashedCheck color='blue' size={16} />
|
||||
)}
|
||||
{!row.complete && !row.valid && (
|
||||
<HoverCard openDelay={50} closeDelay={100}>
|
||||
<HoverCard openDelay={50} closeDelay={100} position='top-start'>
|
||||
<HoverCard.Target>
|
||||
<IconExclamationCircle color='red' size={16} />
|
||||
</HoverCard.Target>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { type ReactNode, useCallback } from 'react';
|
|||
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { navigateToLink } from '@lib/functions/Navigation';
|
||||
import { getBaseUrl, navigateToLink } from '@lib/functions/Navigation';
|
||||
import type {
|
||||
ModelRendererDict,
|
||||
RenderInstanceProps
|
||||
|
|
@ -219,7 +219,10 @@ export function RenderInlineModel({
|
|||
{prefix}
|
||||
{image && <Thumbnail src={image} size={18} />}
|
||||
{url ? (
|
||||
<Anchor href='' onClick={(event: any) => onClick(event)}>
|
||||
<Anchor
|
||||
href={`/${getBaseUrl()}${url}`}
|
||||
onClick={(event: any) => onClick(event)}
|
||||
>
|
||||
{primary}
|
||||
</Anchor>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -194,7 +194,10 @@ export function TableStatusRenderer(
|
|||
accessor?: string
|
||||
): ((record: any) => any) | undefined {
|
||||
return (record: any) => {
|
||||
const status = resolveItem(record, accessor ?? 'status');
|
||||
const status =
|
||||
resolveItem(record, accessor ?? 'status') ??
|
||||
resolveItem(record, 'status_custom_key') ??
|
||||
resolveItem(record, 'status');
|
||||
|
||||
return (
|
||||
status && (
|
||||
|
|
|
|||
|
|
@ -88,7 +88,14 @@ export function RenderStockItem(
|
|||
|
||||
const allocated: number = Math.max(0, instance?.allocated ?? 0);
|
||||
|
||||
if (instance?.serial !== null && instance?.serial !== undefined) {
|
||||
// Determine if this item is serialized
|
||||
const serialized: boolean =
|
||||
instance?.quantity == 1 &&
|
||||
instance?.serial !== null &&
|
||||
instance?.serial !== undefined &&
|
||||
instance?.serial !== '';
|
||||
|
||||
if (serialized) {
|
||||
quantity_string += `${t`Serial Number`}: ${instance.serial}`;
|
||||
} else if (allocated > 0) {
|
||||
const available: number = Math.max(0, instance.quantity - allocated);
|
||||
|
|
|
|||
|
|
@ -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' />
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MantineSize } from '@mantine/core';
|
||||
import type { MantineRadius } from '@mantine/core';
|
||||
|
||||
export const emptyServerAPI = {
|
||||
server: null,
|
||||
|
|
@ -26,7 +26,7 @@ export const emptyServerAPI = {
|
|||
|
||||
export interface SiteMarkProps {
|
||||
value: number;
|
||||
label: MantineSize;
|
||||
label: MantineRadius;
|
||||
}
|
||||
|
||||
export const SizeMarks: SiteMarkProps[] = [
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export function useBuildOrderOutputFields({
|
|||
location: {
|
||||
value: location,
|
||||
onValueChange: (value: any) => {
|
||||
setQuantity(value);
|
||||
setLocation(value);
|
||||
}
|
||||
},
|
||||
auto_allocate: {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function usePluginPanels({
|
|||
// API query to fetch initial information on available plugin panels
|
||||
const pluginQuery = useQuery({
|
||||
enabled: pluginPanelsEnabled && !!model && id !== undefined,
|
||||
queryKey: ['custom-plugin-panels', model, id],
|
||||
queryKey: ['custom-plugin-panels', model, id, instance],
|
||||
throwOnError: (error: any) => {
|
||||
console.error('ERR: Failed to fetch plugin panels');
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -40,19 +40,19 @@ export function UserTheme({ height }: Readonly<{ height: number }>) {
|
|||
);
|
||||
|
||||
// radius
|
||||
function getMark(value: number) {
|
||||
function getRadiusFromValue(value: number) {
|
||||
const obj = SizeMarks.find((mark) => mark.value === value);
|
||||
if (obj) return obj;
|
||||
return SizeMarks[0];
|
||||
if (obj) return obj.label;
|
||||
return 'sm';
|
||||
}
|
||||
function getDefaultRadius() {
|
||||
const value = Number.parseInt(userTheme.radius.toString());
|
||||
return SizeMarks.some((mark) => mark.value === value) ? value : 50;
|
||||
}
|
||||
const [radius, setRadius] = useState(getDefaultRadius());
|
||||
|
||||
const [radius, setRadius] = useState(25);
|
||||
|
||||
function changeRadius(value: number) {
|
||||
const r = getRadiusFromValue(value);
|
||||
setRadius(value);
|
||||
setTheme([{ key: 'radius', value: value.toString() }]);
|
||||
|
||||
setTheme([{ key: 'radius', value: r.toString() }]);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -163,7 +163,7 @@ export function UserTheme({ height }: Readonly<{ height: number }>) {
|
|||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Slider
|
||||
label={(val) => getMark(val).label}
|
||||
label={(val) => getRadiusFromValue(val)}
|
||||
defaultValue={50}
|
||||
step={25}
|
||||
marks={SizeMarks}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { InvenTreeTable } from '../../tables/InvenTreeTable';
|
|||
function ChartTooltip({ label, payload }: Readonly<ChartTooltipProps>) {
|
||||
const formattedLabel: string = useMemo(() => {
|
||||
if (label && typeof label === 'number') {
|
||||
return formatDate(dayjs().format('YYYY-MM-DD')) ?? label;
|
||||
return formatDate(dayjs(label).format('YYYY-MM-DD')) ?? label;
|
||||
} else if (!!label) {
|
||||
return label.toString();
|
||||
} else {
|
||||
|
|
@ -190,7 +190,7 @@ export default function PartStockHistoryDetail({
|
|||
enableBulkDelete: true,
|
||||
params: {
|
||||
part: partId,
|
||||
ordering: 'date'
|
||||
ordering: '-date'
|
||||
},
|
||||
rowActions: rowActions
|
||||
}}
|
||||
|
|
@ -225,7 +225,7 @@ export default function PartStockHistoryDetail({
|
|||
type: 'number',
|
||||
domain: chartLimits,
|
||||
tickFormatter: (value: number) => {
|
||||
return formatDate(dayjs().format('YYYY-MM-DD'));
|
||||
return formatDate(dayjs(value).format('YYYY-MM-DD'));
|
||||
}
|
||||
}}
|
||||
series={[
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -173,17 +173,12 @@ export default function StockDetail() {
|
|||
stockitem.status_custom_key == stockitem.status
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'updated',
|
||||
icon: 'calendar',
|
||||
label: t`Last Updated`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'stocktake',
|
||||
icon: 'calendar',
|
||||
label: t`Last Stocktake`,
|
||||
hidden: !stockitem.stocktake
|
||||
type: 'link',
|
||||
name: 'link',
|
||||
label: t`Link`,
|
||||
external: true,
|
||||
copy: true,
|
||||
hidden: !stockitem.link
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -415,6 +410,19 @@ export default function StockDetail() {
|
|||
icon: 'part',
|
||||
label: t`Packaging`,
|
||||
hidden: !stockitem.packaging
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'updated',
|
||||
icon: 'calendar',
|
||||
label: t`Last Updated`
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'stocktake',
|
||||
icon: 'calendar',
|
||||
label: t`Last Stocktake`,
|
||||
hidden: !stockitem.stocktake
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ interface LocalStateProps {
|
|||
setWidgets: (widgets: string[], noPatch?: boolean) => void;
|
||||
layouts: any;
|
||||
setLayouts: (layouts: any, noPatch?: boolean) => void;
|
||||
showSampleDashboard: boolean;
|
||||
setShowSampleDashboard: (value: boolean) => void;
|
||||
// panels
|
||||
lastUsedPanels: Record<string, string>;
|
||||
setLastUsedPanel: (panelKey: string) => (value: string) => void;
|
||||
|
|
@ -118,6 +120,10 @@ export const useLocalState = create<LocalStateProps>()(
|
|||
if (!noPatch)
|
||||
patchUser('widgets', { widgets: get().widgets, layouts: newLayouts });
|
||||
},
|
||||
showSampleDashboard: true,
|
||||
setShowSampleDashboard: (value) => {
|
||||
set({ showSampleDashboard: value });
|
||||
},
|
||||
// panels
|
||||
lastUsedPanels: {},
|
||||
setLastUsedPanel: (panelKey) => (value) => {
|
||||
|
|
|
|||
|
|
@ -368,14 +368,14 @@ export type StatusColumnProps = TableColumnProps & {
|
|||
};
|
||||
|
||||
export function StatusColumn(props: StatusColumnProps): TableColumn {
|
||||
const accessor: string = props.accessor ?? 'status';
|
||||
const accessor: string = props.accessor ?? 'status_custom_key';
|
||||
|
||||
return {
|
||||
accessor: 'status',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
minWidth: '50px',
|
||||
render: TableStatusRenderer(props.model, accessor ?? 'status_custom_key'),
|
||||
render: TableStatusRenderer(props.model, accessor),
|
||||
...props
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ export function BomTable({
|
|||
if (on_order > 0) {
|
||||
extra.push(
|
||||
<Text key='on_order'>
|
||||
{t`On order`}: {on_order}
|
||||
{t`On order`}: {formatDecimal(on_order)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
|
@ -333,7 +333,7 @@ export function BomTable({
|
|||
if (building > 0) {
|
||||
extra.push(
|
||||
<Text key='building'>
|
||||
{t`Building`}: {building}
|
||||
{t`Building`}: {formatDecimal(building)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,9 +183,13 @@ export default function BuildAllocatedStockTable({
|
|||
|
||||
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
||||
|
||||
const itemsToConsume = useMemo(() => {
|
||||
return selectedItems.filter((item) => !item.part_detail?.trackable);
|
||||
}, [selectedItems]);
|
||||
|
||||
const consumeStock = useConsumeBuildItemsForm({
|
||||
buildId: buildId ?? 0,
|
||||
allocatedItems: selectedItems,
|
||||
allocatedItems: itemsToConsume,
|
||||
onFormSuccess: () => {
|
||||
table.clearSelectedRecords();
|
||||
table.refreshTable();
|
||||
|
|
@ -225,13 +229,16 @@ export default function BuildAllocatedStockTable({
|
|||
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
const part = record.part_detail ?? {};
|
||||
const trackable: boolean = part?.trackable ?? false;
|
||||
|
||||
return [
|
||||
{
|
||||
color: 'green',
|
||||
icon: <IconCircleDashedCheck />,
|
||||
title: t`Consume`,
|
||||
tooltip: t`Consume Stock`,
|
||||
hidden: !user.hasChangeRole(UserRoles.build),
|
||||
hidden: !buildId || trackable || !user.hasChangeRole(UserRoles.build),
|
||||
onClick: () => {
|
||||
setSelectedItems([record]);
|
||||
consumeStock.open();
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ export default function BuildLineTable({
|
|||
);
|
||||
}
|
||||
|
||||
const allocated = record.allocatedQuantity ?? 0;
|
||||
let required = Math.max(0, record.quantity - record.consumed);
|
||||
|
||||
if (output?.pk) {
|
||||
|
|
@ -486,7 +487,7 @@ export default function BuildLineTable({
|
|||
required = record.bom_item_detail?.quantity;
|
||||
}
|
||||
|
||||
if (required <= 0) {
|
||||
if (allocated <= 0 && required <= 0) {
|
||||
return (
|
||||
<Group gap='xs' wrap='nowrap'>
|
||||
<IconCircleCheck size={16} color='green' />
|
||||
|
|
@ -502,7 +503,7 @@ export default function BuildLineTable({
|
|||
return (
|
||||
<ProgressBar
|
||||
progressLabel={true}
|
||||
value={record.allocatedQuantity}
|
||||
value={allocated}
|
||||
maximum={required}
|
||||
/>
|
||||
);
|
||||
|
|
@ -664,9 +665,10 @@ export default function BuildLineTable({
|
|||
(record: any): RowAction[] => {
|
||||
const part = record.part_detail ?? {};
|
||||
const in_production = build.status == buildStatus.PRODUCTION;
|
||||
const consumable = record.bom_item_detail?.consumable ?? false;
|
||||
const consumable: boolean = record.bom_item_detail?.consumable ?? false;
|
||||
const trackable: boolean = part?.trackable ?? false;
|
||||
|
||||
const hasOutput = !!output?.pk;
|
||||
const hasOutput: boolean = !!output?.pk;
|
||||
|
||||
const required = Math.max(
|
||||
0,
|
||||
|
|
@ -677,6 +679,7 @@ export default function BuildLineTable({
|
|||
const canConsume =
|
||||
in_production &&
|
||||
!consumable &&
|
||||
!trackable &&
|
||||
record.allocated > 0 &&
|
||||
user.hasChangeRole(UserRoles.build);
|
||||
|
||||
|
|
@ -952,6 +955,9 @@ export default function BuildLineTable({
|
|||
dataFormatter: formatRecords,
|
||||
enableDownload: true,
|
||||
enableSelection: true,
|
||||
enableLabels: true,
|
||||
modelType: ModelType.buildline,
|
||||
onCellClick: () => {},
|
||||
rowExpansion: rowExpansion
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ export function BuildOrderTable({
|
|||
modelType: ModelType.build,
|
||||
enableSelection: true,
|
||||
enableReports: true,
|
||||
enableLabels: true,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { TableColumn } from '@lib/types/Tables';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||
import { RenderInstance } from '../../components/render/Instance';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
usePurchaseOrderLineItemFields,
|
||||
|
|
@ -249,11 +250,15 @@ export function PurchaseOrderLineItemTable({
|
|||
accessor: 'purchase_price',
|
||||
title: t`Unit Price`
|
||||
}),
|
||||
CurrencyColumn({
|
||||
{
|
||||
accessor: 'total_price',
|
||||
currency_accessor: 'purchase_price_currency',
|
||||
title: t`Total Price`
|
||||
}),
|
||||
title: t`Total Price`,
|
||||
render: (record: any) =>
|
||||
formatCurrency(record.purchase_price, {
|
||||
currency: record.purchase_price_currency,
|
||||
multiplier: record.quantity
|
||||
})
|
||||
},
|
||||
TargetDateColumn({}),
|
||||
LocationColumn({
|
||||
accessor: 'destination_detail',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue