Merge branch 'master' into add-changelog

This commit is contained in:
Matthias Mair 2024-07-22 00:02:01 +02:00 committed by GitHub
commit c965637dd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
199 changed files with 126953 additions and 108007 deletions

View File

@ -2,7 +2,7 @@ version: "3"
services:
db:
image: postgres:13
image: postgres:14
restart: unless-stopped
expose:
- 5432/tcp

View File

@ -97,6 +97,9 @@ if __name__ == '__main__':
)
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)

View File

@ -10,7 +10,7 @@ on:
env:
python_version: 3.9
node_version: 18
node_version: 20
# The OS version must be set per job
server_start_sleep: 60
@ -164,15 +164,27 @@ jobs:
name: schema.yml
path: src/backend/InvenTree/schema.yml
- name: Download public schema
if: needs.paths-filter.outputs.api == 'false'
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)"
version="$(python3 .github/scripts/version_check.py only_version ${{ needs.paths-filter.outputs.api }} 2>&1)"
echo "Version: $version"
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
echo "URL: $url"
curl -s -o api.yaml $url
code=$(curl -s -o api.yaml $url --write-out '%{http_code}' --silent)
if [ "$code" != "200" ]; then
exit 1
fi
echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action
id: breaking_changes
uses: oasdiff/oasdiff-action/diff@main
with:
base: 'api.yaml'
revision: 'src/backend/InvenTree/schema.yml'
format: 'html'
- name: Echoing diff to step
run: echo "${{ steps.breaking_changes.outputs.diff }}" >> $GITHUB_STEP_SUMMARY
- name: Check for differences in API Schema
if: needs.paths-filter.outputs.api == 'false'
run: |
@ -555,6 +567,8 @@ jobs:
run: cd src/frontend && yarn install
- name: Build frontend
run: cd src/frontend && yarn run compile && yarn run build
- name: Write version file - SHA
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
- name: Zip frontend
run: |
cd src/backend/InvenTree/web/static

View File

@ -43,6 +43,10 @@ jobs:
run: cd src/frontend && yarn install
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Write version file - SHA
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
- name: Write version file - TAG
run: cd src/backend/InvenTree/web/static/web/.vite && echo "${{ github.ref_name }}" > tag.txt
- name: Zip frontend
run: |
cd src/backend/InvenTree/web/static/web

View File

@ -7,7 +7,7 @@ on:
env:
python_version: 3.9
node_version: 18
node_version: 20
permissions:
contents: read

View File

@ -14,8 +14,10 @@ env:
- INVENTREE_BACKUP_DIR=/opt/inventree/backup
- INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt
- INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
- APP_REPO=inventree/InvenTree
before_install: contrib/packager.io/preinstall.sh
after_install: contrib/packager.io/postinstall.sh
before_remove: contrib/packager.io/preinstall.sh
before:
- contrib/packager.io/before.sh
dependencies:

View File

