Compare commits

...

18 Commits

Author SHA1 Message Date
Oliver
aed43b0822 Update version.py (#7838)
Bump version number to 0.15.8
2024-08-08 19:38:08 +10:00
github-actions[bot]
88c6696fd2 Add limit to default location annotation (#7771) (#7790)
* Add limit to default location annotation

Limits the number of results from the default_location filter to 1

* Add unit test to verify annotation functionality

(cherry picked from commit 2cb8f4128e)

Co-authored-by: Lavissa <lavissawow@gmail.com>
2024-08-02 10:47:00 +10:00
Lukas
0f3b719c80 Backport plugin_static template tag to 0.15.x (#7764) 2024-07-30 20:51:57 +10:00
Oliver
0d8eb2e0b3 Update version.py
Bump version number to 0.15.7
2024-07-27 14:52:36 +10:00
Matthias Mair
56b16cb1ac Backport Docs code links (#7740)
* Docs code links (#7342)

* Update docs

- Add note about permission denied error

* Add macro for generating link to github code

* Implement similar feature for source directory links

* Adds helper function for link checking

* Allow for specification of "raw" file links

* Remove debug statement

* Generate list of available invoke tasks

(cherry picked from commit 797a0c10df)

* auto-detect current branch and use that for links

* remove debug logging

* style fix

* spell fix

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-07-27 14:51:54 +10:00
github-actions[bot]
e5a36f6936 Make libffi version more flexible (#7734) (#7738)
* Make libffi version more flexible
Can not install on debian 12
Fixes #6036

* target v11 packaging on v12 install
we are only targeting one version per debian / ubuntu channel. This should not present a problem

(cherry picked from commit 4edea65e00)

Co-authored-by: Matthias Mair <code@mjmair.com>
2024-07-26 10:20:45 +10:00
Matthias Mair
61b5a7d393 backport for https://github.com/inventree/InvenTree/pull/7655 (#7739) 2024-07-26 10:20:15 +10:00
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
38 changed files with 601 additions and 212 deletions

View File

@@ -554,6 +554,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

@@ -32,7 +32,7 @@ dependencies:
- gettext
- nginx
- jq
- libffi7
- "libffi7 | libffi8"
targets:
ubuntu-20.04: true
debian-11: true

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

@@ -75,6 +75,7 @@ root_command() {
;;
"Debian GNU/Linux" | "debian gnu/linux" | Raspbian)
if [[ $VER == "12" ]]; then
DIST_VER="11"
SUPPORTED=true
elif [[ $VER == "11" ]]; then
SUPPORTED=true

View File

@@ -5,33 +5,41 @@
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
REPO="inventree/InvenTree"
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/$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/$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/$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 +47,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/$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

4
docs/.gitignore vendored
View File

@@ -13,6 +13,10 @@ site/
# Generated API schema files
docs/api/schema/*.yml
# Temporary cache files
url_cache.txt
invoke-commands.txt
# Temp files
releases.json
versions.json

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/0.15.x/src/backend/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed.
## Environment

View File

@@ -16,7 +16,7 @@ For further information, read more about [installing plugins](./plugins/install.
### Plugin Base Class
Custom plugins must inherit from the [InvenTreePlugin class](https://github.com/inventree/InvenTree/blob/2d1776a151721d65d0ae007049d358085b2fcfd5/InvenTree/plugin/plugin.py#L204). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
!!! warning "Namechange"
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. While the old name is still available till `0.8.0` we strongly suggest upgrading your plugins. Deprecation warnings are raised if the old name is used.
@@ -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/0.15.x/src/backend/InvenTree/plugin/__init__.py). These include:
General classes and mechanisms are provided under the `plugin` [namespaces]({{ sourcefile("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/0.15.x/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 [can be found here]({{ sourcefile("src/backend/InvenTree/plugin/mixins/__init__.py") }}).
#### Models and other internal InvenTree APIs
@@ -72,7 +72,7 @@ MIN_VERSION = None # Lowest InvenTree version number that is supported by the p
MAX_VERSION = None # Highest InvenTree version number that is supported by the plugin
```
Refer to the [sample plugins](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/plugin/samples) for further examples.
Refer to the [sample plugins]({{ sourcedir("src/backend/InvenTree/plugin/samples") }}) for further examples.
### Plugin Config

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/0.15.x/src/backend/InvenTree/plugin/samples/locate/locate_sample.py) for a simple implementation example.
Refer to the [InvenTree source code]({{ sourcefile("src/backend/InvenTree/plugin/samples/locate/locate_sample.py") }}) for a simple implementation example.

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/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.
The current default page base can be found [here]({{ sourcefile("src/backend/InvenTree/templates/page_base.html") }}). Look through this file to determine overridable blocks. The [stock app]({{ sourcedir("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/0.15.x/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]({{ sourcefile("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

@@ -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/0.15.x/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]({{ sourcefile("src/backend/InvenTree/report/templatetags/report.py") }}) where these functions are defined!
## Assigning Variables

View File

@@ -8,9 +8,9 @@ To that end, we have implemented a number of security measures over the years, w
The InvenTree project is managed by a small team of developers, who are responsible for the ongoing development and maintenance of the software. Two geographically distributed users have administrative access to the InvenTree codebase. Merges are only done by one of these two users, the maintainer Oliver.
InvenTree is open-source, and we welcome contributions from the community. However, all contributions are reviewed and scrutinised before being merged into the codebase.
We provide a written [Security Policy](https://github.com/inventree/InvenTree/blob/master/SECURITY.md) in our main repo to ensure that all security issues are handled in a timely manner.
We provide a written [Security Policy]({{ sourcefile("SECURITY.md") }}) in our main repo to ensure that all security issues are handled in a timely manner.
If we become aware of a security issue, we will take immediate action to address the issue, and will provide a public disclosure of the issue once it has been resolved. We support assigning CVEs to security issues where appropriate. Our past security advisories can be found [here](https://github.com/inventree/InvenTree/security/advisories).
If we become aware of a security issue, we will take immediate action to address the issue, and will provide a public disclosure of the issue once it has been resolved. We support assigning CVEs to security issues where appropriate. Our [past security advisories can be found here](https://github.com/inventree/InvenTree/security/advisories).
## Technical measures

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/0.15.x/src/backend/InvenTree/config_template.yaml)
The configuration file *template* can be found on [GitHub]({{ sourcefile("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

@@ -27,13 +27,13 @@ The following guide provides a streamlined production InvenTree installation, wi
### Required Files
The following files required for this setup are provided with the InvenTree source, located in the `/contrib/container/` directory of the [InvenTree source code](https://github.com/inventree/InvenTree/tree/master/contrib/container/):
The following files required for this setup are provided with the InvenTree source, located in the `contrib/container/` directory of the [InvenTree source code]({{ sourcedir("/contrib/container/") }}):
| Filename | Description |
| --- | --- |
| [docker-compose.yml](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/docker-compose.yml)| The docker compose script |
| [.env](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/.env) | Environment variables |
| [Caddyfile](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/Caddyfile) | Caddy configuration file |
| [docker-compose.yml]({{ sourcefile("contrib/container/docker-compose.yml", raw=True) }}) | The docker compose script |
| [.env]({{ sourcefile("contrib/container/.env", raw=True) }}) | Environment variables |
| [Caddyfile]({{ sourcefile("contrib/container/Caddyfile", raw=True) }}) | Caddy configuration file |
Download these files to a directory on your local machine.

View File

@@ -23,7 +23,7 @@ Install required system packages (as superuser):
The following packages are required on a debian system. A different distribution may require a slightly different set of packages
!!! info "Python Version"
InvenTree requires a modern Python version check [here](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md#target-version) for the current minimums.
InvenTree requires a modern Python version [check here]({{ sourcefile("CONTRIBUTING.md") }}) for the current minimums.
```
sudo apt-get update

View File

@@ -84,6 +84,12 @@ To display a list of the available InvenTree administration actions, run the fol
invoke --list
```
This provides a list of the available invoke commands - also displayed below:
```
{{ invoke_commands() }}
```
### Virtual Environment
Installing the required Python packages inside a virtual environment allows a local install separate to the system-wide Python installation. While not strictly necessary, using a virtual environment is **highly recommended** as it prevents conflicts between the different Python installations.

View File

@@ -1,11 +1,171 @@
"""Main entry point for the documentation build process."""
import os
import subprocess
import requests
import yaml
def get_repo_url(raw=False):
"""Return the repository URL for the current project."""
mkdocs_yml = os.path.join(os.path.dirname(__file__), 'mkdocs.yml')
with open(mkdocs_yml, 'r') as f:
mkdocs_config = yaml.safe_load(f)
repo_name = mkdocs_config['repo_name']
if raw:
return f'https://raw.githubusercontent.com/{repo_name}'
else:
return f'https://github.com/{repo_name}'
def check_link(url) -> bool:
"""Check that a provided URL is valid.
We allow a number attempts and a lengthy timeout,
as we do not want false negatives.
"""
CACHE_FILE = os.path.join(os.path.dirname(__file__), 'url_cache.txt')
# Keep a local cache file of URLs we have already checked
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
cache = f.read().splitlines()
if url in cache:
return True
attempts = 5
while attempts > 0:
response = requests.head(url, timeout=5000)
if response.status_code == 200:
# Update the cache file
with open(CACHE_FILE, 'a') as f:
f.write(f'{url}\n')
return True
attempts -= 1
return False
def get_build_enviroment() -> str:
"""Returns the branch we are currently building on, based on the environment variables of the various CI platforms."""
# Check if we are in ReadTheDocs
if os.environ.get('READTHEDOCS') == 'True':
return os.environ.get('READTHEDOCS_GIT_IDENTIFIER')
# We are in GitHub Actions
elif os.environ.get('GITHUB_ACTIONS') == 'true':
return os.environ.get('GITHUB_REF')
else:
return 'master'
def define_env(env):
"""Define custom environment variables for the documentation build process."""
@env.macro
def sourcedir(dirname, branch=None):
"""Return a link to a directory within the source code repository.
Arguments:
- dirname: The name of the directory to link to (relative to the top-level directory)
Returns:
- A fully qualified URL to the source code directory on GitHub
Raises:
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid
"""
if branch == None:
branch = get_build_enviroment()
if dirname.startswith('/'):
dirname = dirname[1:]
# This file exists at ./docs/main.py, so any directory we link to must be relative to the top-level directory
here = os.path.dirname(__file__)
root = os.path.abspath(os.path.join(here, '..'))
directory = os.path.join(root, dirname)
directory = os.path.abspath(directory)
if not os.path.exists(directory) or not os.path.isdir(directory):
raise FileNotFoundError(f'Source directory {dirname} does not exist.')
repo_url = get_repo_url()
url = f'{repo_url}/tree/{branch}/{dirname}'
# Check that the URL exists before returning it
if not check_link(url):
raise FileNotFoundError(f'URL {url} does not exist.')
return url
@env.macro
def sourcefile(filename, branch=None, raw=False):
"""Return a link to a file within the source code repository.
Arguments:
- filename: The name of the file to link to (relative to the top-level directory)
Returns:
- A fully qualified URL to the source code file on GitHub
Raises:
- FileNotFoundError: If the file does not exist, or the generated URL is invalid
"""
if branch == None:
branch = get_build_enviroment()
if filename.startswith('/'):
filename = filename[1:]
# This file exists at ./docs/main.py, so any file we link to must be relative to the top-level directory
here = os.path.dirname(__file__)
root = os.path.abspath(os.path.join(here, '..'))
file_path = os.path.join(root, filename)
if not os.path.exists(file_path):
raise FileNotFoundError(f'Source file {filename} does not exist.')
repo_url = get_repo_url(raw=raw)
if raw:
url = f'{repo_url}/{branch}/{filename}'
else:
url = f'{repo_url}/blob/{branch}/{filename}'
# Check that the URL exists before returning it
if not check_link(url):
raise FileNotFoundError(f'URL {url} does not exist.')
return url
@env.macro
def invoke_commands():
"""Provides an output of the available commands."""
here = os.path.dirname(__file__)
base = os.path.join(here, '..')
base = os.path.abspath(base)
tasks = os.path.join(base, 'tasks.py')
output = os.path.join(here, 'invoke-commands.txt')
command = f'invoke -f {tasks} --list > {output}'
assert subprocess.call(command, shell=True) == 0
with open(output, 'r') as f:
content = f.read()
return content
@env.macro
def listimages(subdir):
"""Return a listing of all asset files in the provided subdir."""

View File

@@ -1326,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.4'
INVENTREE_SW_VERSION = '0.15.8'
# Discover git
try:

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

@@ -299,15 +299,12 @@ def annotate_default_location(reference=''):
rght__gt=OuterRef(f'{reference}rght'),
level__lte=OuterRef(f'{reference}level'),
parent__isnull=False,
)
default_location__isnull=False,
).order_by('-level')
return Coalesce(
F(f'{reference}default_location'),
Subquery(
subquery.order_by('-level')
.filter(default_location__isnull=False)
.values('default_location')
),
Subquery(subquery.values('default_location')[:1]),
Value(None),
output_field=IntegerField(),
)

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

@@ -505,6 +505,82 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
self.assertEqual(item['parent'], parent)
self.assertEqual(item['subcategories'], subcategories)
def test_part_category_default_location(self):
"""Test default location propagation through location trees."""
"""Making a tree structure like this:
main
loc 2
sub1
sub2
loc 3
sub3
loc 4
sub4
sub5
Expected behaviour:
main parent loc: Out of test scope. Parent category data not controlled by the test
sub1 parent loc: loc 2
sub2 parent loc: loc 2
sub3 parent loc: loc 3
sub4 parent loc: loc 4
sub5 parent loc: loc 3
"""
main = PartCategory.objects.create(
name='main',
parent=PartCategory.objects.first(),
default_location=StockLocation.objects.get(id=2),
)
sub1 = PartCategory.objects.create(name='sub1', parent=main)
sub2 = PartCategory.objects.create(
name='sub2', parent=sub1, default_location=StockLocation.objects.get(id=3)
)
sub3 = PartCategory.objects.create(
name='sub3', parent=sub2, default_location=StockLocation.objects.get(id=4)
)
sub4 = PartCategory.objects.create(name='sub4', parent=sub3)
sub5 = PartCategory.objects.create(name='sub5', parent=sub2)
part = Part.objects.create(name='test', category=sub4)
PartCategory.objects.rebuild()
# This query will trigger an internal server error if annotation results are not limited to 1
url = reverse('api-part-list')
response = self.get(url, expected_code=200)
# sub1, expect main to be propagated
url = reverse('api-part-category-detail', kwargs={'pk': sub1.pk})
response = self.get(url, expected_code=200)
self.assertEqual(
response.data['parent_default_location'], main.default_location.pk
)
# sub2, expect main to be propagated
url = reverse('api-part-category-detail', kwargs={'pk': sub2.pk})
response = self.get(url, expected_code=200)
self.assertEqual(
response.data['parent_default_location'], main.default_location.pk
)
# sub3, expect sub2 to be propagated
url = reverse('api-part-category-detail', kwargs={'pk': sub3.pk})
response = self.get(url, expected_code=200)
self.assertEqual(
response.data['parent_default_location'], sub2.default_location.pk
)
# sub4, expect sub3 to be propagated
url = reverse('api-part-category-detail', kwargs={'pk': sub4.pk})
response = self.get(url, expected_code=200)
self.assertEqual(
response.data['parent_default_location'], sub3.default_location.pk
)
# sub5, expect sub2 to be propagated
url = reverse('api-part-category-detail', kwargs={'pk': sub5.pk})
response = self.get(url, expected_code=200)
self.assertEqual(
response.data['parent_default_location'], sub2.default_location.pk
)
class PartOptionsAPITest(InvenTreeAPITestCase):
"""Tests for the various OPTIONS endpoints in the /part/ API.

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

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

@@ -2,6 +2,7 @@
from django import template
from django.conf import settings as djangosettings
from django.templatetags.static import static
from django.urls import reverse
from common.models import InvenTreeSetting
@@ -96,3 +97,17 @@ def notification_list(context, *args, **kwargs):
}
for a in storage.liste
]
@register.simple_tag()
def plugin_static(file: str, **kwargs):
"""Return the URL for a static file within a plugin.
Arguments:
file: The path to the file within the plugin static directory
Keyword Arguments:
plugin: The plugin slug (optional, will be inferred from the context if not provided)
"""
return static(file)

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

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

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

@@ -454,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={
@@ -1215,6 +1221,34 @@ def frontend_download(
handle_extract(dst.name)
def check_already_current(tag=None, sha=None):
"""Check if the currently available frontend is already the requested one."""
ref = 'tag' if tag else 'commit'
if tag:
current = managePyDir().joinpath('web', 'static', 'web', '.vite', 'tag.txt')
elif sha:
current = managePyDir().joinpath('web', 'static', 'web', '.vite', 'sha.txt')
else:
raise ValueError('Either tag or sha needs to be set')
if not current.exists():
print(
f'Current frontend information for {ref} is not available - this is expected in some cases'
)
return False
current_content = current.read_text().strip()
ref_value = tag or sha
if current_content == ref_value:
print(f'Frontend {ref} is already `{ref_value}`')
return True
else:
print(
f'Frontend {ref} is not expected `{ref_value}` but `{current_content}` - new version will be downloaded'
)
return False
# if zip file is specified, try to extract it directly
if file:
handle_extract(file)
@@ -1231,8 +1265,24 @@ def frontend_download(
['git', 'rev-parse', 'HEAD'], encoding='utf-8'
).strip()
except Exception:
print("[ERROR] Cannot get current ref via 'git rev-parse HEAD'")
return
# .deb Packages contain extra information in the VERSION file
version_file = localDir().joinpath('VERSION')
if not version_file.exists():
return
from dotenv import dotenv_values # noqa: WPS433
content = dotenv_values(version_file)
if (
'INVENTREE_PKG_INSTALLER' in content
and content['INVENTREE_PKG_INSTALLER'] == 'PKG'
):
ref = content.get('INVENTREE_COMMIT_SHA')
print(
f'[INFO] Running in package environment, got commit "{ref}" from VERSION file'
)
else:
print("[ERROR] Cannot get current ref via 'git rev-parse HEAD'")
return
if ref is None and tag is None:
print('[ERROR] Either ref or tag needs to be set.')
@@ -1240,6 +1290,8 @@ def frontend_download(
if tag:
tag = tag.lstrip('v')
try:
if check_already_current(tag=tag):
return
handle_download(
f'https://github.com/{repo}/releases/download/{tag}/frontend-build.zip'
)
@@ -1255,6 +1307,8 @@ Then try continuing by running: invoke frontend-download --file <path-to-downloa
return
if ref:
if check_already_current(sha=ref):
return
# get workflow run from all workflow runs on that particular ref
workflow_runs = requests.get(
f'https://api.github.com/repos/{repo}/actions/runs?head_sha={ref}',