Compare commits

...

33 Commits

Author SHA1 Message Date
Oliver
23a9485e7e Update version.py (#7705)
Bump version number to 0.15.6
2024-07-23 09:12:30 +10:00
Oliver
19924cac60 Update SSO.md (#7706)
Fix docs links
2024-07-22 20:20:53 +10:00
github-actions[bot]
c1d9732e7c Cast width / height to integer (#7676) (#7677)
* Cast width / height to integer

* Convert "rotate" parameter

(cherry picked from commit ecec81ead0)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-07-18 12:47:44 +10:00
github-actions[bot]
68e2f08fa5 Added onInput event for fields in forms - fix for issue #7600 (#7660) (#7664)
An onInput event is added for fields in forms that gets triggered everytime an input is detected in the field

(cherry picked from commit a3103cf568)

Co-authored-by: Roche Christopher <rocheinside@gmail.com>
2024-07-16 12:13:45 +10:00
Matthias Mair
cc4535748e backport of https://github.com/inventree/InvenTree/pull/7620 (#7627) 2024-07-12 09:08:58 +10:00
github-actions[bot]
2329179070 Parameter value bug (#7601) (#7602)
* Handle out-of range numerical values

* Add unit test

(cherry picked from commit 84d076848a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-07-09 21:49:15 +10:00
github-actions[bot]
50fdefa473 Fix import widget type (#7535) (#7536)
(cherry picked from commit 3b3352119f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-29 20:50:02 +10:00
github-actions[bot]
1f522f47a5 Plugin load fix (#7505) (#7507)
* Cast setting to int

* Prevent single faulty plugin from killing *all* plugins

* Handle specific errors on _load_plugins

* Update unit test

(cherry picked from commit da42fdf06e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-25 12:52:41 +10:00
github-actions[bot]
b17c835218 Add "showmigrations" task to invoke (#7482) (#7484)
- Helpful for debugging user installs

(cherry picked from commit 442f2594d0)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-20 21:22:36 +10:00
github-actions[bot]
91c5843425 Fix fields for PurchaseOrderCancelSerializer (#7481) (#7483)
- Throwing an error on an OPTIONS request

(cherry picked from commit 758871b8a9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-20 20:56:13 +10:00
Oliver
b57f53c4cf Update version.py (#7458)
Bump version number to 0.15.5
2024-06-17 20:34:10 +10:00
github-actions[bot]
fa1a9da23a Improve stock item tracking API query (#7451) (#7453)
* Improve stock item tracking API query

- Cache related model lookups into single DB queries
- Significant improvements to query speed
- Ref: https://github.com/inventree/InvenTree/issues/7429

* Handle case where item does not exist in DB

(cherry picked from commit 79ea6897ea)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-16 18:54:11 +10:00
github-actions[bot]
fe09437214 Fix for gunicorn command (#7450) (#7452)
* Fix for gunicorn command

* Allow override of worker count

(cherry picked from commit 49f6981f46)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-06-16 16:25:06 +10:00
github-actions[bot]
f1dfced89b fix: Add installer check for python version (#7440) (#7441)
* fix: Add installer check for python version

Closes #7439
Closes #7437
Closes #7377

* fix error message

* Add color

(cherry picked from commit 960c27b142)

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-06-14 08:33:55 +10:00
github-actions[bot]
d7c76aab9d Update link for mobile app docs (#7378) (#7381)
(cherry picked from commit 74f4b85dfd)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-31 17:07:47 +10:00
github-actions[bot]
dfdaddbc7e Catch edge case for merge_stock_items: (#7373) (#7374)
* Catch edge case for merge_stock_items:

- Use current location as backup
- Handle null location

* Fix deltas

(cherry picked from commit 9fa2735f7a)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-30 20:13:28 +10:00
github-actions[bot]
1f6e52138a Plugin reload fix (#7361) (#7362)
- call registry.check_reload when registering an event
- ensure that latest versions of plugins are loaded

(cherry picked from commit 5577a086c9)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-28 01:10:36 +10:00
Oliver
e8c9ec076c Update version.py (#7353)
Bump version to 0.15.4
2024-05-27 20:16:07 +10:00
Oliver
d4d9aa9d1b Fixes for installer (#7344) (#7350)
* - move reqs file to contrib
- detect previously used python version
- safe extra requirements to INSTALLER_EXTRA

* add missing fi

* move site setting

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-05-27 19:45:38 +10:00
github-actions[bot]
54f2072e97 Fix for 'restore' command (#7348) (#7349)
- Fix typo

(cherry picked from commit bda237a13f)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-27 17:11:55 +10:00
github-actions[bot]
d1042cde0e Update docs (#7339) (#7340)
- Add note about permission denied error

(cherry picked from commit 5f9348f56d)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-26 21:50:11 +10:00
github-actions[bot]
5f4275679d PUI: Don't load stock test results for non-trackable part (#7327) (#7337)
(cherry picked from commit e8e64616da)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-26 20:54:45 +10:00
Oliver
1ba0bee1ea Update version.py (#7328)
Bump version number to 0.15.3
2024-05-26 20:50:45 +10:00
Oliver
ea7aa93a28 Merge pull request from GHSA-2crp-q9pc-457j (#7320)
* Merge pull request from GHSA-2crp-q9pc-457j

* ensure API login only works if mfa is not required

* add migration to log out users

* add migration to clear users

* Use `UV_SYSTEM_PYTHON` to allow the system Python interpreter instead of `VIRTUAL_ENV` (#7317)

* Fix docs links - pin to same branch

* Handle exception on migration

* Make migration non-atomic

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-05-24 23:36:00 +10:00
github-actions[bot]
9eccf69456 Add Meta subclass for build serializers (#7315) (#7316)
Ref: https://github.com/inventree/InvenTree/discussions/7314
(cherry picked from commit 0d46af7a74)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-24 09:21:40 +10:00
github-actions[bot]
9cebfa85df Add clearer error message for invalid SITE_URL (#7311) (#7312)
(cherry picked from commit 2fafb7f21c)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-23 23:43:54 +10:00
github-actions[bot]
af3cf62b8e fix: SELinux labels for Caddyfile (#7261) (#7262)
(cherry picked from commit b26640fb36)

Co-authored-by: Philipp Fruck <dev@p-fruck.de>
2024-05-20 09:14:57 +10:00
Oliver
f20a1245e7 Update version.py (#7252)
Bump version number to 0.15.2
2024-05-17 13:45:45 +10:00
github-actions[bot]
92a4989a8d Fix for email template (#7249) (#7251)
- Use `line.part` instead of `part`

(cherry picked from commit 2431fc6d58)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-17 13:44:32 +10:00
github-actions[bot]
be3b22ce36 Docker fix (#7228) (#7229)
* Copy requirements file

* Test more files when building docker image

* Refactor install task

* Raise exception

* Run install task

* Fix typos

- The tests work!

(cherry picked from commit 2265055785)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-15 09:24:22 +10:00
Oliver
258b8e4ecc Update version.py
Bump version to 0.15.1
2024-05-15 09:20:13 +10:00
github-actions[bot]
7df92aad03 Fix permissions for release.yaml (#7220) (#7221)
* Fix permissions for release.yaml

- 0.15.0 release currently borked

* Move permissions to individual job targets

(cherry picked from commit 3eae5096e3)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-05-14 22:11:39 +10:00
Oliver
2dac705779 Mark as release version (#7217) 2024-05-14 21:45:42 +10:00
55 changed files with 491 additions and 300 deletions

View File

@@ -49,9 +49,10 @@ runs:
shell: bash
run: |
python3 -m pip install -U pip
pip3 install invoke wheel uv
- name: Set the VIRTUAL_ENV variable for uv to work
run: echo "VIRTUAL_ENV=${Python_ROOT_DIR}" >> $GITHUB_ENV
pip3 install -U invoke wheel
pip3 install 'uv<0.3.0'
- name: Allow uv to use the system Python by default
run: echo "UV_SYSTEM_PYTHON=1" >> $GITHUB_ENV
shell: bash
- name: Install Specific Python Dependencies
if: ${{ inputs.pip-dependency }}

View File

@@ -73,7 +73,7 @@ jobs:
python-version: ${{ env.python_version }}
- name: Version Check
run: |
pip install --require-hashes -r .github/requirements.txt
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
@@ -85,12 +85,17 @@ jobs:
docker run --rm inventree-test invoke --list
docker run --rm inventree-test gunicorn --version
docker run --rm inventree-test pg_dump --version
docker run --rm inventree-test test -f /home/inventree/init.sh
docker run --rm inventree-test test -f /home/inventree/tasks.py
docker run --rm inventree-test test -f /home/inventree/gunicorn.conf.py
docker run --rm inventree-test test -f /home/inventree/src/backend/requirements.txt
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/manage.py
- name: Build Docker Image
# Build the development docker image (using docker-compose.yml)
run: docker compose --project-directory . -f contrib/container/dev-docker-compose.yml build --no-cache
- name: Update Docker Image
run: |
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke install
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke update
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke setup-dev
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml up -d

View File

@@ -104,7 +104,7 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # pin@v3.0.1
- name: Check Version
run: |
pip install --require-hashes -r .github/requirements.txt
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py
mkdocs:
@@ -122,7 +122,7 @@ jobs:
python-version: ${{ env.python_version }}
- name: Check Config
run: |
pip install --require-hashes -r .github/requirements.txt
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
pip install --require-hashes -r docs/requirements.txt
python docs/ci/check_mkdocs_config.py
- name: Check Links
@@ -168,7 +168,7 @@ jobs:
- name: Download public schema
if: needs.paths-filter.outputs.api == 'false'
run: |
pip install --require-hashes -r .github/requirements.txt >/dev/null 2>&1
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
echo "Version: $version"
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
@@ -187,7 +187,7 @@ jobs:
id: version
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
run: |
pip install --require-hashes -r .github/requirements.txt >/dev/null 2>&1
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
echo "Version: $version"
echo "version=$version" >> "$GITHUB_OUTPUT"

View File

@@ -5,12 +5,12 @@ on:
release:
types: [published]
permissions:
contents: read
jobs:
stable:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # pin@v4.1.5
- name: Version Check
run: |
pip install --require-hashes -r .github/requirements.txt
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py
- name: Push to Stable Branch
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
@@ -30,6 +30,9 @@ jobs:
publish-build:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # pin@v4.1.5
- name: Environment Setup

View File

@@ -39,8 +39,8 @@ repos:
files: src/backend/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [.github/requirements.in, -o, .github/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
files: .github/requirements\.(in|txt)$
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt,--python-version=3.9, --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,--python-version=3.9, --no-strip-extras, --generate-hashes]

View File

@@ -123,7 +123,7 @@ Refer to the [getting started guide](https://docs.inventree.org/en/latest/start/
<!-- Mobile App -->
## :iphone: Mobile App
InvenTree is supported by a [companion mobile app](https://docs.inventree.org/en/latest/app/app/) which allows users access to stock control information and functionality.
InvenTree is supported by a [companion mobile app](https://docs.inventree.org/app/) which allows users access to stock control information and functionality.
<div align="center"><h4>
<a href="https://play.google.com/store/apps/details?id=inventree.inventree_app">Android Play Store</a>

View File

@@ -128,6 +128,7 @@ COPY --from=prebuild /root/.local /root/.local
# Copy source code
COPY src/backend/InvenTree ${INVENTREE_BACKEND_DIR}/InvenTree
COPY src/backend/requirements.txt ${INVENTREE_BACKEND_DIR}/requirements.txt
COPY --from=frontend ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web
# Launch the production server

View File

@@ -114,7 +114,7 @@ services:
env_file:
- .env
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./Caddyfile:/etc/caddy/Caddyfile:ro,z
- ${INVENTREE_EXT_VOLUME}/static:/var/www/static:z
- ${INVENTREE_EXT_VOLUME}/media:/var/www/media:z
- ${INVENTREE_EXT_VOLUME}:/var/log:z

View File

@@ -4,9 +4,9 @@ asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
# via django
django==4.2.11 \
--hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \
--hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3
django==4.2.14 \
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
# via django-auth-ldap
django-auth-ldap==4.8.0 \
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \

View File

@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile .github/requirements.in -o .github/requirements.txt --python-version=3.9 --no-strip-extras --generate-hashes
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt --python-version=3.9 --no-strip-extras --generate-hashes
certifi==2024.2.2 \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1

View File

@@ -2,6 +2,8 @@
#
# packager.io postinstall script functions
#
Color_Off='\033[0m'
On_Red='\033[41m'
function detect_docker() {
if [ -n "$(grep docker </proc/1/cgroup)" ]; then
@@ -44,6 +46,28 @@ function detect_ip() {
echo "IP address is ${INVENTREE_IP}"
}
function detect_python() {
# Detect if there is already a python version installed in /opt/inventree/env/lib
if test -f "${APP_HOME}/env/bin/python"; then
echo "# Python environment already present"
# Extract earliest python version initialised from /opt/inventree/env/lib
SETUP_PYTHON=$(ls -1 ${APP_HOME}/env/bin/python* | sort | head -n 1)
echo "# Found earlier used version: ${SETUP_PYTHON}"
else
echo "# No python environment found - using environment variable: ${SETUP_PYTHON}"
fi
# Ensure python can be executed - abort if not
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "${On_Red}"
echo "# Python ${SETUP_PYTHON} not found - aborting!"
echo "# Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
echo "# If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
echo "${Color_Off}"
exit 1
fi
}
function get_env() {
envname=$1
@@ -90,7 +114,7 @@ function detect_envs() {
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
# Install parser
pip install --require-hashes -r ${APP_HOME}/.github/requirements.txt -q
pip install --require-hashes -r ${APP_HOME}/contrib/dev_reqs/requirements.txt -q
# Load config
local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
@@ -163,12 +187,20 @@ function create_initscripts() {
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && ${SETUP_PYTHON} -m venv env"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install invoke wheel"
# Check INSTALLER_EXTRA exists and load it
if test -f "${APP_HOME}/INSTALLER_EXTRA"; then
echo "# Loading extra packages from INSTALLER_EXTRA"
source ${APP_HOME}/INSTALLER_EXTRA
fi
if [ -n "${SETUP_EXTRA_PIP}" ]; then
echo "# Installing extra pip packages"
if [ -n "${SETUP_DEBUG}" ]; then
echo "# Extra pip packages: ${SETUP_EXTRA_PIP}"
fi
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install ${SETUP_EXTRA_PIP}"
# Write extra packages to INSTALLER_EXTRA
echo "SETUP_EXTRA_PIP='${SETUP_EXTRA_PIP}'" >>${APP_HOME}/INSTALLER_EXTRA
fi
fi
@@ -283,6 +315,20 @@ function set_env() {
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
}
function set_site() {
# Ensure IP is known
if [ -z "${INVENTREE_IP}" ]; then
echo "# No IP address found - skipping"
return
fi
# Check if INVENTREE_SITE_URL in inventree config
if [ -z "$(inventree config:get INVENTREE_SITE_URL)" ]; then
echo "# Setting up InvenTree site URL"
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
fi
}
function final_message() {
echo -e "####################################################################################"
echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in"

View File

@@ -33,6 +33,7 @@ detect_envs
detect_docker
detect_initcmd
detect_ip
detect_python
# create processes
create_initscripts
@@ -45,6 +46,7 @@ update_or_install
if [ "${SETUP_CONF_LOADED}" = "true" ]; then
set_env
fi
set_site
start_inventree
# show info

View File

@@ -96,7 +96,7 @@ The HEAD of the "stable" branch represents the latest stable release code.
## API versioning
The [API version](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
The [API version](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
## Environment

View File

@@ -28,7 +28,7 @@ Please read all release notes and watch out for warnings - we generally provide
#### Plugins
General classes and mechanisms are provided under the `plugin` [namespaces](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/__init__.py). These include:
General classes and mechanisms are provided under the `plugin` [namespaces](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/__init__.py). These include:
```python
# Management objects
@@ -44,7 +44,7 @@ MixinNotImplementedError # Is raised if a mixin was not implemented (core mec
#### Mixins
Mixins are split up internally to keep the source tree clean and enable better testing separation. All public APIs that should be used are exposed under `plugin.mixins`. These include all built-in mixins and notification methods. An up-to-date reference can be found in the source code (current master can be [found here](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/mixins/__init__.py)).
Mixins are split up internally to keep the source tree clean and enable better testing separation. All public APIs that should be used are exposed under `plugin.mixins`. These include all built-in mixins and notification methods. An up-to-date reference can be found in the source code (current master can be [found here](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/mixins/__init__.py)).
#### Models and other internal InvenTree APIs

View File

@@ -28,4 +28,4 @@ If a locate plugin is installed and activated, the [InvenTree mobile app](../../
### Implementation
Refer to the [InvenTree source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/locate/locate_sample.py) for a simple implementation example.
Refer to the [InvenTree source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/locate/locate_sample.py) for a simple implementation example.

View File

@@ -16,7 +16,7 @@ Additionally the `add_label_context` method, allowing custom context data to be
### Example
A sample plugin which provides additional context data to the report templates can be found [in the InvenTree source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/report_plugin_sample.py):
A sample plugin which provides additional context data to the report templates can be found [in the InvenTree source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/integration/report_plugin_sample.py):
```python
"""Sample plugin for extending reporting functionality"""

View File

@@ -59,4 +59,4 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
```
!!! info "More Info"
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/scheduled_task.py).
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/integration/scheduled_task.py).

View File

@@ -65,7 +65,7 @@ Additionally, add the following imports after the extended line.
#### Blocks
The page_base file is split into multiple sections called blocks. This allows you to implement sections of the webpage while getting many items like navbars, sidebars, and general layout provided for you.
The current default page base can be found [here](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/templates/page_base.html). Look through this file to determine overridable blocks. The [stock app](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/stock) offers a great example of implementing these blocks.
The current default page base can be found [here](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/templates/page_base.html). Look through this file to determine overridable blocks. The [stock app](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/stock) offers a great example of implementing these blocks.
!!! warning "Sidebar Block"
You may notice that implementing the `sidebar` block doesn't initially work. Be sure to enable the sidebar using JavaScript. This can be achieved by appending the following code, replacing `label` with a label of your choosing, to the end of your template file.

View File

@@ -9,7 +9,7 @@ The `ValidationMixin` class enables plugins to perform custom validation of obje
Any of the methods described below can be implemented in a custom plugin to provide functionality as required.
!!! info "More Info"
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/validation_sample.py).
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/integration/validation_sample.py).
!!! info "Multi Plugin Support"
It is possible to have multiple plugins loaded simultaneously which support validation methods. For example when validating a field, if one plugin returns a null value (`None`) then the *next* plugin (if available) will be queried.

View File

@@ -183,4 +183,4 @@ Finally added a `{% raw %}|floatformat:0{% endraw %}` to the quantity that remov
A default *BOM Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_bill_of_materials_report.html) for the default test report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_bill_of_materials_report.html) for the default test report template.

View File

@@ -321,4 +321,4 @@ This will result a report page like this:
A default *Build Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_build_order_base.html) for the default build report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_build_order_base.html) for the default build report template.

View File

@@ -12,7 +12,7 @@ Some common functions are provided for use in custom report and label templates.
```
!!! tip "Use the Source, Luke"
To see the full range of available helper functions, refer to the source file [report.py](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templatetags/report.py) where these functions are defined!
To see the full range of available helper functions, refer to the source file [report.py](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templatetags/report.py) where these functions are defined!
## Assigning Variables

View File

@@ -62,4 +62,4 @@ Price: {% render_currency line.total_line_price %}
A default *Purchase Order Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_po_report_base.html) for the default purchase order report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_po_report_base.html) for the default purchase order report template.

View File

@@ -23,4 +23,4 @@ In addition to the default report context variables, the following context varia
A default report template is provided out of the box, which can be used as a starting point for developing custom return order report templates.
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_return_order_report_base.html) for the default return order report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_return_order_report_base.html) for the default return order report template.

View File

@@ -28,4 +28,4 @@ In addition to the default report context variables, the following variables are
A default *Sales Order Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_so_report_base.html) for the default sales order report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_so_report_base.html) for the default sales order report template.

View File

@@ -13,4 +13,4 @@ You can use all content variables from the [StockLocation](./context_variables.m
A default report template is provided out of the box, which can be used as a starting point for developing custom return order report templates.
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.

View File

@@ -84,4 +84,4 @@ A default *Test Report* template is provided out of the box, which is useful for
{% include "img.html" %}
{% endwith %}
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_test_report_base.html) for the default test report template.
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_test_report_base.html) for the default test report template.

View File

@@ -4,13 +4,13 @@ title: InvenTree Single Sign On
## Single Sign On
InvenTree provides the possibility to use 3rd party services to authenticate users. This functionality makes use of [django-allauth](https://django-allauth.readthedocs.io/en/latest/) and supports a wide array of OpenID and OAuth [providers](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html).
InvenTree provides the possibility to use 3rd party services to authenticate users. This functionality makes use of [django-allauth](https://docs.allauth.org/en/latest/) and supports a wide array of OpenID and OAuth [providers](https://docs.allauth.org/en/latest/socialaccount/providers/index.html).
!!! tip "Provider Documentation"
There are a lot of technical considerations when configuring a particular SSO provider. A good starting point is the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html)
There are a lot of technical considerations when configuring a particular SSO provider. A good starting point is the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html)
!!! warning "Advanced Users"
The SSO functionality provided by django-allauth is powerful, but can prove challenging to configure. Please ensure that you understand the implications of enabling SSO for your InvenTree instance. Specific technical details of each available SSO provider are beyond the scope of this documentation - please refer to the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) for more information.
The SSO functionality provided by django-allauth is powerful, but can prove challenging to configure. Please ensure that you understand the implications of enabling SSO for your InvenTree instance. Specific technical details of each available SSO provider are beyond the scope of this documentation - please refer to the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) for more information.
## SSO Configuration
@@ -31,8 +31,8 @@ There are two variables in the configuration file which define the operation of
| Environment Variable |Configuration File | Description | More Info |
| --- | --- | --- | --- |
| INVENTREE_SOCIAL_BACKENDS | `social_backends` | A *list* of provider backends enabled for the InvenTree instance | [django-allauth docs](https://django-allauth.readthedocs.io/en/latest/installation/quickstart.html) |
| INVENTREE_SOCIAL_PROVIDERS | `social_providers` | A *dict* of settings specific to the installed providers | [provider documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) |
| INVENTREE_SOCIAL_BACKENDS | `social_backends` | A *list* of provider backends enabled for the InvenTree instance | [django-allauth docs](https://docs.allauth.org/en/latest/installation/quickstart.html) |
| INVENTREE_SOCIAL_PROVIDERS | `social_providers` | A *dict* of settings specific to the installed providers | [provider documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) |
In the example below, SSO provider modules are activated for *google*, *github* and *microsoft*. Specific configuration options are specified for the *microsoft* provider module:
@@ -44,7 +44,7 @@ In the example below, SSO provider modules are activated for *google*, *github*
Note that the provider modules specified in `social_backends` must be prefixed with `allauth.socialaccounts.providers`
!!! warning "Provider Documentation"
We do not provide any specific documentation for each provider module. Please refer to the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) for more information.
We do not provide any specific documentation for each provider module. Please refer to the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) for more information.
!!! tip "Restart Server"
As the [configuration file](../start/config.md) is only read when the server is launched, ensure you restart the server after editing the file.
@@ -57,7 +57,7 @@ The next step is to create an external authentication app with your provider of
The provider application will be created as part of your SSO provider setup. This is *not* the same as the *SocialApp* entry in the InvenTree admin interface.
!!! info "Read the Documentation"
The [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) is a good starting point here. There are also a number of good tutorials online (at least for the major supported SSO providers).
The [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) is a good starting point here. There are also a number of good tutorials online (at least for the major supported SSO providers).
In general, the external app will generate a *key* and *secret* pair - although different terminology may be used, depending on the provider.

View File

@@ -22,7 +22,7 @@ The InvenTree server tries to locate the `config.yaml` configuration file on sta
!!! tip "Config File Location"
When the InvenTree server boots, it will report the location where it expects to find the configuration file
The configuration file *template* can be found on [GitHub](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/config_template.yaml)
The configuration file *template* can be found on [GitHub](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/config_template.yaml)
!!! info "Template File"
The default configuration file (as defined by the template linked above) will be copied to the specified configuration file location on first run, if a configuration file is not found in that location.

View File

@@ -15,6 +15,14 @@ wget -qO install.sh https://get.inventree.org && bash install.sh
This script does all manual steps without any input. The installation might take up to 5-10 minutes to finish.
#### Permission Denied Error
The above command may need to be run with `sudo` permissions, depending on the system configuration. So, if the script fails with a permission error, try:
```bash
sudo wget -qO install.sh https://get.inventree.org && sudo bash install.sh
```
### File Locations
The installer creates the following directories:

View File

@@ -1022,8 +1022,12 @@ if SITE_URL:
logger.info('Using Site URL: %s', SITE_URL)
# Check that the site URL is valid
validator = URLValidator()
validator(SITE_URL)
try:
validator = URLValidator()
validator(SITE_URL)
except Exception:
print(f"Invalid SITE_URL value: '{SITE_URL}'. InvenTree server cannot start.")
sys.exit(-1)
# Enable or disable multi-site framework
SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False)
@@ -1322,7 +1326,7 @@ PLUGIN_TESTING_SETUP = get_setting(
) # Load plugins from setup hooks in testing?
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_RETRY = get_setting(
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
) # How often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?

View File

@@ -19,7 +19,7 @@ from dulwich.repo import NotGitRepository, Repo
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = '0.15.0 dev'
INVENTREE_SW_VERSION = '0.15.6'
# Discover git
try:
@@ -104,7 +104,7 @@ def inventreeDocUrl():
def inventreeAppUrl():
"""Return URL for InvenTree app site."""
return f'{inventreeDocUrl()}/app/app/'
return f'https://docs.inventree.org/app/'
def inventreeCreditsUrl():

View File

@@ -237,6 +237,16 @@ class BuildOutputCreateSerializer(serializers.Serializer):
The Build object is provided to the serializer context.
"""
class Meta:
"""Serializer metaclass."""
fields = [
'quantity',
'batch_code',
'serial_numbers',
'location',
'auto_allocate',
]
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
@@ -639,6 +649,14 @@ class OverallocationChoice():
class BuildCompleteSerializer(serializers.Serializer):
"""DRF serializer for marking a BuildOrder as complete."""
class Meta:
"""Serializer metaclass"""
fields = [
'accept_overallocated',
'accept_unallocated',
'accept_incomplete',
]
def get_context_data(self):
"""Retrieve extra context data for this serializer.
@@ -732,6 +750,13 @@ class BuildUnallocationSerializer(serializers.Serializer):
- bom_item: Filter against a particular BOM line item
"""
class Meta:
"""Serializer metaclass"""
fields = [
'build_line',
'output',
]
build_line = serializers.PrimaryKeyRelatedField(
queryset=BuildLine.objects.all(),
many=False,

View File

@@ -271,7 +271,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""
fields = ([],)
fields = []
def get_context_data(self):
"""Return custom context information about the order."""

View File

@@ -484,6 +484,9 @@ class PurchaseOrderTest(OrderTest):
url = reverse('api-po-cancel', kwargs={'pk': po.pk})
# Get an OPTIONS request from the endpoint
self.options(url, data={'context': True}, expected_code=200)
# Try to cancel the PO, but without required permissions
self.post(url, {}, expected_code=403)

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import decimal
import hashlib
import logging
import math
import os
import re
from datetime import datetime, timedelta
@@ -3757,6 +3758,12 @@ class PartParameter(InvenTree.models.InvenTreeMetadataModel):
except ValueError:
self.data_numeric = None
if self.data_numeric is not None and type(self.data_numeric) is float:
# Prevent out of range numbers, etc
# Ref: https://github.com/inventree/InvenTree/issues/7593
if math.isnan(self.data_numeric) or math.isinf(self.data_numeric):
self.data_numeric = None
part = models.ForeignKey(
Part,
on_delete=models.CASCADE,

View File

@@ -47,6 +47,25 @@ class TestParams(TestCase):
t3.full_clean()
t3.save() # pragma: no cover
def test_invalid_numbers(self):
"""Test that invalid floating point numbers are correctly handled."""
p = Part.objects.first()
t = PartParameterTemplate.objects.create(name='Yaks')
valid_floats = ['-12', '1.234', '17', '3e45', '-12e34']
for value in valid_floats:
param = PartParameter(part=p, template=t, data=value)
param.full_clean()
self.assertIsNotNone(param.data_numeric)
invalid_floats = ['88E6352', 'inf', '-inf', 'nan', '3.14.15', '3eee3']
for value in invalid_floats:
param = PartParameter(part=p, template=t, data=value)
param.full_clean()
self.assertIsNone(param.data_numeric)
def test_metadata(self):
"""Unit tests for the metadata field."""
for model in [PartParameterTemplate]:

View File

@@ -60,6 +60,9 @@ def register_event(event, *args, **kwargs):
# Determine if there are any plugins which are interested in responding
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'):
# Check if the plugin registry needs to be reloaded
registry.check_reload()
with transaction.atomic():
for slug, plugin in registry.plugins.items():
if not plugin.mixin_enabled('events'):

View File

@@ -219,51 +219,15 @@ class PluginsRegistry:
"""
logger.info('Loading plugins')
registered_successful = False
blocked_plugin = None
retry_counter = settings.PLUGIN_RETRY
while not registered_successful:
try:
# We are using the db so for migrations etc we need to try this block
self._init_plugins(blocked_plugin)
self._activate_plugins(full_reload=full_reload)
registered_successful = True
except (OperationalError, ProgrammingError): # pragma: no cover
# Exception if the database has not been migrated yet
logger.info('Database not accessible while loading plugins')
break
except IntegrationPluginError as error:
logger.exception(
'[PLUGIN] Encountered an error with %s:\n%s',
error.path,
error.message,
)
log_error({error.path: error.message}, 'load')
blocked_plugin = error.path # we will not try to load this app again
# Initialize apps without any plugins
self._clean_registry()
self._clean_installed_apps()
self._activate_plugins(force_reload=True, full_reload=full_reload)
# We do not want to end in an endless loop
retry_counter -= 1
if retry_counter <= 0: # pragma: no cover
if settings.PLUGIN_TESTING:
print('[PLUGIN] Max retries, breaking loading')
break
if settings.PLUGIN_TESTING:
print(
f'[PLUGIN] Above error occurred during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left'
)
# now the loading will re-start up with init
# disable full reload after the first round
if full_reload:
full_reload = False
try:
self._init_plugins()
self._activate_plugins(full_reload=full_reload)
except (OperationalError, ProgrammingError, IntegrityError):
# Exception if the database has not been migrated yet, or is not ready
pass
except IntegrationPluginError:
# Plugin specific error has already been handled
pass
# ensure plugins_loaded is True
self.plugins_loaded = True
@@ -478,18 +442,13 @@ class PluginsRegistry:
# endregion
# region general internal loading / activating / deactivating / unloading
def _init_plugins(self, disabled: str = None):
"""Initialise all found plugins.
def _init_plugin(self, plugin, configs: dict):
"""Initialise a single plugin.
Args:
disabled (str, optional): Loading path of disabled app. Defaults to None.
Raises:
error: IntegrationPluginError
plugin: Plugin module
"""
# Imports need to be in this level to prevent early db model imports
from InvenTree import version
from plugin.models import PluginConfig
def safe_reference(plugin, key: str, active: bool = True):
"""Safe reference to plugin dicts."""
@@ -503,6 +462,99 @@ class PluginsRegistry:
self.plugins_inactive[key] = plugin.db
self.plugins_full[key] = plugin
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
plg_name = plugin.NAME
plg_key = slugify(
plugin.SLUG if getattr(plugin, 'SLUG', None) else plg_name
) # keys are slugs!
logger.info("Loading plugin '%s'", plg_key)
if plg_key in configs:
plg_db = configs[plg_key]
else:
plg_db = self.get_plugin_config(plg_key, plg_name)
plugin.db = plg_db
# Check if this is a 'builtin' plugin
builtin = plugin.check_is_builtin()
package_name = None
# Extract plugin package name
if getattr(plugin, 'is_package', False):
package_name = getattr(plugin, 'package_name', None)
# Auto-enable builtin plugins
if builtin and plg_db and not plg_db.active:
plg_db.active = True
plg_db.save()
# Save the package_name attribute to the plugin
if plg_db.package_name != package_name:
plg_db.package_name = package_name
plg_db.save()
# Determine if this plugin should be loaded:
# - If PLUGIN_TESTING is enabled
# - If this is a 'builtin' plugin
# - If this plugin has been explicitly enabled by the user
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
# Initialize package - we can be sure that an admin has activated the plugin
logger.debug('Loading plugin `%s`', plg_name)
try:
t_start = time.time()
plg_i: InvenTreePlugin = plugin()
dt = time.time() - t_start
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
except Exception as error:
handle_error(
error, log_name='init'
) # log error and raise it -> disable plugin
logger.warning('Plugin `%s` could not be loaded', plg_name)
# Safe extra attributes
plg_i.is_package = getattr(plg_i, 'is_package', False)
plg_i.pk = plg_db.pk if plg_db else None
plg_i.db = plg_db
# Run version check for plugin
if (plg_i.MIN_VERSION or plg_i.MAX_VERSION) and not plg_i.check_version():
# Disable plugin
safe_reference(plugin=plg_i, key=plg_key, active=False)
p = plg_name
v = version.inventreeVersion()
_msg = _(
f"Plugin '{p}' is not compatible with the current InvenTree version {v}"
)
if v := plg_i.MIN_VERSION:
_msg += _(f'Plugin requires at least version {v}')
if v := plg_i.MAX_VERSION:
_msg += _(f'Plugin requires at most version {v}')
# Log to error stack
log_error(_msg, reference='init')
else:
safe_reference(plugin=plg_i, key=plg_key)
else: # pragma: no cover
safe_reference(plugin=plugin, key=plg_key, active=False)
def _init_plugins(self):
"""Initialise all found plugins.
Args:
disabled (str, optional): Loading path of disabled app. Defaults to None.
Raises:
error: IntegrationPluginError
"""
# Imports need to be in this level to prevent early db model imports
from plugin.models import PluginConfig
logger.debug('Starting plugin initialization')
# Fetch and cache list of existing plugin configuration instances
@@ -510,102 +562,32 @@ class PluginsRegistry:
# Initialize plugins
for plg in self.plugin_modules:
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
plg_name = plg.NAME
plg_key = slugify(
plg.SLUG if getattr(plg, 'SLUG', None) else plg_name
) # keys are slugs!
# Attempt to load each individual plugin
attempts = settings.PLUGIN_RETRY
try:
if plg_key in plugin_configs:
# Configuration already exists
plg_db = plugin_configs[plg_key]
else:
# Configuration needs to be created
plg_db = self.get_plugin_config(plg_key, plg_name)
except (OperationalError, ProgrammingError) as error:
# Exception if the database has not been migrated yet - check if test are running - raise if not
if not settings.PLUGIN_TESTING:
raise error # pragma: no cover
plg_db = None
except IntegrityError as error: # pragma: no cover
logger.exception('Error initializing plugin `%s`: %s', plg_name, error)
handle_error(error, log_name='init')
# Append reference to plugin
plg.db = plg_db
# Check if this is a 'builtin' plugin
builtin = plg.check_is_builtin()
package_name = None
# Extract plugin package name
if getattr(plg, 'is_package', False):
package_name = getattr(plg, 'package_name', None)
# Auto-enable builtin plugins
if builtin and plg_db and not plg_db.active:
plg_db.active = True
plg_db.save()
# Save the package_name attribute to the plugin
if plg_db.package_name != package_name:
plg_db.package_name = package_name
plg_db.save()
# Determine if this plugin should be loaded:
# - If PLUGIN_TESTING is enabled
# - If this is a 'builtin' plugin
# - If this plugin has been explicitly enabled by the user
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
# Check if the plugin was blocked -> threw an error; option1: package, option2: file-based
if disabled and disabled in (plg.__name__, plg.__module__):
safe_reference(plugin=plg, key=plg_key, active=False)
continue # continue -> the plugin is not loaded
# Initialize package - we can be sure that an admin has activated the plugin
logger.debug('Loading plugin `%s`', plg_name)
while attempts > 0:
attempts -= 1
try:
t_start = time.time()
plg_i: InvenTreePlugin = plg()
dt = time.time() - t_start
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
self._init_plugin(plg, plugin_configs)
break
except IntegrationPluginError as error:
# Error has been handled downstream
pass
except Exception as error:
# Handle the error, log it and try again
handle_error(
error, log_name='init'
) # log error and raise it -> disable plugin
logger.warning('Plugin `%s` could not be loaded', plg_name)
# Safe extra attributes
plg_i.is_package = getattr(plg_i, 'is_package', False)
plg_i.pk = plg_db.pk if plg_db else None
plg_i.db = plg_db
# Run version check for plugin
if (
plg_i.MIN_VERSION or plg_i.MAX_VERSION
) and not plg_i.check_version():
# Disable plugin
safe_reference(plugin=plg_i, key=plg_key, active=False)
p = plg_name
v = version.inventreeVersion()
_msg = _(
f"Plugin '{p}' is not compatible with the current InvenTree version {v}"
error, log_name='init', do_raise=settings.PLUGIN_TESTING
)
if v := plg_i.MIN_VERSION:
_msg += _(f'Plugin requires at least version {v}')
if v := plg_i.MAX_VERSION:
_msg += _(f'Plugin requires at most version {v}')
# Log to error stack
log_error(_msg, reference='init')
else:
safe_reference(plugin=plg_i, key=plg_key)
else: # pragma: no cover
safe_reference(plugin=plg, key=plg_key, active=False)
if attempts == 0:
logger.exception(
'[PLUGIN] Encountered an error with %s:\n%s',
error.path,
str(error),
)
logger.debug('Finished plugin initialization')
def __get_mixin_order(self):
"""Returns a list of mixin classes, in the order that they should be activated."""

View File

@@ -273,15 +273,17 @@ class RegistryTests(TestCase):
# Reload to rediscover plugins
registry.reload_plugins(full_reload=True, collect=True)
self.assertEqual(len(registry.errors), 3)
self.assertEqual(len(registry.errors), 2)
# There should be at least one discovery error in the module `broken_file`
self.assertGreater(len(registry.errors.get('discovery')), 0)
self.assertEqual(
registry.errors.get('discovery')[0]['broken_file'],
"name 'bb' is not defined",
)
# There should be at least one load error with an intentional KeyError
self.assertGreater(len(registry.errors.get('load')), 0)
self.assertGreater(len(registry.errors.get('init')), 0)
self.assertEqual(
registry.errors.get('load')[0]['broken_sample'], "'This is a dummy error'"
registry.errors.get('init')[0]['broken_sample'], "'This is a dummy error'"
)

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

@@ -229,17 +229,17 @@ class StockItemResource(InvenTreeResource):
is_building = Field(
attribute='is_building',
column_name=_('Building'),
widget=widgets.IntegerWidget(),
widget=widgets.BooleanWidget(),
)
review_needed = Field(
attribute='review_needed',
column_name=_('Review Needed'),
widget=widgets.IntegerWidget(),
widget=widgets.BooleanWidget(),
)
delete_on_deplete = Field(
attribute='delete_on_deplete',
column_name=_('Delete on Deplete'),
widget=widgets.IntegerWidget(),
widget=widgets.BooleanWidget(),
)
# Date management

View File

@@ -1402,6 +1402,22 @@ class StockTrackingList(ListAPI):
return self.serializer_class(*args, **kwargs)
def get_delta_model_map(self) -> dict:
"""Return a mapping of delta models to their respective models and serializers.
This is used to generate additional context information for the historical data,
with some attempt at caching so that we can reduce the number of database hits.
"""
return {
'part': (Part, PartBriefSerializer),
'location': (StockLocation, StockSerializers.LocationSerializer),
'customer': (Company, CompanySerializer),
'purchaseorder': (PurchaseOrder, PurchaseOrderSerializer),
'salesorder': (SalesOrder, SalesOrderSerializer),
'returnorder': (ReturnOrder, ReturnOrderSerializer),
'buildorder': (Build, BuildSerializer),
}
def list(self, request, *args, **kwargs):
"""List all stock tracking entries."""
queryset = self.filter_queryset(self.get_queryset())
@@ -1415,84 +1431,36 @@ class StockTrackingList(ListAPI):
data = serializer.data
# Attempt to add extra context information to the historical data
delta_models = self.get_delta_model_map()
# Construct a set of related models we need to lookup for later
related_model_lookups = {key: set() for key in delta_models.keys()}
# Run a first pass through the data to determine which related models we need to lookup
for item in data:
deltas = item['deltas']
if not deltas:
deltas = {}
for key in delta_models.keys():
if key in deltas:
related_model_lookups[key].add(deltas[key])
# Add part detail
if 'part' in deltas:
try:
part = Part.objects.get(pk=deltas['part'])
serializer = PartBriefSerializer(part)
deltas['part_detail'] = serializer.data
except Exception:
pass
for key in delta_models.keys():
model, serializer = delta_models[key]
# Add location detail
if 'location' in deltas:
try:
location = StockLocation.objects.get(pk=deltas['location'])
serializer = StockSerializers.LocationSerializer(location)
deltas['location_detail'] = serializer.data
except Exception:
pass
# Fetch all related models in one go
related_models = model.objects.filter(pk__in=related_model_lookups[key])
# Add stockitem detail
if 'stockitem' in deltas:
try:
stockitem = StockItem.objects.get(pk=deltas['stockitem'])
serializer = StockSerializers.StockItemSerializer(stockitem)
deltas['stockitem_detail'] = serializer.data
except Exception:
pass
# Construct a mapping of pk -> serialized data
related_data = {obj.pk: serializer(obj).data for obj in related_models}
# Add customer detail
if 'customer' in deltas:
try:
customer = Company.objects.get(pk=deltas['customer'])
serializer = CompanySerializer(customer)
deltas['customer_detail'] = serializer.data
except Exception:
pass
# Now, update the data with the serialized data
for item in data:
deltas = item['deltas']
# Add PurchaseOrder detail
if 'purchaseorder' in deltas:
try:
order = PurchaseOrder.objects.get(pk=deltas['purchaseorder'])
serializer = PurchaseOrderSerializer(order)
deltas['purchaseorder_detail'] = serializer.data
except Exception:
pass
# Add SalesOrder detail
if 'salesorder' in deltas:
try:
order = SalesOrder.objects.get(pk=deltas['salesorder'])
serializer = SalesOrderSerializer(order)
deltas['salesorder_detail'] = serializer.data
except Exception:
pass
# Add ReturnOrder detail
if 'returnorder' in deltas:
try:
order = ReturnOrder.objects.get(pk=deltas['returnorder'])
serializer = ReturnOrderSerializer(order)
deltas['returnorder_detail'] = serializer.data
except Exception:
pass
# Add BuildOrder detail
if 'buildorder' in deltas:
try:
order = Build.objects.get(pk=deltas['buildorder'])
serializer = BuildSerializer(order)
deltas['buildorder_detail'] = serializer.data
except Exception:
pass
if key in deltas:
item['deltas'][f'{key}_detail'] = related_data.get(
deltas[key], None
)
if page is not None:
return self.get_paginated_response(data)

View File

@@ -1678,6 +1678,9 @@ class StockItem(
- Tracking history for the *other* item is deleted
- Any allocations (build order, sales order) are moved to this StockItem
"""
if isinstance(other_items, StockItem):
other_items = [other_items]
if len(other_items) == 0:
return
@@ -1685,7 +1688,7 @@ class StockItem(
tree_ids = {self.tree_id}
user = kwargs.get('user', None)
location = kwargs.get('location', None)
location = kwargs.get('location', self.location)
notes = kwargs.get('notes', None)
parent_id = self.parent.pk if self.parent else None
@@ -1693,6 +1696,9 @@ class StockItem(
for other in other_items:
# If the stock item cannot be merged, return
if not self.can_merge(other, raise_error=raise_error, **kwargs):
logger.warning(
'Stock item <%s> could not be merge into <%s>', other.pk, self.pk
)
return
tree_ids.add(other.tree_id)
@@ -1722,7 +1728,7 @@ class StockItem(
user,
quantity=self.quantity,
notes=notes,
deltas={'location': location.pk},
deltas={'location': location.pk if location else None},
)
self.location = location

View File

@@ -233,6 +233,7 @@
{% settings_value "TEST_STATION_DATA" as test_station_fields %}
{% if item.part.trackable %}
loadStockTestResultsTable(
$("#test-result-table"), {
part: {{ item.part.id }},
@@ -248,6 +249,7 @@
url: '{% url "api-stockitem-testreport-list" %}',
});
});
{% endif %}
{% if user.is_staff %}
$("#delete-test-results").click(function() {

View File

@@ -22,7 +22,7 @@
{% for line in lines %}
<tr style="height: 2.5rem; border-bottom: 1px solid">
<td style='padding-left: 1em;'>
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if part.description %} - <em>{{ part.description }}</em>{% endif %}
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if line.part.description %} - <em>{{ line.part.description }}</em>{% endif %}
</td>
<td style="text-align: center;">
{% decimal line.required %} {% include "part/part_units.html" with part=line.part %}

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
@@ -1642,6 +1643,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);
}
},

View File

@@ -3,11 +3,12 @@
import datetime
import logging
from django.contrib.auth import get_user, login
from django.contrib.auth import get_user, login, logout
from django.contrib.auth.models import Group, User
from django.urls import include, path, re_path
from django.views.generic.base import RedirectView
from allauth.account.adapter import get_adapter
from dj_rest_auth.views import LoginView, LogoutView
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
from rest_framework import exceptions, permissions
@@ -17,6 +18,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
import InvenTree.helpers
from common.models import InvenTreeSetting
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import (
ListAPI,
@@ -216,7 +218,22 @@ class GroupList(ListCreateAPI):
class Login(LoginView):
"""API view for logging in via API."""
...
def process_login(self):
"""Process the login request, ensure that MFA is enforced if required."""
# Normal login process
ret = super().process_login()
# Now check if MFA is enforced
user = self.request.user
adapter = get_adapter(self.request)
# User requires 2FA or MFA is enforced globally - no logins via API
if adapter.has_2fa_enabled(user) or InvenTreeSetting.get_setting(
'LOGIN_ENFORCE_MFA'
):
logout(self.request)
raise exceptions.PermissionDenied('MFA required for this user')
return ret
@extend_schema_view(

View File

@@ -0,0 +1,31 @@
# Generated by Django 4.2.12 on 2024-05-23 16:40
from importlib import import_module
from django.conf import settings
from django.db import migrations
def clear_sessions(apps, schema_editor):
"""Clear all user sessions."""
try:
engine = import_module(settings.SESSION_ENGINE)
engine.SessionStore.clear_expired()
print('Cleared all user sessions to deal with GHSA-2crp-q9pc-457j')
except Exception:
# Database may not be ready yet, so this does not matter anyhow
pass
class Migration(migrations.Migration):
atomic = False
dependencies = [
("users", "0010_alter_apitoken_key"),
]
operations = [
migrations.RunPython(
clear_sessions, reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -253,9 +253,9 @@ distlib==0.3.8 \
--hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \
--hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64
# via virtualenv
django==4.2.12 \
--hash=sha256:6a6b4aff8a2db2dc7dcc5650cb2c7a7a0d1eb38e2aa2335fdf001e41801e9797 \
--hash=sha256:7640e86835d44ae118c2916a803d8081f40e214ee18a5a92a0202994ca60a4b4
django==4.2.14 \
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
# via
# django-admin-shell
# django-slowtests

View File

@@ -329,9 +329,9 @@ diff-match-patch==20230430 \
# via django-import-export
dj-rest-auth==6.0.0 \
--hash=sha256:760b45f3a07cd6182e6a20fe07d0c55230c5f950167df724d7914d0dd8c50133
django==4.2.12 \
--hash=sha256:6a6b4aff8a2db2dc7dcc5650cb2c7a7a0d1eb38e2aa2335fdf001e41801e9797 \
--hash=sha256:7640e86835d44ae118c2916a803d8081f40e214ee18a5a92a0202994ca60a4b4
django==4.2.14 \
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
# via
# dj-rest-auth
# django-allauth

View File

@@ -36,7 +36,7 @@ if (IS_DEV_OR_DEMO) {
}
export const docLinks = {
app: 'https://docs.inventree.org/en/latest/app/app/',
app: 'https://docs.inventree.org/app/',
getting_started: 'https://docs.inventree.org/en/latest/getting_started/',
api: 'https://docs.inventree.org/en/latest/api/api/',
developer: 'https://docs.inventree.org/en/latest/develop/starting/',

View File

@@ -230,7 +230,12 @@ def plugins(c, uv=False):
@task(help={'uv': 'Use UV package manager (experimental)'})
def install(c, uv=False):
"""Installs required python packages."""
print("Installing required python packages from 'src/backend/requirements.txt'")
INSTALL_FILE = 'src/backend/requirements.txt'
print(f"Installing required python packages from '{INSTALL_FILE}'")
if not Path(INSTALL_FILE).is_file():
raise FileNotFoundError(f"Requirements file '{INSTALL_FILE}' not found")
# Install required Python packages with PIP
if not uv:
@@ -238,13 +243,13 @@ def install(c, uv=False):
'pip3 install --no-cache-dir --disable-pip-version-check -U pip setuptools'
)
c.run(
'pip3 install --no-cache-dir --disable-pip-version-check -U --require-hashes -r src/backend/requirements.txt'
f'pip3 install --no-cache-dir --disable-pip-version-check -U --require-hashes -r {INSTALL_FILE}'
)
else:
c.run(
'pip3 install --no-cache-dir --disable-pip-version-check -U uv setuptools'
)
c.run('uv pip install -U --require-hashes -r src/backend/requirements.txt')
c.run(f'uv pip install -U --require-hashes -r {INSTALL_FILE}')
# Run plugins install
plugins(c, uv=uv)
@@ -403,7 +408,7 @@ def restore(
ignore_database=False,
):
"""Restore the database and media files."""
base_cmd = '--no-input --uncompress -v 2'
base_cmd = '--noinput --uncompress -v 2'
if path:
base_cmd += f' -I {path}'
@@ -449,6 +454,12 @@ def migrate(c):
print('InvenTree database migrations completed!')
@task(help={'app': 'Specify an app to show migrations for (leave blank for all apps)'})
def showmigrations(c, app=''):
"""Show the migration status of the database."""
manage(c, f'showmigrations {app}', pty=True)
@task(
post=[clean_settings, translate_stats],
help={
@@ -759,18 +770,31 @@ def wait(c):
return manage(c, 'wait_for_db')
@task(pre=[wait], help={'address': 'Server address:port (default=0.0.0.0:8000)'})
def gunicorn(c, address='0.0.0.0:8000'):
@task(
pre=[wait],
help={
'address': 'Server address:port (default=0.0.0.0:8000)',
'workers': 'Specify number of worker threads (override config file)',
},
)
def gunicorn(c, address='0.0.0.0:8000', workers=None):
"""Launch a gunicorn webserver.
Note: This server will not auto-reload in response to code changes.
"""
c.run(
'gunicorn -c ./docker/gunicorn.conf.py InvenTree.wsgi -b {address} --chdir ./InvenTree'.format(
address=address
),
pty=True,
)
here = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(here, 'contrib', 'container', 'gunicorn.conf.py')
chdir = os.path.join(here, 'src', 'backend', 'InvenTree')
cmd = f'gunicorn -c {config_file} InvenTree.wsgi -b {address} --chdir {chdir}'
if workers:
cmd += f' --workers={workers}'
print('Starting Gunicorn Server:')
print(cmd)
c.run(cmd, pty=True)
@task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})