@ -17,7 +17,7 @@ repos:
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1
rev: v0.5.1
hooks:
- id: ruff-format
args: [--preview]
@ -27,23 +27,23 @@ repos:
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.1.35
rev: 0.2.13
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt]
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --no-strip-extras, --generate-hashes]
files: src/backend/requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [src/backend/requirements.in, -o, src/backend/requirements.txt]
args: [src/backend/requirements.in, -o, src/backend/requirements.txt, --no-strip-extras, --generate-hashes]
files: src/backend/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt]
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt, --no-strip-extras, --generate-hashes]
files: contrib/dev_reqs/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [docs/requirements.in, -o, docs/requirements.txt]
args: [docs/requirements.in, -o, docs/requirements.txt, --no-strip-extras, --generate-hashes]
files: docs/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
@ -54,9 +54,11 @@ repos:
hooks:
- id: djlint-django
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:
- tomli
exclude: >
(?x)^(
docs/docs/stylesheets/.*|
@ -75,7 +77,7 @@ repos:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.4.0"
rev: "v9.6.0"
hooks:
- id: eslint
additional_dependencies:
@ -87,7 +89,7 @@ repos:
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.3
rev: v8.18.4
hooks:
- id: gitleaks
#- repo: https://github.com/jumanjihouse/pre-commit-hooks

View File

@ -9,7 +9,7 @@
# - Runs InvenTree web server under django development server
# - Monitors source files for any changes, and live-reloads server
ARG base_image=python:3.11-alpine3.18
ARG base_image=python:3.11-alpine3.20
FROM ${base_image} AS inventree_base
# Build arguments for this image
@ -64,7 +64,7 @@ RUN apk add --no-cache \
# Weasyprint requirements : https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#alpine-3-12
py3-pip py3-pillow py3-cffi py3-brotli pango poppler-utils openldap \
# Postgres client
postgresql13-client \
postgresql14-client \
# MySQL / MariaDB client
mariadb-client mariadb-connector-c \
&& \

View File

@ -20,7 +20,7 @@ services:
# Use PostgreSQL as the database backend
# Note: This can be changed to a different backend if required
inventree-dev-db:
image: postgres:13
image: postgres:14
expose:
- 5432/tcp
environment:

View File

@ -38,7 +38,7 @@ services:
# Database service
# Use PostgreSQL as the database backend
inventree-db:
image: postgres:13
image: postgres:14
container_name: inventree-db
expose:
- ${INVENTREE_DB_PORT:-5432}/tcp

View File

@ -1,12 +1,12 @@
#!/bin/ash
# Install system packages required for building InvenTree python libraries
# Note that for postgreslql, we use the 13 version, which matches the version used in the InvenTree docker image
# Note that for postgreslql, we use the 14 version, which matches the version used in the InvenTree docker image
apk add gcc g++ musl-dev openssl-dev libffi-dev cargo python3-dev openldap-dev \
libstdc++ build-base linux-headers py3-grpcio \
jpeg-dev openjpeg-dev libwebp-dev zlib-dev \
sqlite sqlite-dev \
mariadb-connector-c-dev mariadb-client mariadb-dev \
postgresql13-dev postgresql-libs \
postgresql14-dev postgresql-libs \
$@

View File

@ -11,12 +11,15 @@ django==4.2.14 \
django-auth-ldap==4.8.0 \
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
--hash=sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738
# via -r contrib/container/requirements.in
gunicorn==22.0.0 \
--hash=sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9 \
--hash=sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63
# via -r contrib/container/requirements.in
invoke==2.2.0 \
--hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 \
--hash=sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5
# via -r contrib/container/requirements.in
mariadb==1.1.10 \
--hash=sha256:03d6284ef713d1cad40146576a4cc2d6cbc1662060f2a0e59b174e1694521698 \
--hash=sha256:1ce87971c02375236ff8933e6c593c748e7b2f2950b86eabfab4289fd250ea63 \
@ -29,6 +32,7 @@ mariadb==1.1.10 \
--hash=sha256:a332893e3ef7ceb7970ab4bd7c844bcb4bd68a051ca51313566f9808d7411f2d \
--hash=sha256:d7b09ec4abd02ed235257feb769f90cd4066e8f536b55b92f5166103d5b66a63 \
--hash=sha256:dff8b28ce4044574870d7bdd2d9f9f5da8e5f95a7ff6d226185db733060d1a93
# via -r contrib/container/requirements.in
mysqlclient==2.2.4 \
--hash=sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711 \
--hash=sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41 \
@ -39,6 +43,7 @@ mysqlclient==2.2.4 \
--hash=sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5 \
--hash=sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54 \
--hash=sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab
# via -r contrib/container/requirements.in
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
@ -48,6 +53,7 @@ packaging==24.0 \
psycopg[binary, pool]==3.1.18 \
--hash=sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b \
--hash=sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e
# via -r contrib/container/requirements.in
psycopg-binary==3.1.18 \
--hash=sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7 \
--hash=sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c \
@ -131,7 +137,9 @@ pyasn1-modules==0.4.0 \
# via python-ldap
python-ldap==3.4.4 \
--hash=sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828
# via django-auth-ldap
# via
# -r contrib/container/requirements.in
# django-auth-ldap
pyyaml==6.0.1 \
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
@ -184,9 +192,11 @@ pyyaml==6.0.1 \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via -r contrib/container/requirements.in
setuptools==70.3.0 \
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
# via -r contrib/container/requirements.in
sqlparse==0.5.0 \
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
@ -215,6 +225,8 @@ uv==0.1.38 \
--hash=sha256:b0b15e51a0f8240969bc412ed0dd60cfe3f664b30173139ef263d71c596d631f \
--hash=sha256:ea44c07605d1359a7d82bf42706dd86d341f15f4ca2e1f36e51626a7111c2ad5 \
--hash=sha256:f87c9711493c53d32012a96b49c4d53aabdf7ed666cbf2c3fb55dd402a6b31a8
# via -r contrib/container/requirements.in
wheel==0.43.0 \
--hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \
--hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81
# via -r contrib/container/requirements.in

View File

@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt --no-strip-extras --generate-hashes
certifi==2024.7.4 \
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
@ -103,6 +103,7 @@ idna==3.7 \
jc==1.25.2 \
--hash=sha256:26e412a65a478f9da3097653db6277f915cfae5c0f0a3f42026b405936abd358 \
--hash=sha256:97ada193495f79550f06fe0cbfb119ff470bcca57c1cc593a5cdb0008720e0b3
# via -r contrib/dev_reqs/requirements.in
pygments==2.17.2 \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
@ -159,9 +160,11 @@ pyyaml==6.0.1 \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via -r contrib/dev_reqs/requirements.in
requests==2.32.2 \
--hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \
--hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c
# via -r contrib/dev_reqs/requirements.in
ruamel-yaml==0.18.6 \
--hash=sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636 \
--hash=sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b

View File

@ -5,33 +5,40 @@
set -eu
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
echo "Setting VERSION information to $VERSION"
echo "$VERSION" > VERSION
# The sha is the second element in APP_PKG_ITERATION
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
SHA=$(echo $APP_PKG_ITERATION | cut -d'.' -f2)
# Download info
echo "Getting info from github for commit $SHA"
curl -L \
echo "INFO collection | Getting info from github for commit $SHA"
curl -L -s -f \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA > commit.json
curl -L \
https://api.github.com/repos/$APP_REPO/commits/$SHA > commit.json
echo "INFO collection | Got commit.json with size $(wc -c commit.json)"
curl -L -s -f \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA/branches-where-head > branches.json
https://api.github.com/repos/$APP_REPO/commits/$SHA/branches-where-head > branches.json
echo "INFO collection | Got branches.json with size $(wc -c branches.json)"
curl -L -s -f \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/$APP_REPO/commits/$APP_PKG_VERSION > tag.json
echo "INFO collection | Got tag.json with size $(wc -c tag.json)"
# Extract info
echo "Extracting info from github"
echo "INFO extract | Extracting info from github"
DATE=$(jq -r '.commit.committer.date' commit.json)
BRANCH=$(jq -r '.[].name' branches.json)
NODE_ID=$(jq -r '.node_id' commit.json)
SIGNATURE=$(jq -r '.commit.verification.signature' commit.json)
FULL_SHA=$(jq -r '.sha' commit.json)
echo "Write VERSION information"
echo "INFO write | Write VERSION information"
echo "$VERSION" > VERSION
echo "INVENTREE_COMMIT_HASH='$SHA'" >> VERSION
echo "INVENTREE_COMMIT_SHA='$FULL_SHA'" >> VERSION
echo "INVENTREE_COMMIT_DATE='$DATE'" >> VERSION
echo "INVENTREE_PKG_INSTALLER='PKG'" >> VERSION
echo "INVENTREE_PKG_BRANCH='$BRANCH'" >> VERSION
@ -39,5 +46,22 @@ echo "INVENTREE_PKG_TARGET='$TARGET'" >> VERSION
echo "NODE_ID='$NODE_ID'" >> VERSION
echo "SIGNATURE='$SIGNATURE'" >> VERSION
echo "Written VERSION information"
echo "INFO write | Written VERSION information"
echo "### VERSION ###"
cat VERSION
echo "### VERSION ###"
# Try to get frontend
echo "INFO frontend | Trying to get frontend"
# Check if tag sha is the same as the commit sha
TAG_SHA=$(jq -r '.sha' tag.json)
if [ "$TAG_SHA" != "$FULL_SHA" ]; then
echo "INFO frontend | Tag sha '$TAG_SHA' is not the same as commit sha $FULL_SHA, can not download frontend"
else
echo "INFO frontend | Getting frontend from github via tag"
curl https://github.com/$APP_REPO/releases/download/$APP_PKG_VERSION/frontend-build.zip -L -O -f
mkdir -p src/backend/InvenTree/web/static
echo "INFO frontend | Unzipping frontend"
unzip -qq frontend-build.zip -d src/backend/InvenTree/web/static/web
echo "INFO frontend | Unzipped frontend"
fi

View File

@ -60,7 +60,7 @@ function detect_python() {
fi
# Try to detect a python between 3.9 and 3.12 in reverse order
if [ -z "${SETUP_PYTHON}" ]; then
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "# Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
echo "# Checking for python3.${i}"
@ -318,17 +318,17 @@ function set_env() {
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE}
# Database engine
sed -i s=#ENGINE:\ sampleengine=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ ENGINE:\ Database\ engine.\ Selection\ from:=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
# Database name
sed -i s=#NAME:\ \'/path/to/database\'=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ NAME:\ Database\ name=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
# Database user
sed -i s=#USER:\ sampleuser=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ USER:\ Database\ username\ \(if\ required\)=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
# Database password
sed -i s=#PASSWORD:\ samplepassword=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ PASSWORD:\ Database\ password\ \(if\ required\)=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
# Database host
sed -i s=#HOST:\ samplehost=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ HOST:\ Database\ host\ address\ \(if\ required\)=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
# Database port
sed -i s=#PORT:\ 123456=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ PORT:\ Database\ host\ port\ \(if\ required\)=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
# Fixing the permissions
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}

View File

@ -1,6 +1,6 @@
#!/bin/bash
#
# packager.io preinstall script
# packager.io preinstall/preremove script
#
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:

View File

@ -125,7 +125,7 @@ The core software modules are targeting the following versions:
| Python | {{ config.extra.min_python_version }} | Minimum required version |
| Invoke | {{ config.extra.min_invoke_version }} | Minimum required version |
| Django | {{ config.extra.django_version }} | Pinned version |
| Node | 18 | Only needed for frontend development |
| Node | 20 | Only needed for frontend development |
Any other software dependencies are handled by the project package config.

View File

@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile docs/requirements.in -o docs/requirements.txt
# uv pip compile docs/requirements.in -o docs/requirements.txt --no-strip-extras --generate-hashes
anyio==4.3.0 \
--hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \
--hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6
@ -284,6 +284,7 @@ mkdocs==1.6.0 \
--hash=sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7 \
--hash=sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512
# via
# -r docs/requirements.in
# mkdocs-autorefs
# mkdocs-git-revision-date-localized-plugin
# mkdocs-include-markdown-plugin
@ -303,15 +304,19 @@ mkdocs-get-deps==0.2.0 \
mkdocs-git-revision-date-localized-plugin==1.2.5 \
--hash=sha256:0c439816d9d0dba48e027d9d074b2b9f1d7cd179f74ba46b51e4da7bb3dc4b9b \
--hash=sha256:d796a18b07cfcdb154c133e3ec099d2bb5f38389e4fd54d3eb516a8a736815b8
# via -r docs/requirements.in
mkdocs-include-markdown-plugin==6.0.6 \
--hash=sha256:7c80258b2928563c75cc057a7b9a0014701c40804b1b6aa290f3b4032518b43c \
--hash=sha256:7ccafbaa412c1e5d3510c4aff46d1fe64c7a810c01dace4c636253d1aa5bc193
# via -r docs/requirements.in
mkdocs-macros-plugin==1.0.5 \
--hash=sha256:f60e26f711f5a830ddf1e7980865bf5c0f1180db56109803cdd280073c1a050a \
--hash=sha256:fe348d75f01c911f362b6d998c57b3d85b505876dde69db924f2c512c395c328
# via -r docs/requirements.in
mkdocs-material==9.5.24 \
--hash=sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34 \
--hash=sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
--hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
@ -319,10 +324,13 @@ mkdocs-material-extensions==1.3.1 \
mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in
mkdocstrings[python]==0.25.1 \
--hash=sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf \
--hash=sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51
# via mkdocstrings-python
# via
# -r docs/requirements.in
# mkdocstrings-python
mkdocstrings-python==1.10.2 \
--hash=sha256:38a4fd41953defb458a107033440c229c7e9f98f35a24e84d888789c97da5a63 \
--hash=sha256:e8e596b37f45c09b67bec253e035fe18988af5bbbbf44e0ccd711742eed750e5
@ -330,6 +338,7 @@ mkdocstrings-python==1.10.2 \
neoteroi-mkdocs==1.0.5 \
--hash=sha256:1f3b372dee79269157361733c0f45b3a89189077078e0e3224d829a144ef3579 \
--hash=sha256:29875ef444b08aec5619a384142e16f1b4e851465cab4e380fb2b8ae730fe046
# via -r docs/requirements.in
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9

View File

@ -1,12 +1,21 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 222
INVENTREE_API_VERSION = 225
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
- Adds "filters" field to DataImportSession API
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
- Add notes field to ManufacturerPart and SupplierPart API endpoints
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
- Allow adjustment of "packaging" field when receiving items against a purchase order
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process

View File

@ -120,26 +120,28 @@ class Build(
self.validate_reference_field(self.reference)
self.reference_int = self.rebuild_reference_field(self.reference)
if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'):
# Check that the BOM is valid
if not self.part.is_bom_valid():
raise ValidationError({
'part': _('Assembly BOM has not been validated')
})
# Check part when initially creating the build order
if not self.pk or self.has_field_changed('part'):
if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'):
# Check that the BOM is valid
if not self.part.is_bom_valid():
raise ValidationError({
'part': _('Assembly BOM has not been validated')
})
if get_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART'):
# Check that the part is active
if not self.part.active:
raise ValidationError({
'part': _('Build order cannot be created for an inactive part')
})
if get_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART'):
# Check that the part is active
if not self.part.active:
raise ValidationError({
'part': _('Build order cannot be created for an inactive part')
})
if get_global_setting('BUILDORDER_REQUIRE_LOCKED_PART'):
# Check that the part is locked
if not self.part.locked:
raise ValidationError({
'part': _('Build order cannot be created for an unlocked part')
})
if get_global_setting('BUILDORDER_REQUIRE_LOCKED_PART'):
# Check that the part is locked
if not self.part.locked:
raise ValidationError({
'part': _('Build order cannot be created for an unlocked part')
})
# On first save (i.e. creation), run some extra checks
if self.pk is None:

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.11 on 2024-07-16 12:58
import InvenTree.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0070_remove_manufacturerpartattachment_manufacturer_part_and_more'),
]
operations = [
migrations.AddField(
model_name='manufacturerpart',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
),
migrations.AddField(
model_name='supplierpart',
name='notes',
field=InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes'),
),
]

View File

@ -451,6 +451,7 @@ class Address(InvenTree.models.InvenTreeModel):
class ManufacturerPart(
InvenTree.models.InvenTreeAttachmentMixin,
InvenTree.models.InvenTreeBarcodeMixin,
InvenTree.models.InvenTreeNotesMixin,
InvenTree.models.InvenTreeMetadataModel,
):
"""Represents a unique part as provided by a Manufacturer Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) Each ManufacturerPart is also linked to a Part object. A Part may be available from multiple manufacturers.
@ -624,6 +625,7 @@ class SupplierPartManager(models.Manager):
class SupplierPart(
InvenTree.models.MetadataMixin,
InvenTree.models.InvenTreeBarcodeMixin,
InvenTree.models.InvenTreeNotesMixin,
common.models.MetaMixin,
InvenTree.models.InvenTreeModel,
):

View File

@ -213,7 +213,7 @@ class ContactSerializer(DataImportExportSerializerMixin, InvenTreeModelSerialize
@register_importer()
class ManufacturerPartSerializer(
DataImportExportSerializerMixin, InvenTreeTagModelSerializer
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
):
"""Serializer for ManufacturerPart object."""
@ -232,6 +232,7 @@ class ManufacturerPartSerializer(
'MPN',
'link',
'barcode_hash',
'notes',
'tags',
]
@ -305,7 +306,7 @@ class ManufacturerPartParameterSerializer(
@register_importer()
class SupplierPartSerializer(
DataImportExportSerializerMixin, InvenTreeTagModelSerializer
DataImportExportSerializerMixin, InvenTreeTagModelSerializer, NotesFieldMixin
):
"""Serializer for SupplierPart object."""
@ -340,6 +341,7 @@ class SupplierPartSerializer(
'supplier_detail',
'url',
'updated',
'notes',
'tags',
]

View File

@ -171,11 +171,40 @@ src="{% static 'img/blank_image.png' %}"
</div>
</div>
<div class='panel panel-hidden' id='panel-manufacturer-part-notes'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Manufacturer Part Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
<textarea id='manufacturer-part-notes'></textarea>
</div>
</div>
{% endblock page_content %}
{% block js_ready %}
{{ block.super }}
// Load the "notes" tab
onPanelLoad('manufacturer-part-notes', function() {
setupNotesField(
'manufacturer-part-notes',
'{% url "api-manufacturer-part-detail" part.pk %}',
{
model_type: "manufacturerpart",
model_id: {{ part.pk }},
editable: {% js_bool roles.purchase_order.change %},
}
);
});
onPanelLoad("attachments", function() {
loadAttachmentTable('manufacturerpart', {{ part.pk }});
});

View File

@ -8,3 +8,5 @@
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
{% trans "Attachments" as text %}
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label="manufacturer-part-notes" text=text icon="fa-clipboard" %}

View File

@ -264,11 +264,40 @@ src="{% static 'img/blank_image.png' %}"
</div>
</div>
<div class='panel panel-hidden' id='panel-supplier-part-notes'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Supplier Part Notes" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% include "notes_buttons.html" %}
</div>
</div>
</div>
<div class='panel-content'>
<textarea id='supplier-part-notes'></textarea>
</div>
</div>
{% endblock page_content %}
{% block js_ready %}
{{ block.super }}
// Load the "notes" tab
onPanelLoad('supplier-part-notes', function() {
setupNotesField(
'supplier-part-notes',
'{% url "api-supplier-part-detail" part.pk %}',
{
model_type: "supplierpart",
model_id: {{ part.pk }},
editable: {% js_bool roles.purchase_order.change %},
}
);
});
{% if barcodes %}
$("#show-qr-code").click(function() {

View File

@ -8,3 +8,5 @@
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
{% trans "Supplier Part Pricing" as text %}
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}
{% trans "Notes" as text %}
{% include "sidebar_item.html" with label="supplier-part-notes" text=text icon="fa-clipboard" %}

View File

@ -11,6 +11,8 @@
# Note: Database configuration options can also be specified from environmental variables,
# with the prefix INVENTREE_DB_
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
# Do not change this section if you are using the package - use `inventree config` instead
# TO MAINTAINERS: Do not change database strings
database:
# --- Available options: ---
# ENGINE: Database engine. Selection from:

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.14 on 2024-07-16 03:04
from django.db import migrations, models
import importer.validators
class Migration(migrations.Migration):
dependencies = [
('importer', '0002_dataimportsession_field_overrides'),
]
operations = [
migrations.AddField(
model_name='dataimportsession',
name='field_filters',
field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Filters'),
),
]

View File

@ -32,8 +32,9 @@ class DataImportSession(models.Model):
data_file: FileField for the data file to import
status: IntegerField for the status of the import session
user: ForeignKey to the User who initiated the import
field_defaults: JSONField for field default values
field_overrides: JSONField for field override values
field_defaults: JSONField for field default values - provides a backup value for a field
field_overrides: JSONField for field override values - used to force a value for a field
field_filters: JSONField for field filter values - optional field API filters
"""
@staticmethod
@ -101,6 +102,13 @@ class DataImportSession(models.Model):
validators=[importer.validators.validate_field_defaults],
)
field_filters = models.JSONField(
blank=True,
null=True,
verbose_name=_('Field Filters'),
validators=[importer.validators.validate_field_defaults],
)
@property
def field_mapping(self):
"""Construct a dict of field mappings for this import session.

View File

@ -50,6 +50,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
'column_mappings',
'field_defaults',
'field_overrides',
'field_filters',
'row_count',
'completed_row_count',
]
@ -104,6 +105,19 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
return overrides
def validate_field_filters(self, filters):
"""De-stringify the field filters."""
if filters is None:
return None
if type(filters) is not dict:
try:
filters = json.loads(str(filters))
except:
raise ValidationError(_('Invalid field filters'))
return filters
def create(self, validated_data):
"""Override create method for this serializer.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -742,6 +742,14 @@ class PurchaseOrder(TotalPriceMixin, Order):
# Extract optional notes field
notes = kwargs.get('notes', '')
# Extract optional packaging field
packaging = kwargs.get('packaging', None)
if not packaging:
# Default to the packaging field for the linked supplier part
if line.part:
packaging = line.part.packaging
# Extract optional barcode field
barcode = kwargs.get('barcode', None)
@ -791,6 +799,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
purchase_order=self,
status=status,
batch=batch_code,
packaging=packaging,
serial=sn,
purchase_price=unit_purchase_price,
)

View File

@ -373,13 +373,13 @@ class PurchaseOrderLineItemSerializer(
fields = [
'pk',
'part',
'quantity',
'reference',
'notes',
'order',
'order_detail',
'overdue',
'part',
'part_detail',
'supplier_part_detail',
'received',
@ -454,6 +454,14 @@ class PurchaseOrderLineItemSerializer(
return queryset
part = serializers.PrimaryKeyRelatedField(
queryset=part_models.SupplierPart.objects.all(),
many=False,
required=True,
allow_null=True,
label=_('Supplier Part'),
)
quantity = serializers.FloatField(min_value=0, required=True)
def validate_quantity(self, quantity):
@ -588,7 +596,10 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
'location',
'quantity',
'status',
'batch_code' 'serial_numbers',
'batch_code',
'serial_numbers',
'packaging',
'note',
]
line_item = serializers.PrimaryKeyRelatedField(
@ -646,6 +657,22 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
choices=StockStatus.items(), default=StockStatus.OK.value, label=_('Status')
)
packaging = serializers.CharField(
label=_('Packaging'),
help_text=_('Override packaging information for incoming stock items'),
required=False,
default='',
allow_blank=True,
)
note = serializers.CharField(
label=_('Note'),
help_text=_('Additional note for incoming stock items'),
required=False,
default='',
allow_blank=True,
)
barcode = serializers.CharField(
label=_('Barcode'),
help_text=_('Scanned barcode'),
@ -798,7 +825,9 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
status=item['status'],
barcode=item.get('barcode', ''),
batch_code=item.get('batch_code', ''),
packaging=item.get('packaging', ''),
serials=item.get('serials', None),
notes=item.get('note', None),
)
except (ValidationError, DjangoValidationError) as exc:
# Catch model errors and re-throw as DRF errors

View File

@ -1137,6 +1137,56 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertEqual(item.quantity, 10)
self.assertEqual(item.batch, 'B-xyz-789')
def test_packaging(self):
"""Test that we can supply a 'packaging' value when receiving items."""
line_1 = models.PurchaseOrderLineItem.objects.get(pk=1)
line_2 = models.PurchaseOrderLineItem.objects.get(pk=2)
line_1.part.packaging = 'Reel'
line_1.part.save()
line_2.part.packaging = 'Tube'
line_2.part.save()
# Receive items without packaging data
data = {
'items': [
{'line_item': line_1.pk, 'quantity': 1},
{'line_item': line_2.pk, 'quantity': 1},
],
'location': 1,
}
n = StockItem.objects.count()
self.post(self.url, data, expected_code=201)
item_1 = StockItem.objects.filter(supplier_part=line_1.part).first()
self.assertEqual(item_1.packaging, 'Reel')
item_2 = StockItem.objects.filter(supplier_part=line_2.part).first()
self.assertEqual(item_2.packaging, 'Tube')
# Receive items and override packaging data
data = {
'items': [
{'line_item': line_1.pk, 'quantity': 1, 'packaging': 'Bag'},
{'line_item': line_2.pk, 'quantity': 1, 'packaging': 'Box'},
],
'location': 1,
}
self.post(self.url, data, expected_code=201)
item_1 = StockItem.objects.filter(supplier_part=line_1.part).last()
self.assertEqual(item_1.packaging, 'Bag')
item_2 = StockItem.objects.filter(supplier_part=line_2.part).last()
self.assertEqual(item_2.packaging, 'Box')
# Check that the expected number of stock items has been created
self.assertEqual(n + 4, StockItem.objects.count())
class SalesOrderTest(OrderTest):
"""Tests for the SalesOrder API."""

View File

@ -1,5 +1,7 @@
"""API for the plugin app."""
from typing import Optional
from django.core.exceptions import ValidationError
from django.urls import include, path, re_path
from django.utils.translation import gettext_lazy as _
@ -266,7 +268,9 @@ class PluginSettingList(ListAPI):
filterset_fields = ['plugin__active', 'plugin__key']
def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin:
def check_plugin(
plugin_slug: Optional[str], plugin_pk: Optional[int]
) -> InvenTreePlugin:
"""Check that a plugin for the provided slug exists and get the config.
Args:
@ -286,16 +290,16 @@ def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin:
raise NotFound(detail='Plugin not specified')
# Define filter
filter = {}
filters = {}
if plugin_slug:
filter['key'] = plugin_slug
filters['key'] = plugin_slug
elif plugin_pk:
filter['pk'] = plugin_pk
filters['pk'] = plugin_pk
ref = plugin_slug or plugin_pk
# Check that the 'plugin' specified is valid
try:
plugin_cgf = PluginConfig.objects.filter(**filter).first()
plugin_cgf = PluginConfig.objects.filter(**filters).first()
except PluginConfig.DoesNotExist:
raise NotFound(detail=f"Plugin '{ref}' not installed")

View File

@ -39,7 +39,7 @@ def qrcode(data, **kwargs):
fill_color = kwargs.pop('fill_color', 'black')
back_color = kwargs.pop('back_color', 'white')
format = kwargs.pop('format', 'PNG')
img_format = kwargs.pop('format', 'PNG')
params.update(**kwargs)
@ -51,7 +51,7 @@ def qrcode(data, **kwargs):
qri = qr.make_image(fill_color=fill_color, back_color=back_color)
# Render to byte-encoded image
return image_data(qri, fmt=format)
return image_data(qri, fmt=img_format)
@register.simple_tag()
@ -59,7 +59,7 @@ def barcode(data, barcode_class='code128', **kwargs):
"""Render a barcode."""
constructor = python_barcode.get_barcode_class(barcode_class)
format = kwargs.pop('format', 'PNG')
img_format = kwargs.pop('format', 'PNG')
data = str(data).zfill(constructor.digits)
@ -70,4 +70,4 @@ def barcode(data, barcode_class='code128', **kwargs):
image = barcode_image.render(writer_options=kwargs)
# Render to byte-encoded image
return image_data(image, fmt=format)
return image_data(image, fmt=img_format)

View File

@ -170,6 +170,18 @@ def uploaded_image(
width = kwargs.get('width', None)
height = kwargs.get('height', None)
if width is not None:
try:
width = int(width)
except ValueError:
width = None
if height is not None:
try:
height = int(height)
except ValueError:
height = None
if width is not None and height is not None:
# Resize the image, width *and* height are provided
img = img.resize((width, height))
@ -185,10 +197,12 @@ def uploaded_image(
img = img.resize((wsize, height))
# Optionally rotate the image
rotate = kwargs.get('rotate', None)
if rotate is not None:
img = img.rotate(rotate)
if rotate := kwargs.get('rotate', None):
try:
rotate = int(rotate)
img = img.rotate(rotate)
except ValueError:
pass
# Return a base-64 encoded image
img_data = report.helpers.encode_image_base64(img)

View File

@ -163,7 +163,13 @@ function generateTreeStructure(data, options) {
const nodes = {};
const roots = [];
for (let node of data) {
if (!data || !Array.isArray(data) || data.length == 0) {
return [];
}
for (let ii = 0; ii < data.length; ii++) {
let node = data[ii];
nodes[node.pk] = node;
node.selectable = false;
@ -174,11 +180,15 @@ function generateTreeStructure(data, options) {
if (options.processNode) {
node = options.processNode(node);
data[ii] = node;
}
}
for (let node of data) {
if (node.parent != null) {
for (let ii = 0; ii < data.length; ii++) {
let node = data[ii];
if (!!node.parent) {
if (nodes[node.parent].nodes) {
nodes[node.parent].nodes.push(node);
} else {
@ -186,7 +196,7 @@ function generateTreeStructure(data, options) {
}
if (node.state.expanded) {
while (node.parent != null) {
while (!!node.parent) {
nodes[node.parent].state.expanded = true;
node = nodes[node.parent];
}

View File

@ -298,7 +298,8 @@ function constructDeleteForm(fields, options) {
* - closeText: Text for the "close" button
* - fields: list of fields to display, with the following options
* - filters: API query filters
* - onEdit: callback or array of callbacks which get fired when field is edited
* - onEdit: callback or array of callbacks which get fired when field is edited - does not get triggered until the field loses focus, ref: https://api.jquery.com/change/
* - onInput: callback or array of callbacks which get fired when an input is detected in the field
* - secondary: Define a secondary modal form for this field
* - label: Specify custom label
* - help_text: Specify custom help_text
@ -1501,8 +1502,23 @@ function handleFormErrors(errors, fields={}, options={}) {
for (var field_name in errors) {
var field = fields[field_name] || {};
var field_errors = errors[field_name];
let field = fields[field_name] || null;
let field_errors = errors[field_name];
// No matching field - append to non_field_errors
if (!field || field.hidden) {
if (Array.isArray(field_errors)) {
field_errors.forEach((err) => {
non_field_errors.append(`<div class='alert alert-block alert-danger'>${err}</div>`);
});
} else {
non_field_errors.append(`<div class='alert alert-block alert-danger'>${field_errors.toString()}</div>`);
}
continue;
}
// for nested objects with children and dependent fields with a child defined, extract nested errors
if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
@ -1646,6 +1662,23 @@ function addFieldCallback(name, field, options) {
});
}
if(field.onInput){
el.on('input', function(){
var value = getFormFieldValue(name, field, options);
let onInputHandlers = field.onInput;
if (!Array.isArray(onInputHandlers)) {
onInputHandlers = [onInputHandlers];
}
for (const onInput of onInputHandlers) {
onInput(value, name, field, options);
}
});
}
// attach field callback for nested fields
if(field.type === "nested object") {
for (const [c_name, c_field] of Object.entries(field.children)) {

View File

@ -343,7 +343,7 @@ function poLineItemFields(options={}) {
reference: {},
purchase_price: {
icon: 'fa-dollar-sign',
onEdit: function(value, name, field, opts) {
onInput: function(value, name, field, opts) {
updateFieldValue('auto_pricing', value === '', {}, opts);
}
},
@ -1136,7 +1136,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
);
// Hidden barcode input
var barcode_input = constructField(
const barcode_input = constructField(
`items_barcode_${pk}`,
{
type: 'string',
@ -1145,7 +1145,8 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
var sn_input = constructField(
// Hidden serial number input
const sn_input = constructField(
`items_serial_numbers_${pk}`,
{
type: 'string',
@ -1159,6 +1160,37 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
// Hidden packaging input
const packaging_input = constructField(
`items_packaging_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Packaging" %}',
help_text: '{% trans "Specify packaging for incoming stock items" %}',
icon: 'fa-boxes',
value: line_item.supplier_part_detail.packaging,
},
{
hideLabels: true,
}
);
// Hidden note input
const note_input = constructField(
`items_note_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Note" %}',
icon: 'fa-sticky-note',
value: '',
},
{
hideLabels: true,
}
);
var quantity_input_group = `${quantity_input}${pack_size_div}`;
// Construct list of StockItem status codes
@ -1220,6 +1252,16 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
}
);
buttons += makeIconButton(
'fa-boxes',
'button-row-add-packaging',
pk,
'{% trans "Specify packaging" %}',
{
collapseTarget: `row-packaging-${pk}`
}
);
if (line_item.part_detail.trackable) {
buttons += makeIconButton(
'fa-hashtag',
@ -1232,6 +1274,16 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
);
}
buttons += makeIconButton(
'fa-sticky-note',
'button-row-add-note',
pk,
'{% trans "Add note" %}',
{
collapseTarget: `row-note-${pk}`,
}
);
if (line_items.length > 1) {
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
}
@ -1275,12 +1327,23 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
<td colspan='2'>${batch_input}</td>
<td></td>
</tr>
<tr id='row-packaging-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Packaging" %}</th>
<td colspan='2'>${packaging_input}</td>
<td></td>
</tr>
<tr id='row-serials-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Serials" %}</th>
<td colspan=2'>${sn_input}</td>
<td></td>
</tr>
<tr id='row-note-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Note" %}</th>
<td colspan='2'>${note_input}</td>
<td></td>
`;
return html;
@ -1472,6 +1535,14 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
line.batch_code = getFormFieldValue(`items_batch_code_${pk}`);
}
if (getFormFieldElement(`items_packaging_${pk}`).exists()) {
line.packaging = getFormFieldValue(`items_packaging_${pk}`);
}
if (getFormFieldElement(`items_note_${pk}`).exists()) {
line.note = getFormFieldValue(`items_note_${pk}`);
}
if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) {
line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`);
}

View File

@ -17,6 +17,7 @@
formatDecimal,
formatPriceRange,
getCurrencyConversionRates,
getFormFieldElement,
getFormFieldValue,
getTableData,
global_settings,
@ -1010,14 +1011,16 @@ function mergeStockItems(items, options={}) {
*/
function adjustStock(action, items, options={}) {
var formTitle = 'Form Title Here';
var actionTitle = null;
let formTitle = 'Form Title Here';
let actionTitle = null;
const allowExtraFields = action == 'move';
// API url
var url = null;
var specifyLocation = false;
var allowSerializedStock = false;
let specifyLocation = false;
let allowSerializedStock = false;
switch (action) {
case 'move':
@ -1069,7 +1072,7 @@ function adjustStock(action, items, options={}) {
for (var idx = 0; idx < items.length; idx++) {
var item = items[idx];
const item = items[idx];
if ((item.serial != null) && (item.serial != '') && !allowSerializedStock) {
continue;
@ -1112,7 +1115,6 @@ function adjustStock(action, items, options={}) {
let quantityString = '';
var location = locationDetail(item, false);
if (item.location_detail) {
@ -1152,11 +1154,68 @@ function adjustStock(action, items, options={}) {
);
}
let buttons = wrapButtons(makeRemoveButton(
let buttons = '';
if (allowExtraFields) {
buttons += makeIconButton(
'fa-layer-group',
'button-row-add-batch',
pk,
'{% trans "Adjust batch code" %}',
{
collapseTarget: `row-batch-${pk}`
}
);
buttons += makeIconButton(
'fa-boxes',
'button-row-add-packaging',
pk,
'{% trans "Adjust packaging" %}',
{
collapseTarget: `row-packaging-${pk}`
}
);
}
buttons += makeRemoveButton(
'button-stock-item-remove',
pk,
'{% trans "Remove stock item" %}',
));
);
buttons = wrapButtons(buttons);
// Add in options for "batch code" and "serial numbers"
const batch_input = constructField(
`items_batch_code_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Batch Code" %}',
help_text: '{% trans "Enter batch code for incoming stock items" %}',
icon: 'fa-layer-group',
value: item.batch,
},
{
hideLabels: true,
}
);
const packaging_input = constructField(
`items_packaging_${pk}`,
{
type: 'string',
required: false,
label: '{% trans "Packaging" %}',
help_text: '{% trans "Specify packaging for incoming stock items" %}',
icon: 'fa-boxes',
value: item.packaging,
},
{
hideLabels: true,
}
);
html += `
<tr id='stock_item_${pk}' class='stock-item-row'>
@ -1170,6 +1229,19 @@ function adjustStock(action, items, options={}) {
</div>
</td>
<td id='buttons_${pk}'>${buttons}</td>
</tr>
<!-- Hidden row for extra data entry -->
<tr id='row-batch-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Batch" %}</th>
<td colspan='2'>${batch_input}</td>
<td></td>
</tr>
<tr id='row-packaging-${pk}' class='collapse'>
<td colspan='2'></td>
<th>{% trans "Packaging" %}</th>
<td colspan='2'>${packaging_input}</td>
<td></td>
</tr>`;
itemCount += 1;
@ -1266,21 +1338,30 @@ function adjustStock(action, items, options={}) {
var item_pk_values = [];
items.forEach(function(item) {
var pk = item.pk;
let pk = item.pk;
// Does the row exist in the form?
var row = $(opts.modal).find(`#stock_item_${pk}`);
let row = $(opts.modal).find(`#stock_item_${pk}`);
if (row.exists()) {
item_pk_values.push(pk);
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
data.items.push({
let quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
let line = {
pk: pk,
quantity: quantity,
});
quantity: quantity
};
if (getFormFieldElement(`items_batch_code_${pk}`).exists()) {
line.batch = getFormFieldValue(`items_batch_code_${pk}`);
}
if (getFormFieldElement(`items_packaging_${pk}`).exists()) {
line.packaging = getFormFieldValue(`items_packaging_${pk}`);
}
data.items.push(line);
}
});

View File

@ -1,9 +1,11 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt --no-strip-extras --generate-hashes
asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
# via django
# via
# -c src/backend/requirements.txt
# django
build==1.2.1 \
--hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \
--hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4
@ -61,7 +63,9 @@ cffi==1.16.0 \
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
# via cryptography
# via
# -c src/backend/requirements.txt
# cryptography
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
@ -157,7 +161,9 @@ charset-normalizer==3.3.2 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via pdfminer-six
# via
# -c src/backend/requirements.txt
# pdfminer-six
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
@ -215,6 +221,7 @@ coverage[toml]==7.5.4 \
--hash=sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633 \
--hash=sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9 \
--hash=sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c
# via -r src/backend/requirements-dev.in
cryptography==42.0.8 \
--hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \
--hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \
@ -248,7 +255,9 @@ cryptography==42.0.8 \
--hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \
--hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \
--hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e
# via pdfminer-six
# via
# -c src/backend/requirements.txt
# pdfminer-six
distlib==0.3.8 \
--hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \
--hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64
@ -257,18 +266,23 @@ django==4.2.14 \
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
# via
# -c src/backend/requirements.txt
# django-admin-shell
# django-slowtests
django-admin-shell==2.0.1 \
--hash=sha256:334a651e53ae4f59d0d279d7ede7dc5ed7a7733d4d093765b447dca5274c7b30 \
--hash=sha256:b129e282ebd581c2099c0504edf081259728b3a504b40c5784d0457b8cb41470
# via -r src/backend/requirements-dev.in
django-querycount==0.8.3 \
--hash=sha256:0782484e8a1bd29498fa0195a67106e47cdcc98fafe80cebb1991964077cb694
# via -r src/backend/requirements-dev.in
django-slowtests==1.1.1 \
--hash=sha256:3c6936d420c9df444ac03625b41d97de043c662bbde61fbcd33e4cd407d0c247
# via -r src/backend/requirements-dev.in
django-test-migrations==1.3.0 \
--hash=sha256:b42edb1af481e08c9d91c95aa9b373e76e905a931bc19c086ec00a6cb936876e \
--hash=sha256:b52b29475f9a1bcaa4512f2ec8fad08b5f470cf1cf522e86b7d950252fb6fbf1
# via -r src/backend/requirements-dev.in
filelock==3.15.4 \
--hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \
--hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7
@ -280,10 +294,13 @@ identify==2.5.36 \
importlib-metadata==7.1.0 \
--hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \
--hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2
# via build
# via
# -c src/backend/requirements.txt
# build
isort==5.13.2 \
--hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \
--hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6
# via -r src/backend/requirements-dev.in
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
@ -291,10 +308,13 @@ nodeenv==1.9.1 \
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via build
# via
# -c src/backend/requirements.txt
# build
pdfminer-six==20231228 \
--hash=sha256:6004da3ad1a7a4d45930cb950393df89b068e73be365a6ff64a838d37bcb08c4 \
--hash=sha256:e8d3c3310e6fbc1fe414090123ab01351634b4ecb021232206c4c9a8ca3e3b8f
# via -r src/backend/requirements-dev.in
pip==24.1 \
--hash=sha256:a775837439bf5da2c1a0c2fa43d5744854497c689ddbd9344cf3ea6d00598540 \
--hash=sha256:bdae551038c0ce6a83030b4aedef27fc95f0daa683593fea22fa05e55ed8e317
@ -302,6 +322,7 @@ pip==24.1 \
pip-tools==7.4.1 \
--hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \
--hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9
# via -r src/backend/requirements-dev.in
platformdirs==4.2.2 \
--hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
--hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
@ -309,10 +330,13 @@ platformdirs==4.2.2 \
pre-commit==3.7.1 \
--hash=sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a \
--hash=sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5
# via -r src/backend/requirements-dev.in
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
# via
# -c src/backend/requirements.txt
# cffi
pyproject-hooks==1.1.0 \
--hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \
--hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2
@ -371,15 +395,22 @@ pyyaml==6.0.1 \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via pre-commit
setuptools==70.3.0 \
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
# via pip-tools
# via
# -c src/backend/requirements.txt
# pre-commit
setuptools==71.0.3 \
--hash=sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d \
--hash=sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements-dev.in
# pip-tools
sqlparse==0.5.0 \
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
# via django
# via
# -c src/backend/requirements.txt
# django
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
@ -391,6 +422,7 @@ typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# -c src/backend/requirements.txt
# asgiref
# django-test-migrations
virtualenv==20.26.3 \
@ -404,4 +436,6 @@ wheel==0.43.0 \
zipp==3.19.2 \
--hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \
--hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c
# via importlib-metadata
# via
# -c src/backend/requirements.txt
# importlib-metadata

View File

@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt --no-strip-extras --generate-hashes
asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
@ -266,6 +266,7 @@ charset-normalizer==3.3.2 \
coreapi==2.3.3 \
--hash=sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb \
--hash=sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3
# via -r src/backend/requirements.in
coreschema==0.0.4 \
--hash=sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f \
--hash=sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607
@ -303,7 +304,9 @@ cryptography==42.0.8 \
--hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \
--hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \
--hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e
# via djangorestframework-simplejwt
# via
# -r src/backend/requirements.in
# djangorestframework-simplejwt
cssselect2==0.7.0 \
--hash=sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a \
--hash=sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969
@ -327,10 +330,12 @@ diff-match-patch==20230430 \
# via django-import-export
dj-rest-auth==6.0.0 \
--hash=sha256:760b45f3a07cd6182e6a20fe07d0c55230c5f950167df724d7914d0dd8c50133
# via -r src/backend/requirements.in
django==4.2.14 \
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
# via
# -r src/backend/requirements.in
# dj-rest-auth
# django-allauth
# django-allauth-2fa
@ -363,40 +368,53 @@ django==4.2.14 \
# drf-spectacular
django-allauth[openid, saml]==0.63.3 \
--hash=sha256:2374164c468a309e6badf70bc3405136df6036f24a20a13387f2a063066bdaa9
# via django-allauth-2fa
# via
# -r src/backend/requirements.in
# django-allauth-2fa
django-allauth-2fa==0.11.1 \
--hash=sha256:02ffdf1025836f072c2f6ec0964494589cf1d52362f663f9ff6d9ca61a7b6962 \
--hash=sha256:2f2d61dd488f66ad45e59780b061f5abe96caea9c3466e3ee4ea50ea1faebef6
# via -r src/backend/requirements.in
django-cleanup==8.1.0 \
--hash=sha256:70df905076a44e7a111b31198199af633dee08876e199e6dce36ca8dd6b8b10f \
--hash=sha256:7903873ea73b3f7e61e055340d27dba49b70634f60c87a573ad748e172836458
# via -r src/backend/requirements.in
django-cors-headers==4.4.0 \
--hash=sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6 \
--hash=sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2
# via -r src/backend/requirements.in
django-crispy-forms==1.14.0 \
--hash=sha256:35887b8851a931374dd697207a8f56c57a9c5cb9dbf0b9fa54314da5666cea5b \
--hash=sha256:bc4d2037f6de602d39c0bc452ac3029d1f5d65e88458872cc4dbc01c3a400604
# via -r src/backend/requirements.in
django-dbbackup==4.1.0 \
--hash=sha256:c411d38d0f8e60ab3254017278c14ebd75d4001b5634fc73be7fbe8a5260583b \
--hash=sha256:c539b5246b429a22a8efadbab3719ee6b8eda45c66c4ff6592056c590d51c782
# via -r src/backend/requirements.in
django-error-report-2==0.4.2 \
--hash=sha256:1dd99c497af09b7ea99f5fbaf910501838150a9d5390796ea00e187bc62f6c1b \
--hash=sha256:603e1e3b24d01bbfeab6379af948893b2b034031c80fa8b45cf1c4735341c04b
# via -r src/backend/requirements.in
django-filter==24.2 \
--hash=sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e \
--hash=sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48
# via -r src/backend/requirements.in
django-flags==5.0.13 \
--hash=sha256:52df74b86d93f5cb402190ad26b68a5ba0f127e9e016189f1a6f2e8ba3c06a42 \
--hash=sha256:ff6940cf37e07d6d0c4ac28c5420c8cfc478b62541473dba4aa02d600f7db9fc
# via -r src/backend/requirements.in
django-formtools==2.5.1 \
--hash=sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93 \
--hash=sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a
# via -r src/backend/requirements.in
django-ical==1.9.2 \
--hash=sha256:44c9b6fa90d09f25e9ebaa91ed9eb007f079afbc23d6aac909cfc18188a8e90c \
--hash=sha256:74a16bca05735f91a00120cad7250f3c3aa292a9f698a6cfdc544a922c11de70
# via -r src/backend/requirements.in
django-import-export==3.3.9 \
--hash=sha256:16797965e93a8001fe812c61e3b71fb858c57c1bd16da195fe276d6de685348e \
--hash=sha256:dd6cabc08ed6d1bd37a392e7fb542bd7d196b615c800168f5c69f0f55f49b103
# via -r src/backend/requirements.in
django-js-asset==2.2.0 \
--hash=sha256:0c57a82cae2317e83951d956110ce847f58ff0cdc24e314dbc18b35033917e94 \
--hash=sha256:7ef3e858e13d06f10799b56eea62b1e76706f42cf4e709be4e13356bc0ae30d8
@ -404,15 +422,19 @@ django-js-asset==2.2.0 \
django-maintenance-mode==0.21.1 \
--hash=sha256:b79afddb671c59972ae542e4fafbc99117d2d37991843eaaa837e328eed12b1b \
--hash=sha256:c02fff0e386b7f8b2ab54479d3a0d336ae34014da22a7a2365ca96d5a2c1db94
# via -r src/backend/requirements.in
django-markdownify==0.9.5 \
--hash=sha256:2c4ae44e386c209453caf5e9ea1b74f64535985d338ad2d5ad5e7089cc94be86 \
--hash=sha256:34c34eba4a797282a5c5bd97b13cec84d6a4c0673ad47ce1c1d000d74dd8d4ab
# via -r src/backend/requirements.in
django-money==3.2.0 \
--hash=sha256:2e4174b47993780bf4b61ad3fa0a66ebe140da42fdbe68b628c7ba9788287214 \
--hash=sha256:3099f906407175af06b56ef3ff5c250e2fc525ff00f50d42f77b98597e625459
# via -r src/backend/requirements.in
django-mptt==0.16.0 \
--hash=sha256:56c9606bf0b329b5f5afd55dd8bfd073612ea1d5999b10903b09de62bee84c8e \
--hash=sha256:8716849ba3318d94e2e100ed0923a05c1ffdf8195f8472b690dbaf737d2af3b5
# via -r src/backend/requirements.in
django-otp==1.5.0 \
--hash=sha256:e7142139f1e9686be5f396669a3d3d61178cd9b3e9de9de5933888668908b46b \
--hash=sha256:e88871d2d3b333a86c2cd0cb721be8098d4d6344cb220315a500e5a5c8254295
@ -423,9 +445,11 @@ django-picklefield==3.2 \
# via django-q2
django-q-sentry==0.1.6 \
--hash=sha256:9b8b4d7fad253a7d9a47f2c2ab0d9dea83078b7ef45c8849dbb1e4176ef8d050
# via -r src/backend/requirements.in
django-q2==1.6.2 \
--hash=sha256:c2d75552c80b83ca0d8c0b0db7db4f17e9f43ee131a46d0ddd514c5f5fc603cb \
--hash=sha256:cd83c16b5791cd99f83a8d106d2447305d73c6c8ed8ec22c7cb954fe0e814284
# via -r src/backend/requirements.in
django-recurrence==1.11.1 \
--hash=sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5 \
--hash=sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70
@ -433,41 +457,53 @@ django-recurrence==1.11.1 \
django-redis==5.4.0 \
--hash=sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42 \
--hash=sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b
# via -r src/backend/requirements.in
django-sesame==3.2.2 \
--hash=sha256:523ebd4d04e28c897c262f25b78b6fd8f37e11cdca6e277fdc8bf496bd686cf5 \
--hash=sha256:5d753a309166356b6a0d7fc047690943b9e80b4aa7952f1a6400fe6ce60d573c
# via -r src/backend/requirements.in
django-sql-utils==0.7.0 \
--hash=sha256:9371ff28eaf326836a7c52887259123cdd3fbffb7b738e42ae1a21258be0feb6 \
--hash=sha256:fefc40c826896b60fcf33e35b6e30b523fc958955a16006438cd3ba6d795a532
# via -r src/backend/requirements.in
django-sslserver==0.22 \
--hash=sha256:c598a363d2ccdc2421c08ddb3d8b0973f80e8e47a3a5b74e4a2896f21c2947c5
# via -r src/backend/requirements.in
django-stdimage==6.0.2 \
--hash=sha256:880ab14828be56b53f711c3afae83c219ddd5d9af00850626736feb48382bf7f \
--hash=sha256:9a73f7da48c48074580e2b032d5bdb7164935dbe4b9dc4fb88a7e112f3d521c8
# via -r src/backend/requirements.in
django-taggit==5.0.1 \
--hash=sha256:a0ca8a28b03c4b26c2630fd762cb76ec39b5e41abf727a7b66f897a625c5e647 \
--hash=sha256:edcd7db1e0f35c304e082a2f631ddac2e16ef5296029524eb792af7430cab4cc
# via -r src/backend/requirements.in
django-user-sessions==2.0.0 \
--hash=sha256:0965554279f556b47062965609fa08b3ae45bbc581001dbe84b2ea599cc67748 \
--hash=sha256:41b8b1ebeb4736065efbc96437c9cfbf491c39e10fd547a76b98f2312e11fa3e
# via -r src/backend/requirements.in
django-weasyprint==2.3.0 \
--hash=sha256:2f849e15bfd6c1b2a58512097b9042eddf3533651d37d2e096cd6f7d8be6442b \
--hash=sha256:807cb3b16332123d97c8bbe2ac9c70286103fe353235351803ffd33b67284735
# via -r src/backend/requirements.in
django-xforwardedfor-middleware==2.0 \
--hash=sha256:16fd1cb27f33a5541b6f3e0b43afb1b7334a76f27a1255b69e14ec5c440f0b24
# via -r src/backend/requirements.in
djangorestframework==3.14.0 \
--hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \
--hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08
# via
# -r src/backend/requirements.in
# dj-rest-auth
# djangorestframework-simplejwt
# drf-spectacular
djangorestframework-simplejwt[crypto]==5.3.1 \
--hash=sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220 \
--hash=sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae
# via -r src/backend/requirements.in
drf-spectacular==0.27.2 \
--hash=sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981 \
--hash=sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b
# via -r src/backend/requirements.in
dulwich==0.22.1 \
--hash=sha256:0d72a88c7af8fafa14c8743e8923c8d46bd0b850a0b7f5e34eb49201f1ead88e \
--hash=sha256:0ea4c5feedd35e8bde175a9ab91ef6705c3cef5ee209eeb2f67dd0b59ff1825f \
@ -516,6 +552,7 @@ dulwich==0.22.1 \
--hash=sha256:e90b8a2f24149c5803b733a24f1a016a2943b1f5a9ab2360db545e4638354c35 \
--hash=sha256:f9e10678fe0692c5167553981d97cbe342ed055c49016aef10da336e2962b1f2 \
--hash=sha256:fd51e77ff1b4ca08bc9b09b85646a3e77f275827b7b30180d76d769ce608e64d
# via -r src/backend/requirements.in
et-xmlfile==1.1.0 \
--hash=sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c \
--hash=sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada
@ -523,6 +560,7 @@ et-xmlfile==1.1.0 \
feedparser==6.0.11 \
--hash=sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45 \
--hash=sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5
# via -r src/backend/requirements.in
fonttools[woff]==4.53.0 \
--hash=sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d \
--hash=sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64 \
@ -620,10 +658,13 @@ grpcio==1.64.1 \
--hash=sha256:ed6091fa0adcc7e4ff944090cf203a52da35c37a130efa564ded02b7aff63bcd \
--hash=sha256:ee73a2f5ca4ba44fa33b4d7d2c71e2c8a9e9f78d53f6507ad68e7d2ad5f64a22 \
--hash=sha256:f10193c69fc9d3d726e83bbf0f3d316f1847c3071c8c93d8090cf5f326b14309
# via opentelemetry-exporter-otlp-proto-grpc
# via
# -r src/backend/requirements.in
# opentelemetry-exporter-otlp-proto-grpc
gunicorn==22.0.0 \
--hash=sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9 \
--hash=sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63
# via -r src/backend/requirements.in
html5lib==1.1 \
--hash=sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d \
--hash=sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f
@ -893,6 +934,7 @@ opentelemetry-api==1.25.0 \
--hash=sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737 \
--hash=sha256:77c4985f62f2614e42ce77ee4c9da5fa5f0bc1e1821085e9a47533a9323ae869
# via
# -r src/backend/requirements.in
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-instrumentation
@ -905,6 +947,7 @@ opentelemetry-api==1.25.0 \
opentelemetry-exporter-otlp==1.25.0 \
--hash=sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0 \
--hash=sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60
# via -r src/backend/requirements.in
opentelemetry-exporter-otlp-proto-common==1.25.0 \
--hash=sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693 \
--hash=sha256:c93f4e30da4eee02bacd1e004eb82ce4da143a2f8e15b987a9f603e0a85407d3
@ -930,12 +973,15 @@ opentelemetry-instrumentation==0.46b0 \
opentelemetry-instrumentation-django==0.46b0 \
--hash=sha256:cc11b2e24f9bdd20759570390ed8619d9c5acbf788b4a5401e36e280dfc20feb \
--hash=sha256:ecc85941263122f99dbd96463a981b2d1eeea618ca287a58abe0af9fd67631ee
# via -r src/backend/requirements.in
opentelemetry-instrumentation-redis==0.46b0 \
--hash=sha256:8b4639fe52edb6ccdc633c54c01630005ab63faeffd97754cddbf6bdc1f04c5e \
--hash=sha256:e796530808829a9c32f19eaf470f0b01caef13bd89f1d964f536198de881e460
# via -r src/backend/requirements.in
opentelemetry-instrumentation-requests==0.46b0 \
--hash=sha256:a8c2472800d8686f3f286cd524b8746b386154092e85a791ba14110d1acc9b81 \
--hash=sha256:ef0ad63bfd0d52631daaf7d687e763dbd89b465f5cb052f12a4e67e5e3d181e4
# via -r src/backend/requirements.in
opentelemetry-instrumentation-wsgi==0.46b0 \
--hash=sha256:2386014b026f5307c802417eeab74265785ae3dd6eee8c5581a830e3b2d3435b \
--hash=sha256:f4e1001e8477eb546cac7c13cff0b0cf127812b1188a37bcaa3e43eb741451e2
@ -951,6 +997,7 @@ opentelemetry-sdk==1.25.0 \
--hash=sha256:ce7fc319c57707ef5bf8b74fb9f8ebdb8bfafbe11898410e0d2a761d08a98ec7 \
--hash=sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9
# via
# -r src/backend/requirements.in
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-semantic-conventions==0.46b0 \
@ -976,6 +1023,7 @@ packaging==24.1 \
pdf2image==1.17.0 \
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
# via -r src/backend/requirements.in
pillow==10.3.0 \
--hash=sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c \
--hash=sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2 \
@ -1047,6 +1095,7 @@ pillow==10.3.0 \
--hash=sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27 \
--hash=sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a
# via
# -r src/backend/requirements.in
# django-stdimage
# pdf2image
# python-barcode
@ -1055,9 +1104,11 @@ pillow==10.3.0 \
pint==0.21 \
--hash=sha256:3e98bdf01f4dcf840cc0207c0b6f7510d4e0c6288efc1bf470626e875c831172 \
--hash=sha256:998b695e84a34d11702da4a8b9457a39bb5c7ab5ec68db90e948e30878e421f1
# via -r src/backend/requirements.in
pip-licenses==4.4.0 \
--hash=sha256:996817118375445243a34faafe23c06f6b2d250247c4046571b5a6722d45be69 \
--hash=sha256:dbad2ac5a25f574cabe2716f2f031a0c5fa359bed9b3ef615301f4e546893b46
# via -r src/backend/requirements.in
prettytable==3.10.0 \
--hash=sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92 \
--hash=sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568
@ -1104,6 +1155,7 @@ pypng==0.20220715.0 \
python-barcode[images]==0.15.1 \
--hash=sha256:057636fba37369c22852410c8535b36adfbeb965ddfd4e5b6924455d692e0886 \
--hash=sha256:3b1825fbdb11e597466dff4286b4ea9b1e86a57717b59e563ae679726fc854de
# via -r src/backend/requirements.in
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
@ -1113,6 +1165,7 @@ python-dateutil==2.9.0.post0 \
python-dotenv==1.0.1 \
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
# via -r src/backend/requirements.in
python-fsutil==0.14.1 \
--hash=sha256:0d45e623f0f4403f674bdd8ae7aa7d24a4b3132ea45c65416bd2865e6b20b035 \
--hash=sha256:8fb204fa8059f37bdeee8a1dc0fff010170202ea47c4225ee71bb3c26f3997be
@ -1186,12 +1239,15 @@ pyyaml==6.0.1 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
# via
# -r src/backend/requirements.in
# drf-spectacular
# tablib
qrcode[pil]==7.4.2 \
--hash=sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a \
--hash=sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845
# via django-allauth-2fa
# via
# -r src/backend/requirements.in
# django-allauth-2fa
rapidfuzz==3.9.3 \
--hash=sha256:05ee0696ebf0dfe8f7c17f364d70617616afc7dafe366532730ca34056065b8a \
--hash=sha256:0c34139df09a61b1b557ab65782ada971b4a3bce7081d1b2bee45b0a52231adb \
@ -1286,6 +1342,7 @@ rapidfuzz==3.9.3 \
--hash=sha256:f50fed4a9b0c9825ff37cf0bccafd51ff5792090618f7846a7650f21f85579c9 \
--hash=sha256:f57e8305c281e8c8bc720515540e0580355100c0a7a541105c6cafc5de71daae \
--hash=sha256:fd84b7f652a5610733400307dc732f57c4a907080bef9520412e6d9b55bc9adc
# via -r src/backend/requirements.in
redis==5.0.7 \
--hash=sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db \
--hash=sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b
@ -1376,6 +1433,7 @@ regex==2024.4.28 \
--hash=sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2 \
--hash=sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e \
--hash=sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb
# via -r src/backend/requirements.in
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
@ -1485,14 +1543,17 @@ rpds-py==0.18.1 \
# via
# jsonschema
# referencing
sentry-sdk==2.7.0 \
--hash=sha256:d846a211d4a0378b289ced3c434480945f110d0ede00450ba631fc2852e7a0d4 \
--hash=sha256:db9594c27a4d21c1ebad09908b1f0dc808ef65c2b89c1c8e7e455143262e37c1
# via django-q-sentry
setuptools==70.3.0 \
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
sentry-sdk==2.8.0 \
--hash=sha256:6051562d2cfa8087bb8b4b8b79dc44690f8a054762a29c07e22588b1f619bfb5 \
--hash=sha256:aa4314f877d9cd9add5a0c9ba18e3f27f99f7de835ce36bd150e48a41c7c646f
# via
# -r src/backend/requirements.in
# django-q-sentry
setuptools==71.0.3 \
--hash=sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d \
--hash=sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207
# via
# -r src/backend/requirements.in
# django-money
# opentelemetry-instrumentation
sgmllib3k==1.0.0 \
@ -1515,7 +1576,9 @@ sqlparse==0.5.0 \
tablib[html, ods, xls, xlsx, yaml]==3.5.0 \
--hash=sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9 \
--hash=sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33
# via django-import-export
# via
# -r src/backend/requirements.in
# django-import-export
tinycss2==1.2.1 \
--hash=sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847 \
--hash=sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627
@ -1552,7 +1615,9 @@ wcwidth==0.2.13 \
weasyprint==61.2 \
--hash=sha256:47df6cfeeff8c6c28cf2e4caf837cde17715efe462708ada74baa2eb391b6059 \
--hash=sha256:76c6dc0e75e09182d5645d92c66ddf86b1b992c9420235b723fb374b584e5bf4
# via django-weasyprint
# via
# -r src/backend/requirements.in
# django-weasyprint
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
@ -1564,6 +1629,7 @@ webencodings==0.5.1 \
whitenoise==6.7.0 \
--hash=sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636 \
--hash=sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6
# via -r src/backend/requirements.in
wrapt==1.16.0 \
--hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \
--hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \

View File

@ -52,7 +52,7 @@
"dayjs": "^1.11.10",
"embla-carousel-react": "^8.1.6",
"html5-qrcode": "^2.3.8",
"mantine-datatable": "^7.11.1",
"mantine-datatable": "^7.11.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-grid-layout": "^1.4.4",

View File

@ -31,7 +31,7 @@ export function DashboardItemProxy({
queryFn: fetchData,
refetchOnWindowFocus: autoupdate
});
const [dashdata, setDashData] = useState({ title: t`Title`, value: '000' });
const [dashData, setDashData] = useState({ title: t`Title`, value: '000' });
useEffect(() => {
if (data) {
@ -44,7 +44,7 @@ export function DashboardItemProxy({
<div key={id}>
<StatisticItem
id={id}
data={dashdata}
data={dashData}
isLoading={isLoading || isFetching}
/>
</div>

View File

@ -1,11 +1,8 @@
import { t } from '@lingui/macro';
import { IconUserStar } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation';
import { base_url } from '../../main';
import { useLocalState } from '../../states/LocalState';
import { useUserState } from '../../states/UserState';
import { ModelInformationDict } from '../render/ModelType';

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { notifications } from '@mantine/notifications';
import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';

View File

@ -85,7 +85,7 @@ function UploadModal({
apiPath: string;
setImage: (image: string) => void;
}) {
const [file1, setFile] = useState<FileWithPath | null>(null);
const [currentFile, setCurrentFile] = useState<FileWithPath | null>(null);
let uploading = false;
// Components to show in the Dropzone when no file is selected
@ -168,7 +168,7 @@ function UploadModal({
return (
<Paper style={{ height: '220px' }}>
<Dropzone
onDrop={(files) => setFile(files[0])}
onDrop={(files) => setCurrentFile(files[0])}
maxFiles={1}
accept={IMAGE_MIME_TYPE}
loading={uploading}
@ -198,7 +198,9 @@ function UploadModal({
}}
/>
</Dropzone.Reject>
<Dropzone.Idle>{file1 ? fileInfo(file1) : noFileIdle}</Dropzone.Idle>
<Dropzone.Idle>
{currentFile ? fileInfo(currentFile) : noFileIdle}
</Dropzone.Idle>
</Group>
</Dropzone>
<Paper
@ -218,12 +220,15 @@ function UploadModal({
>
<Button
variant="outline"
disabled={!file1}
onClick={() => setFile(null)}
disabled={!currentFile}
onClick={() => setCurrentFile(null)}
>
<Trans>Clear</Trans>
</Button>
<Button disabled={!file1} onClick={() => uploadImage(file1)}>
<Button
disabled={!currentFile}
onClick={() => uploadImage(currentFile)}
>
<Trans>Submit</Trans>
</Button>
</Paper>
@ -354,31 +359,27 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
};
return (
<>
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos="relative">
<>
<ApiImage
src={img}
mah={IMAGE_DIMENSION}
maw={IMAGE_DIMENSION}
onClick={expandImage}
/>
{permissions.hasChangeRole(props.appRole) &&
hasOverlay &&
hovered && (
<Overlay color="black" opacity={0.8} onClick={expandImage}>
<ImageActionButtons
visible={hovered}
actions={props.imageActions}
apiPath={props.apiPath}
hasImage={props.src ? true : false}
pk={props.pk}
setImage={setAndRefresh}
/>
</Overlay>
)}
</>
</AspectRatio>
</>
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos="relative">
<>
<ApiImage
src={img}
mah={IMAGE_DIMENSION}
maw={IMAGE_DIMENSION}
onClick={expandImage}
/>
{permissions.hasChangeRole(props.appRole) && hasOverlay && hovered && (
<Overlay color="black" opacity={0.8} onClick={expandImage}>
<ImageActionButtons
visible={hovered}
actions={props.imageActions}
apiPath={props.apiPath}
hasImage={props.src ? true : false}
pk={props.pk}
setImage={setAndRefresh}
/>
</Overlay>
)}
</>
</AspectRatio>
);
}

View File

@ -168,7 +168,7 @@ export default function NotesEditor({
id: 'notes'
});
});
}, [noteUrl, ref.current]);
}, [api, noteUrl, ref.current]);
const plugins: any[] = useMemo(() => {
let plg = [

View File

@ -81,7 +81,9 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
<Trans>Preview not available, click "Reload Preview".</Trans>
</div>
)}
{pdfUrl && <iframe src={pdfUrl} width="100%" height="100%" />}
{pdfUrl && (
<iframe src={pdfUrl} width="100%" height="100%" title="PDF Preview" />
)}
</>
);
}

View File

@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro';
import {
ActionIcon,
Alert,
Button,
Card,
Center,

View File

@ -139,6 +139,7 @@ export function OptionsApiForm({
if (!props.ignorePermissionCheck) {
fields = extractAvailableFields(response, props.method);
}
return fields;
},
throwOnError: (error: any) => {
@ -183,7 +184,7 @@ export function OptionsApiForm({
<ApiForm
id={id}
props={formProps}
optionsLoading={optionsQuery.isFetching}
optionsLoading={optionsQuery.isFetching || !optionsQuery.data}
/>
);
}
@ -206,9 +207,6 @@ export function ApiForm({
const [fields, setFields] = useState<ApiFormFieldSet>(
() => props.fields ?? {}
);
useEffect(() => {
setFields(props.fields ?? {});
}, [props.fields]);
const defaultValues: FieldValues = useMemo(() => {
let defaultValuesMap = mapFields(fields ?? {}, (_path, field) => {
@ -251,6 +249,31 @@ export function ApiForm({
[props.url, props.pk, props.pathParams]
);
// Define function to process API response
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
const res: NestedDict = {};
for (const [k, field] of Object.entries(fields)) {
const dataValue = data[k];
if (
field.field_type === 'nested object' &&
field.children &&
typeof dataValue === 'object'
) {
res[k] = processFields(field.children, dataValue);
} else {
res[k] = dataValue;
if (field.onValueChange) {
field.onValueChange(dataValue, data);
}
}
}
return res;
};
// Query manager for retrieving initial data from the server
const initialDataQuery = useQuery({
enabled: false,
@ -263,66 +286,51 @@ export function ApiForm({
props.pathParams
],
queryFn: async () => {
try {
// Await API call
let response = await api.get(url);
return await api
.get(url)
.then((response: any) => {
// Process API response
const fetchedData: any = processFields(fields, response.data);
// Define function to process API response
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
const res: NestedDict = {};
// TODO: replace with .map()
for (const [k, field] of Object.entries(fields)) {
const dataValue = data[k];
if (
field.field_type === 'nested object' &&
field.children &&
typeof dataValue === 'object'
) {
res[k] = processFields(field.children, dataValue);
} else {
res[k] = dataValue;
if (field.onValueChange) {
field.onValueChange(dataValue, data);
}
}
}
return res;
};
// Process API response
const initialData: any = processFields(fields, response.data);
// Update form values, but only for the fields specified for this form
form.reset(initialData);
// Update the field references, too
Object.keys(fields).forEach((fieldName) => {
if (fieldName in initialData) {
let field = fields[fieldName] ?? {};
fields[fieldName] = {
...field,
value: initialData[fieldName]
};
}
// Update form values, but only for the fields specified for this form
form.reset(fetchedData);
return fetchedData;
})
.catch(() => {
return {};
});
return response;
} catch (error) {
console.error('ERR: Error fetching initial data:', error);
// Re-throw error to allow react-query to handle error
throw error;
}
}
});
useEffect(() => {
let _fields: any = props.fields || {};
let _initialData: any = props.initialData || {};
let _fetchedData: any = initialDataQuery.data || {};
for (const k of Object.keys(_fields)) {
// Ensure default values override initial field spec
if (defaultValues[k]) {
_fields[k].value = defaultValues[k];
}
// Ensure initial data overrides default values
if (_initialData && _initialData[k]) {
_fields[k].value = _initialData[k];
}
// Ensure fetched data overrides also
if (_fetchedData && _fetchedData[k]) {
_fields[k].value = _fetchedData[k];
}
}
setFields(_fields);
}, [props.fields, props.initialData, defaultValues, initialDataQuery.data]);
// Fetch initial data on form load
useEffect(() => {
// Fetch initial data if the fetchInitialData property is set
if (props.fetchInitialData) {
if (!optionsLoading && props.fetchInitialData) {
queryClient.removeQueries({
queryKey: [
'form-initial-data',
@ -335,22 +343,16 @@ export function ApiForm({
});
initialDataQuery.refetch();
}
}, [props.fetchInitialData]);
}, [props.fetchInitialData, optionsLoading]);
const isLoading = useMemo(
const isLoading: boolean = useMemo(
() =>
isFormLoading ||
initialDataQuery.isFetching ||
optionsLoading ||
isSubmitting ||
!fields,
[
isFormLoading,
initialDataQuery.isFetching,
isSubmitting,
fields,
optionsLoading
]
[isFormLoading, initialDataQuery, isSubmitting, fields, optionsLoading]
);
const [initialFocus, setInitialFocus] = useState<string>('');
@ -370,7 +372,7 @@ export function ApiForm({
});
}
if (isLoading || initialFocus == focusField) {
if (isLoading) {
return;
}
@ -522,6 +524,14 @@ export function ApiForm({
props.onFormError?.();
}, [props.onFormError]);
if (optionsLoading || initialDataQuery.isFetching) {
return (
<Paper mah={'65vh'}>
<LoadingOverlay visible zIndex={1010} />
</Paper>
);
}
return (
<Stack>
<Boundary label={`ApiForm-${id}`}>
@ -535,13 +545,15 @@ export function ApiForm({
{/* Form Fields */}
<Stack gap="sm">
{(!isValid || nonFieldErrors.length > 0) && (
<Alert radius="sm" color="red" title={t`Error`}>
{nonFieldErrors.length > 0 && (
<Alert radius="sm" color="red" title={t`Form Error`}>
{nonFieldErrors.length > 0 ? (
<Stack gap="xs">
{nonFieldErrors.map((message) => (
<Text key={message}>{message}</Text>
))}
</Stack>
) : (
<Text>{t`Errors exist for one or more form fields`}</Text>
)}
</Alert>
)}
@ -561,8 +573,8 @@ export function ApiForm({
<Boundary label={`ApiForm-${id}-FormContent`}>
<FormProvider {...form}>
<Stack gap="xs">
{!optionsLoading &&
Object.entries(fields).map(([fieldName, field]) => (
{Object.entries(fields).map(([fieldName, field]) => {
return (
<ApiFormField
key={fieldName}
fieldName={fieldName}
@ -571,7 +583,8 @@ export function ApiForm({
url={url}
setFields={setFields}
/>
))}
);
})}
</Stack>
</FormProvider>
</Boundary>

View File

@ -1,15 +1,7 @@
import { t } from '@lingui/macro';
import {
Alert,
FileInput,
NumberInput,
Stack,
Switch,
TextInput
} from '@mantine/core';
import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form';
import { useId } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react';
import { ReactNode, useCallback, useEffect, useMemo } from 'react';
import { Control, FieldValues, useController } from 'react-hook-form';
@ -149,7 +141,7 @@ export function ApiFormField({
label: hideLabels ? undefined : definition.label,
description: hideLabels ? undefined : definition.description
};
}, [definition]);
}, [hideLabels, definition]);
// pull out onValueChange as this can cause strange errors when passing the
// definition to the input components via spread syntax
@ -202,7 +194,7 @@ export function ApiFormField({
}
return val;
}, [value]);
}, [definition.field_type, value]);
// Coerce the value to a (stringified) boolean value
const booleanValue: boolean = useMemo(() => {

View File

@ -1,6 +1,6 @@
import { Select } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { ApiFormFieldType } from './ApiFormField';
@ -10,7 +10,8 @@ import { ApiFormFieldType } from './ApiFormField';
*/
export function ChoiceField({
controller,
definition
definition,
fieldName
}: {
controller: UseControllerReturn<FieldValues, any>;
definition: ApiFormFieldType;
@ -23,6 +24,8 @@ export function ChoiceField({
fieldState: { error }
} = controller;
const { value } = field;
// Build a set of choices for the field
const choices: any[] = useMemo(() => {
let choices = definition.choices ?? [];
@ -48,6 +51,14 @@ export function ChoiceField({
[field.onChange, definition]
);
const choiceValue = useMemo(() => {
if (!value) {
return '';
} else {
return value.toString();
}
}, [value]);
return (
<Select
id={fieldId}
@ -57,7 +68,7 @@ export function ChoiceField({
{...field}
onChange={onChange}
data={choices}
value={field.value}
value={choiceValue}
label={definition.label}
description={definition.description}
placeholder={definition.placeholder}

View File

@ -1,8 +1,10 @@
import { Trans, t } from '@lingui/macro';
import { Container, Flex, Group, Table } from '@mantine/core';
import { Container, Group, Table } from '@mantine/core';
import { useEffect, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form';
import { InvenTreeIcon } from '../../../functions/icons';
import { StandaloneField } from '../StandaloneField';
import { ApiFormFieldType } from './ApiFormField';
export function TableField({
@ -83,23 +85,51 @@ export function TableField({
/*
* Display an "extra" row below the main table row, for additional information.
* - Each "row" can display an extra row of information below the main row
*/
export function TableFieldExtraRow({
visible,
content,
colSpan
fieldDefinition,
defaultValue,
emptyValue,
onValueChange
}: {
visible: boolean;
content: React.ReactNode;
colSpan?: number;
fieldDefinition: ApiFormFieldType;
defaultValue?: any;
emptyValue?: any;
onValueChange: (value: any) => void;
}) {
// Callback whenever the visibility of the sub-field changes
useEffect(() => {
if (!visible) {
// If the sub-field is hidden, reset the value to the "empty" value
onValueChange(emptyValue);
}
}, [visible]);
const field: ApiFormFieldType = useMemo(() => {
return {
...fieldDefinition,
default: defaultValue,
onValueChange: (value: any) => {
onValueChange(value);
}
};
}, [fieldDefinition]);
return (
visible && (
<Table.Tr>
<Table.Td colSpan={colSpan ?? 3}>
<Group justify="flex-start" grow>
<InvenTreeIcon icon="downright" />
{content}
<Table.Td colSpan={10}>
<Group grow preventGrowOverflow={false} justify="flex-apart" p="xs">
<Container flex={0} p="xs">
<InvenTreeIcon icon="downright" />
</Container>
<StandaloneField
fieldDefinition={field}
defaultValue={defaultValue}
/>
</Group>
</Table.Td>
</Table.Tr>

View File

@ -56,6 +56,9 @@ export default function TextField({
error={error?.message}
radius="sm"
onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => {
onChange(event.currentTarget.value);
}}
rightSection={
value && !definition.required ? (
<IconX size="1rem" color="red" onClick={() => onTextChange('')} />

View File

@ -138,16 +138,27 @@ export default function ImporterDataSelector({
// Find the field definition in session.availableFields
let fieldDef = session.availableFields[field];
if (fieldDef) {
// Construct field filters based on session field filters
let filters = fieldDef.filters ?? {};
if (session.fieldFilters[field]) {
filters = {
...filters,
...session.fieldFilters[field]
};
}
fields[field] = {
...fieldDef,
field_type: fieldDef.type,
description: fieldDef.help_text
description: fieldDef.help_text,
filters: filters
};
}
}
return fields;
}, [selectedFieldNames, session.availableFields]);
}, [selectedFieldNames, session.availableFields, session.fieldFilters]);
const importData = useCallback(
(rows: number[]) => {

View File

@ -29,18 +29,16 @@ export default function ImporterImportProgress({
}, []);
return (
<>
<Center>
<Container>
<Stack gap="xs">
<StylishText size="lg">{t`Importing Records`}</StylishText>
<Loader />
<Text size="lg">
{t`Imported rows`}: {session.sessionData.row_count}
</Text>
</Stack>
</Container>
</Center>
</>
<Center>
<Container>
<Stack gap="xs">
<StylishText size="lg">{t`Importing Records`}</StylishText>
<Loader />
<Text size="lg">
{t`Imported rows`}: {session.sessionData.row_count}
</Text>
</Stack>
</Container>
</Center>
);
}

View File

@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro';
import { Carousel } from '@mantine/carousel';
import { Anchor, Button, Paper, Text, Title, rem } from '@mantine/core';
import { Anchor, Button, Paper, Text, Title } from '@mantine/core';
import { DocumentationLinkItem } from './DocumentationLinks';
import * as classes from './GettingStartedCarousel.css';

View File

@ -35,7 +35,7 @@ export function QrCodeModal({
key: 'camId',
defaultValue: null
});
const [ScanningEnabled, setIsScanning] = useState<boolean>(false);
const [scanningEnabled, setScanningEnabled] = useState<boolean>(false);
const [wasAutoPaused, setWasAutoPaused] = useState<boolean>(false);
const documentState = useDocumentVisibility();
@ -48,7 +48,7 @@ export function QrCodeModal({
// Stop/star when leaving or reentering page
useEffect(() => {
if (ScanningEnabled && documentState === 'hidden') {
if (scanningEnabled && documentState === 'hidden') {
stopScanning();
setWasAutoPaused(true);
} else if (wasAutoPaused && documentState === 'visible') {
@ -128,12 +128,12 @@ export function QrCodeModal({
icon: <IconX />
});
});
setIsScanning(true);
setScanningEnabled(true);
}
}
function stopScanning() {
if (qrCodeScanner && ScanningEnabled) {
if (qrCodeScanner && scanningEnabled) {
qrCodeScanner.stop().catch((err: string) => {
showNotification({
title: t`Error while stopping`,
@ -142,7 +142,7 @@ export function QrCodeModal({
icon: <IconX />
});
});
setIsScanning(false);
setScanningEnabled(false);
}
}
@ -151,7 +151,7 @@ export function QrCodeModal({
<Group>
<Text size="sm">{camId?.label}</Text>
<Space style={{ flex: 1 }} />
<Badge>{ScanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
<Badge>{scanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
</Group>
<Container px={0} id="reader" w={'100%'} mih="300px" />
{!camId ? (
@ -164,14 +164,14 @@ export function QrCodeModal({
<Button
style={{ flex: 1 }}
onClick={() => startScanning()}
disabled={camId != undefined && ScanningEnabled}
disabled={camId != undefined && scanningEnabled}
>
<Trans>Start scanning</Trans>
</Button>
<Button
style={{ flex: 1 }}
onClick={() => stopScanning()}
disabled={!ScanningEnabled}
disabled={!scanningEnabled}
>
<Trans>Stop scanning</Trans>
</Button>

View File

@ -2,7 +2,7 @@ import { ActionIcon, Container, Group, Indicator, Tabs } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconBell, IconSearch } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { useMatch, useNavigate } from 'react-router-dom';
import { api } from '../../App';
@ -132,10 +132,35 @@ export function Header() {
}
function NavTabs() {
const user = useUserState();
const navigate = useNavigate();
const match = useMatch(':tabName/*');
const tabValue = match?.params.tabName;
const tabs: ReactNode[] = useMemo(() => {
let _tabs: ReactNode[] = [];
mainNavTabs.forEach((tab) => {
if (tab.role && !user.hasViewRole(tab.role)) {
return;
}
_tabs.push(
<Tabs.Tab
value={tab.name}
key={tab.name}
onClick={(event: any) =>
navigateToLink(`/${tab.name}`, navigate, event)
}
>
{tab.text}
</Tabs.Tab>
);
});
return _tabs;
}, [mainNavTabs, user]);
return (
<Tabs
defaultValue="home"
@ -146,19 +171,7 @@ function NavTabs() {
}}
value={tabValue}
>
<Tabs.List>
{mainNavTabs.map((tab) => (
<Tabs.Tab
value={tab.name}
key={tab.name}
onClick={(event: any) =>
navigateToLink(`/${tab.name}`, navigate, event)
}
>
{tab.text}
</Tabs.Tab>
))}
</Tabs.List>
<Tabs.List>{tabs.map((tab) => tab)}</Tabs.List>
</Tabs>
);
}

View File

@ -7,7 +7,6 @@ import {
Drawer,
Group,
Loader,
LoadingOverlay,
Space,
Stack,
Text,

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro';
import { Alert, Anchor, Group, Skeleton, Space, Text } from '@mantine/core';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { ReactNode, useCallback } from 'react';
import { api } from '../../App';
@ -71,7 +71,7 @@ const RendererLookup: EnumDictionary<
[ModelType.parttesttemplate]: RenderPartTestTemplate,
[ModelType.projectcode]: RenderProjectCode,
[ModelType.purchaseorder]: RenderPurchaseOrder,
[ModelType.purchaseorderline]: RenderPurchaseOrder,
[ModelType.purchaseorderlineitem]: RenderPurchaseOrder,
[ModelType.returnorder]: RenderReturnOrder,
[ModelType.salesorder]: RenderSalesOrder,
[ModelType.salesordershipment]: RenderSalesOrderShipment,
@ -180,7 +180,7 @@ export function RenderInlineModel({
return (
<Group gap="xs" justify="space-between" wrap="nowrap">
<Group gap="xs" justify="left" wrap="nowrap">
{image && Thumbnail({ src: image, size: 18 })}
{image && <Thumbnail src={image} size={18} />}
{url ? (
<Anchor href={url} onClick={(event: any) => onClick(event)}>
<Text size="sm">{primary}</Text>

Some files were not shown because too many files have changed in this diff Show More