Merge branch 'master' into pui-plugins

This commit is contained in:
Oliver Walters 2024-08-11 06:23:51 +00:00
commit 3c695870da
630 changed files with 300583 additions and 174725 deletions

View File

@ -1,5 +1,3 @@
version: "3"
services:
db:
image: postgres:13

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -13,5 +13,5 @@ runs:
invoke export-records -f data.json
python3 ./src/backend/InvenTree/manage.py flush --noinput
invoke migrate
invoke import-records -f data.json
invoke import-records -f data.json
invoke import-records -c -f data.json
invoke import-records -c -f data.json

View File

@ -44,6 +44,11 @@ runs:
with:
python-version: ${{ env.python_version }}
cache: pip
cache-dependency-path: |
src/backend/requirements.txt
src/backend/requirements-dev.txt
contrib/container/requirements.txt
contrib/dev_reqs/requirements.txt
- name: Install Base Python Dependencies
if: ${{ inputs.python == 'true' }}
shell: bash
@ -93,4 +98,4 @@ runs:
- name: Run invoke update
if: ${{ inputs.update == 'true' }}
shell: bash
run: invoke update --uv
run: invoke update --uv --skip-backup --skip-static

View File

@ -18,7 +18,7 @@ updates:
directories:
- /contrib/container
- /docs
- /.github
- /contrib/dev_reqs
- /src/backend
schedule:
interval: weekly

View File

@ -10,6 +10,7 @@ tagged branch:
"""
import itertools
import json
import os
import re
@ -18,8 +19,11 @@ from pathlib import Path
import requests
REPO = os.getenv('GITHUB_REPOSITORY', 'inventree/inventree')
GITHUB_API_URL = os.getenv('GITHUB_API_URL', 'https://api.github.com')
def get_existing_release_tags():
def get_existing_release_tags(include_prerelease=True):
"""Request information on existing releases via the GitHub API."""
# Check for github token
token = os.getenv('GITHUB_TOKEN', None)
@ -28,9 +32,7 @@ def get_existing_release_tags():
if token:
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(
'https://api.github.com/repos/inventree/inventree/releases', headers=headers
)
response = requests.get(f'{GITHUB_API_URL}/repos/{REPO}/releases', headers=headers)
if response.status_code != 200:
raise ValueError(
@ -50,6 +52,9 @@ def get_existing_release_tags():
print(f"Version '{tag}' did not match expected pattern")
continue
if not include_prerelease and release['prerelease']:
continue
tags.append([int(x) for x in match.groups()])
return tags
@ -73,7 +78,7 @@ def check_version_number(version_string, allow_duplicate=False):
version_tuple = [int(x) for x in match.groups()]
# Look through the existing releases
existing = get_existing_release_tags()
existing = get_existing_release_tags(include_prerelease=False)
# Assume that this is the highest release, unless told otherwise
highest_release = True
@ -90,6 +95,11 @@ def check_version_number(version_string, allow_duplicate=False):
if __name__ == '__main__':
# Ensure that we are running in GH Actions
if os.environ.get('GITHUB_ACTIONS', '') != 'true':
print('This script is intended to be run within a GitHub Action!')
sys.exit(1)
if 'only_version' in sys.argv:
here = Path(__file__).parent.absolute()
version_file = here.joinpath(
@ -97,16 +107,18 @@ if __name__ == '__main__':
)
text = version_file.read_text()
results = re.findall(r"""INVENTREE_API_VERSION = (.*)""", text)
# If 2. args is true lower the version number by 1
if len(sys.argv) > 2 and sys.argv[2] == 'true':
results[0] = str(int(results[0]) - 1)
print(results[0])
exit(0)
# GITHUB_REF_TYPE may be either 'branch' or 'tag'
GITHUB_REF_TYPE = os.environ['GITHUB_REF_TYPE']
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
GITHUB_REF = os.environ['GITHUB_REF']
GITHUB_REF_NAME = os.environ['GITHUB_REF_NAME']
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
# Print out version information, makes debugging actions *much* easier!
@ -187,10 +199,13 @@ if __name__ == '__main__':
print(f"Version check passed for '{version}'!")
print(f"Docker tags: '{docker_tags}'")
target_repos = [REPO.lower(), f'ghcr.io/{REPO.lower()}']
# Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/
with open(os.getenv('GITHUB_ENV'), 'a') as env_file:
# Construct tag string
tags = ','.join([f'inventree/inventree:{tag}' for tag in docker_tags])
tag_list = [[f'{r}:{t}' for t in docker_tags] for r in target_repos]
tags = ','.join(itertools.chain(*tag_list))
env_file.write(f'docker_tags={tags}\n')

View File

@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:

View File

@ -39,7 +39,7 @@ jobs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
id: filter
with:
@ -66,9 +66,9 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Set Up Python ${{ env.python_version }}
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
with:
python-version: ${{ env.python_version }}
- name: Version Check
@ -115,18 +115,19 @@ jobs:
- name: Run Unit Tests
run: |
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke test --disable-pty
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke test --migrations --disable-pty
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml down
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke test --disable-pty
- name: Run Migration Tests
run: |
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke test --migrations
- name: Clean up test folder
run: |
rm -rf InvenTree/_testfolder
- name: Set up QEMU
if: github.event_name != 'pull_request'
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # pin@v3.0.0
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@v3.2.0
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # pin@v3.3.0
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # pin@v3.6.1
- name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # pin@v3.5.0
@ -134,20 +135,20 @@ jobs:
id: docker_login
run: |
if [ -z "${{ secrets.DOCKER_USERNAME }}" ]; then
echo "skip_dockerhub_login=true" >> $GITHUB_ENV
echo "skip_dockerhub_login=true" >> $GITHUB_OUTPUT
else
echo "skip_dockerhub_login=false" >> $GITHUB_ENV
echo "skip_dockerhub_login=false" >> $GITHUB_OUTPUT
fi
- name: Login to Dockerhub
if: github.event_name != 'pull_request' && steps.docker_login.outputs.skip_dockerhub_login != 'true'
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # pin@v3.2.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # pin@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log into registry ghcr.io
if: github.event_name != 'pull_request'
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # pin@v3.2.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # pin@v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -165,7 +166,7 @@ jobs:
- name: Push Docker Images
id: push-docker
if: github.event_name != 'pull_request'
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # pin@v5.4.0
uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # pin@v6.5.0
with:
context: .
file: ./contrib/container/Dockerfile

View File

@ -10,11 +10,9 @@ on:
env:
python_version: 3.9
node_version: 18
node_version: 20
# The OS version must be set per job
server_start_sleep: 60
requests_version: 2.31.0
pyyaml_version: 6.0.1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INVENTREE_DB_ENGINE: sqlite3
@ -40,7 +38,7 @@ jobs:
force: ${{ steps.force.outputs.force }}
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
id: filter
with:
@ -72,7 +70,7 @@ jobs:
needs: ["pre-commit"]
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -94,9 +92,9 @@ jobs:
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
with:
python-version: ${{ env.python_version }}
cache: "pip"
@ -115,9 +113,9 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # pin@v5.1.0
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
with:
python-version: ${{ env.python_version }}
- name: Check Config
@ -151,7 +149,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -161,20 +159,32 @@ jobs:
- name: Export API Documentation
run: invoke schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
- name: Upload schema
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # pin@v4.3.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4.3.5
with:
name: schema.yml
path: src/backend/InvenTree/schema.yml
- name: Download public schema
if: needs.paths-filter.outputs.api == 'false'
run: |
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
version="$(python3 .github/scripts/version_check.py only_version ${{ needs.paths-filter.outputs.api }} 2>&1)"
echo "Version: $version"
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
echo "URL: $url"
curl -s -o api.yaml $url
code=$(curl -s -o api.yaml $url --write-out '%{http_code}' --silent)
if [ "$code" != "200" ]; then
exit 1
fi
echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action
id: breaking_changes
uses: oasdiff/oasdiff-action/diff@a2ff6682b27d175162a74c09ace8771bd3d512f8 # pin@main
with:
base: 'api.yaml'
revision: 'src/backend/InvenTree/schema.yml'
format: 'html'
- name: Echoing diff to step
run: echo "${{ steps.breaking_changes.outputs.diff }}" >> $GITHUB_STEP_SUMMARY
- name: Check for differences in API Schema
if: needs.paths-filter.outputs.api == 'false'
run: |
@ -201,12 +211,13 @@ jobs:
version: ${{ needs.schema.outputs.version }}
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
name: Checkout Code
with:
repository: inventree/schema
token: ${{ secrets.SCHEMA_PAT }}
- name: Download schema artifact
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: schema.yml
- name: Move schema to correct location
@ -215,8 +226,9 @@ jobs:
mkdir export/${version}
mv schema.yml export/${version}/api.yaml
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
name: Commit schema changes
with:
commit_message: "Update API schema for ${version}"
commit_message: "Update API schema for ${{ env.version }} / ${{ github.sha }}"
python:
name: Tests - inventree-python
@ -238,7 +250,7 @@ jobs:
INVENTREE_SITE_URL: http://127.0.0.1:12345
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -269,7 +281,8 @@ jobs:
continue-on-error: true # continue if a step fails so that coverage gets pushed
strategy:
matrix:
python_version: [3.9, 3.12]
python_version: [3.9]
# python_version: [3.9, 3.12] # Disabled due to requirement issues
env:
INVENTREE_DB_NAME: ./inventree.sqlite
@ -279,7 +292,7 @@ jobs:
python_version: ${{ matrix.python_version }}
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -295,7 +308,7 @@ jobs:
- name: Coverage Tests
run: invoke test --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # pin@v4.4.1
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -333,7 +346,7 @@ jobs:
- 6379:6379
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -377,7 +390,7 @@ jobs:
- 3306:3306
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -416,7 +429,7 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -427,7 +440,7 @@ jobs:
- name: Run Tests
run: invoke test --migrations --report --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # pin@v4.4.1
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -447,7 +460,7 @@ jobs:
INVENTREE_PLUGINS_ENABLED: false
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
name: Checkout Code
- name: Environment Setup
uses: ./.github/actions/setup
@ -504,7 +517,7 @@ jobs:
VITE_COVERAGE: true
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -522,7 +535,7 @@ jobs:
- name: Run Playwright tests
id: tests
run: cd src/frontend && npx nyc playwright test
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # pin@v4
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with:
name: playwright-report
@ -532,7 +545,7 @@ jobs:
if: always()
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # pin@v4.4.1
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -545,7 +558,7 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -554,11 +567,13 @@ 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
zip -r frontend-build.zip web/ web/.vite
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # pin@v4.3.3
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4.3.5
with:
name: frontend-build
path: src/backend/InvenTree/web/static/web

View File

@ -1,13 +1,16 @@
# Runs on releases
name: Publish release notes
name: Publish release
on:
release:
types: [published]
permissions:
contents: read
jobs:
stable:
runs-on: ubuntu-latest
name: Write release to stable branch
permissions:
contents: write
pull-requests: write
@ -15,7 +18,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Version Check
run: |
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
@ -28,13 +31,15 @@ jobs:
branch: stable
force: true
publish-build:
build:
runs-on: ubuntu-latest
name: Build and attest frontend
permissions:
id-token: write
contents: write
pull-requests: write
attestations: write
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -43,14 +48,38 @@ jobs:
run: cd src/frontend && yarn install
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
- 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
zip -r ../frontend-build.zip *
- uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
zip -r ../frontend-build.zip * .vite
- name: Attest Build Provenance
id: attest
uses: actions/attest-build-provenance@v1
with:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
- name: Upload frontend
uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: src/backend/InvenTree/web/static/frontend-build.zip
asset_name: frontend-build.zip
tag: ${{ github.ref }}
overwrite: true
- name: Upload Attestation
uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # pin@2.9.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
asset_name: frontend-build.intoto.jsonl
file: ${{ steps.attest.outputs.bundle-path}}
tag: ${{ github.ref }}
overwrite: true

View File

@ -32,12 +32,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: SARIF file
path: results.sarif
@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
with:
sarif_file: results.sarif

View File

@ -7,7 +7,7 @@ on:
env:
python_version: 3.9
node_version: 18
node_version: 20
permissions:
contents: read
@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- name: Environment Setup
uses: ./.github/actions/setup
with:

1
.gitignore vendored
View File

@ -111,3 +111,4 @@ InvenTree/web/static
docs/schema.yml
docs/docs/api/*.yml
docs/docs/api/schema/*.yml
inventree_settings.json

View File

@ -14,7 +14,10 @@ env:
- INVENTREE_BACKUP_DIR=/opt/inventree/backup
- INVENTREE_PLUGIN_FILE=/opt/inventree/plugins.txt
- INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
- APP_REPO=inventree/InvenTree
before_install: contrib/packager.io/preinstall.sh
after_install: contrib/packager.io/postinstall.sh
before_remove: contrib/packager.io/preinstall.sh
before:
- contrib/packager.io/before.sh
dependencies:
@ -32,7 +35,7 @@ dependencies:
- gettext
- nginx
- jq
- libffi7
- "libffi7 | libffi8"
targets:
ubuntu-20.04: true
debian-11: true

View File

@ -17,7 +17,7 @@ repos:
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1
rev: v0.5.1
hooks:
- id: ruff-format
args: [--preview]
@ -27,42 +27,46 @@ repos:
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.1.35
rev: 0.2.13
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --python-version=3.9, --no-strip-extras, --generate-hashes]
args: [src/backend/requirements-dev.in, -o, src/backend/requirements-dev.txt, --no-strip-extras, --generate-hashes]
files: src/backend/requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [src/backend/requirements.in, -o, src/backend/requirements.txt,--python-version=3.9, --no-strip-extras,--generate-hashes]
args: [src/backend/requirements.in, -o, src/backend/requirements.txt, --no-strip-extras, --generate-hashes]
files: src/backend/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt, --no-strip-extras, --generate-hashes]
files: contrib/dev_reqs/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [docs/requirements.in, -o, docs/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
args: [docs/requirements.in, -o, docs/requirements.txt, --no-strip-extras, --generate-hashes]
files: docs/requirements\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt,--python-version=3.11, --no-strip-extras, --generate-hashes]
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes]
files: contrib/container/requirements\.(in|txt)$
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.1
hooks:
- id: djlint-django
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:
- tomli
exclude: >
(?x)^(
docs/docs/stylesheets/.*|
docs/docs/javascripts/.*|
docs/docs/webfonts/.* |
src/frontend/src/locales/.* |
pyproject.toml |
src/frontend/vite.config.ts |
)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
@ -73,7 +77,7 @@ repos:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.1.0"
rev: "v9.6.0"
hooks:
- id: eslint
additional_dependencies:
@ -85,7 +89,7 @@ repos:
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
rev: v8.18.4
hooks:
- id: gitleaks
#- repo: https://github.com/jumanjihouse/pre-commit-hooks

26
.vscode/launch.json vendored
View File

@ -6,19 +6,37 @@
"configurations": [
{
"name": "InvenTree Server",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": ["runserver"],
"args": [
"runserver",
// "0.0.0.0:8000", // expose server in network (useful for testing with mobile app)
// "--noreload" // disable auto-reload
],
"django": true,
"justMyCode": true
},
{
"name": "InvenTree Server - Tests",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": [
"test",
// "part.test_api.PartCategoryAPITest", // run only a specific test
],
"django": true,
"justMyCode": true
},
{
"name": "InvenTree Server - 3rd party",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": ["runserver"],
"args": [
"runserver"
],
"django": true,
"justMyCode": false
},

View File

@ -39,7 +39,7 @@ InvenTree/
│ │ ├─ tsconfig.json # Settings for frontend compilation
├─ .pkgr.yml # Build definition for Debian/Ubuntu packages
├─ .pre-commit-config.yaml # Code formatter/linter configuration
├─ CONTRIBUTING.md # Contirbution guidelines and overview
├─ CONTRIBUTING.md # Contribution guidelines and overview
├─ Procfile # Process definition for Debian/Ubuntu packages
├─ README.md # General project information and overview
├─ runtime.txt # Python runtime settings for Debian/Ubuntu packages build

View File

@ -66,7 +66,7 @@ InvenTree is designed to be **extensible**, and provides multiple options for **
<li><a href="https://www.djangoproject.com/">Django</a></li>
<li><a href="https://www.django-rest-framework.org/">DRF</a></li>
<li><a href="https://django-q.readthedocs.io/">Django Q</a></li>
<li><a href="https://django-allauth.readthedocs.io/">Django-Allauth</a></li>
<li><a href="https://docs.allauth.org/">Django-Allauth</a></li>
</ul>
</details>

View File

@ -3,6 +3,7 @@ coverage:
project:
default:
target: 82%
patch: off
github_checks:
annotations: true

View File

@ -11,6 +11,7 @@
ARG base_image=python:3.11-alpine3.18
FROM ${base_image} AS inventree_base
ARG base_image
# Build arguments for this image
ARG commit_tag=""
@ -48,13 +49,18 @@ ENV INVENTREE_BACKGROUND_WORKERS="4"
ENV INVENTREE_WEB_ADDR=0.0.0.0
ENV INVENTREE_WEB_PORT=8000
LABEL org.label-schema.schema-version="1.0" \
org.label-schema.build-date=${DATE} \
org.label-schema.vendor="inventree" \
org.label-schema.name="inventree/inventree" \
org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
org.label-schema.vcs-url="https://github.com/inventree/InvenTree.git" \
org.label-schema.vcs-ref=${commit_tag}
LABEL org.opencontainers.image.created=${DATE} \
org.opencontainers.image.vendor="inventree" \
org.opencontainers.image.title="InvenTree backend server" \
org.opencontainers.image.description="InvenTree is the open-source inventory management system" \
org.opencontainers.image.url="https://inventree.org" \
org.opencontainers.image.documentation="https://docs.inventree.org" \
org.opencontainers.image.source="https://github.com/inventree/InvenTree" \
org.opencontainers.image.revision=${commit_hash} \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.base.name="docker.io/library/${base_image}" \
org.opencontainers.image.version=${commit_tag}
# Install required system level packages
RUN apk add --no-cache \

View File

@ -1,5 +1,3 @@
version: "3.8"
# Docker compose recipe for InvenTree development server
# - Runs PostgreSQL as the database backend
# - Uses built-in django webserver

View File

@ -1,5 +1,3 @@
version: "3.8"
# Docker compose recipe for a production-ready InvenTree setup, with the following containers:
# - PostgreSQL as the database backend
# - gunicorn as the InvenTree web server

View File

@ -1,7 +1,7 @@
#!/bin/ash
# Install system packages required for building InvenTree python libraries
# Note that for postgreslql, we use the 13 version, which matches the version used in the InvenTree docker image
# Note that for postgreslql, we use the version 13, which matches the version used in the InvenTree docker image
apk add gcc g++ musl-dev openssl-dev libffi-dev cargo python3-dev openldap-dev \
libstdc++ build-base linux-headers py3-grpcio \

View File

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

View File

@ -1,4 +1,4 @@
# Packages needed for CI/packages
requests==2.32.2
requests==2.32.3
pyyaml==6.0.1
jc==1.25.2
jc==1.25.3

View File

@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# 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
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt --no-strip-extras --generate-hashes
certifi==2024.7.4 \
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
# via requests
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
@ -100,9 +100,10 @@ idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
# via requests
jc==1.25.2 \
--hash=sha256:26e412a65a478f9da3097653db6277f915cfae5c0f0a3f42026b405936abd358 \
--hash=sha256:97ada193495f79550f06fe0cbfb119ff470bcca57c1cc593a5cdb0008720e0b3
jc==1.25.3 \
--hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \
--hash=sha256:fa3140ceda6cba1210d1362f363cd79a0514741e8a1dd6167db2b2e2d5f24f7b
# via -r contrib/dev_reqs/requirements.in
pygments==2.17.2 \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
@ -159,9 +160,11 @@ pyyaml==6.0.1 \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
requests==2.32.2 \
--hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \
--hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c
# via -r contrib/dev_reqs/requirements.in
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via -r contrib/dev_reqs/requirements.in
ruamel-yaml==0.18.6 \
--hash=sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636 \
--hash=sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b
@ -218,9 +221,9 @@ ruamel-yaml-clib==0.2.8 \
--hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \
--hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412
# via ruamel-yaml
urllib3==2.2.1 \
--hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \
--hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via requests
xmltodict==0.13.0 \
--hash=sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56 \

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

View File

@ -4,6 +4,8 @@
#
Color_Off='\033[0m'
On_Red='\033[41m'
PYTHON_FROM=9
PYTHON_TO=12
function detect_docker() {
if [ -n "$(grep docker </proc/1/cgroup)" ]; then
@ -57,6 +59,19 @@ function detect_python() {
echo "# No python environment found - using environment variable: ${SETUP_PYTHON}"
fi
# Try to detect a python between 3.9 and 3.12 in reverse order
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "# Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
echo "# Checking for python3.${i}"
if [ -n "$(which python3.${i})" ]; then
SETUP_PYTHON="python3.${i}"
echo "# Found python3.${i} installed - using for setup ${SETUP_PYTHON}"
break
fi
done
fi
# Ensure python can be executed - abort if not
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "${On_Red}"
@ -117,22 +132,22 @@ function detect_envs() {
pip install --require-hashes -r ${APP_HOME}/contrib/dev_reqs/requirements.txt -q
# Load config
local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
export INVENTREE_CONF_DATA=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
# Parse the config file
export INVENTREE_MEDIA_ROOT=$(jq -r '.[].media_root' <<< ${CONF})
export INVENTREE_STATIC_ROOT=$(jq -r '.[].static_root' <<< ${CONF})
export INVENTREE_BACKUP_DIR=$(jq -r '.[].backup_dir' <<< ${CONF})
export INVENTREE_PLUGINS_ENABLED=$(jq -r '.[].plugins_enabled' <<< ${CONF})
export INVENTREE_PLUGIN_FILE=$(jq -r '.[].plugin_file' <<< ${CONF})
export INVENTREE_SECRET_KEY_FILE=$(jq -r '.[].secret_key_file' <<< ${CONF})
export INVENTREE_MEDIA_ROOT=$(jq -r '.[].media_root' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_STATIC_ROOT=$(jq -r '.[].static_root' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_BACKUP_DIR=$(jq -r '.[].backup_dir' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_PLUGINS_ENABLED=$(jq -r '.[].plugins_enabled' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_PLUGIN_FILE=$(jq -r '.[].plugin_file' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_SECRET_KEY_FILE=$(jq -r '.[].secret_key_file' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_ENGINE=$(jq -r '.[].database.ENGINE' <<< ${CONF})
export INVENTREE_DB_NAME=$(jq -r '.[].database.NAME' <<< ${CONF})
export INVENTREE_DB_USER=$(jq -r '.[].database.USER' <<< ${CONF})
export INVENTREE_DB_PASSWORD=$(jq -r '.[].database.PASSWORD' <<< ${CONF})
export INVENTREE_DB_HOST=$(jq -r '.[].database.HOST' <<< ${CONF})
export INVENTREE_DB_PORT=$(jq -r '.[].database.PORT' <<< ${CONF})
export INVENTREE_DB_ENGINE=$(jq -r '.[].database.ENGINE' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_NAME=$(jq -r '.[].database.NAME' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_USER=$(jq -r '.[].database.USER' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_PASSWORD=$(jq -r '.[].database.PASSWORD' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_HOST=$(jq -r '.[].database.HOST' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_PORT=$(jq -r '.[].database.PORT' <<< ${INVENTREE_CONF_DATA})
else
echo "# No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
@ -231,7 +246,11 @@ function create_initscripts() {
}
function create_admin() {
# Create data for admin user
# Create data for admin users - stop with setting SETUP_ADMIN_NOCREATION to true
if [ "${SETUP_ADMIN_NOCREATION}" == "true" ]; then
echo "# Admin creation is disabled - skipping"
return
fi
if test -f "${SETUP_ADMIN_PASSWORD_FILE}"; then
echo "# Admin data already exists - skipping"
@ -299,17 +318,17 @@ function set_env() {
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE}
# Database engine
sed -i s=#ENGINE:\ sampleengine=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ ENGINE:\ Database\ engine.\ Selection\ from:=ENGINE:\ ${INVENTREE_DB_ENGINE}=g ${INVENTREE_CONFIG_FILE}
# Database name
sed -i s=#NAME:\ \'/path/to/database\'=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ NAME:\ Database\ name=NAME:\ \'${INVENTREE_DB_NAME}\'=g ${INVENTREE_CONFIG_FILE}
# Database user
sed -i s=#USER:\ sampleuser=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ USER:\ Database\ username\ \(if\ required\)=USER:\ ${INVENTREE_DB_USER}=g ${INVENTREE_CONFIG_FILE}
# Database password
sed -i s=#PASSWORD:\ samplepassword=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ PASSWORD:\ Database\ password\ \(if\ required\)=PASSWORD:\ ${INVENTREE_DB_PASSWORD}=g ${INVENTREE_CONFIG_FILE}
# Database host
sed -i s=#HOST:\ samplehost=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ HOST:\ Database\ host\ address\ \(if\ required\)=HOST:\ ${INVENTREE_DB_HOST}=g ${INVENTREE_CONFIG_FILE}
# Database port
sed -i s=#PORT:\ 123456=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
sed -i s=#\ PORT:\ Database\ host\ port\ \(if\ required\)=PORT:\ ${INVENTREE_DB_PORT}=g ${INVENTREE_CONFIG_FILE}
# Fixing the permissions
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
@ -340,3 +359,44 @@ function final_message() {
echo -e " Password: ${INVENTREE_ADMIN_PASSWORD}"
echo -e "####################################################################################"
}
function update_checks() {
echo "# Running upgrade"
local old_version=$1
local old_version_rev=$(echo ${old_version} | cut -d'-' -f1 | cut -d'.' -f2)
echo "# Old version is: ${old_version} - release: ${old_version_rev}"
local ABORT=false
function check_config_value() {
local env_key=$1
local config_key=$2
local name=$3
local value=$(inventree config:get ${env_key})
if [ -z "${value}" ] || [ "$value" == "null" ]; then
value=$(jq -r ".[].${config_key}" <<< ${INVENTREE_CONF_DATA})
fi
if [ -z "${value}" ] || [ "$value" == "null" ]; then
echo "# No setting for ${name} found - please set it manually either in ${INVENTREE_CONFIG_FILE} under '${config_key}' or with 'inventree config:set ${env_key}=value'"
ABORT=true
else
echo "# Found setting for ${name} - ${value}"
fi
}
# Custom checks if old version is below 0.8.0
if [ "${old_version_rev}" -lt "9" ]; then
echo "# Old version is below 0.9.0 - You might be missing some configs"
# Check for BACKUP_DIR and SITE_URL in INVENTREE_CONF_DATA and config
check_config_value "INVENTREE_SITE_URL" "site_url" "site URL"
check_config_value "INVENTREE_BACKUP_DIR" "backup_dir" "backup dir"
if [ "${ABORT}" = true ]; then
echo "# Aborting - please set the missing values and run the update again"
exit 1
fi
echo "# All checks passed - continuing with the update"
fi
}

View File

@ -11,7 +11,7 @@ PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
. ${APP_HOME}/contrib/packager.io/functions.sh
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
# Get the envs
detect_local_env
@ -24,6 +24,7 @@ export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.c
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false}
export SETUP_PYTHON=${SETUP_PYTHON:-python3.9}
export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
# SETUP_DEBUG can be set to get debug info
# SETUP_EXTRA_PIP can be set to install extra pip packages
# SETUP_PYTHON can be set to use a different python version
@ -35,6 +36,14 @@ detect_initcmd
detect_ip
detect_python
# Check if we are updating and need to alert
echo "# Checking if update checks are needed"
if [ -z "$2" ]; then
echo "# Normal install - no need for checks"
else
update_checks $2
fi
# create processes
create_initscripts
create_admin

View File

@ -0,0 +1,15 @@
#!/bin/bash
#
# packager.io preinstall/preremove script
#
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON
if test -f "${APP_HOME}/env/bin/pip"; then
echo "# Clearing precompiled files"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke clear-generated"
else
echo "# No python environment found - skipping"
fi

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@ -4,7 +4,11 @@ title: Internal Barcodes
## Internal Barcodes
InvenTree defines an internal format for generating barcodes for various items. This format uses a simple JSON-style string to uniquely identify an item in the database.
InvenTree ships with two integrated internal formats for generating barcodes for various items which are available through the built-in InvenTree Barcode plugin. The used format can be selected through the plugin settings of the InvenTree Barcode plugin.
### 1. JSON-based QR Codes
This format uses a simple JSON-style string to uniquely identify an item in the database.
Some simple examples of this format are shown below:
@ -12,10 +16,49 @@ Some simple examples of this format are shown below:
| --- | --- |
| Part | `{% raw %}{"part": 10}{% endraw %}` |
| Stock Item | `{% raw %}{"stockitem": 123}{% endraw %}` |
| Stock Location | `{% raw %}{"stocklocation": 1}{% endraw %}` |
| Supplier Part | `{% raw %}{"supplierpart": 99}{% endraw %}` |
The numerical ID value used is the *Primary Key* (PK) of the particular object in the database.
#### Downsides
1. The JSON format includes binary only characters (`{% raw %}{{% endraw %}` and `{% raw %}"{% endraw %}`) which requires unnecessary use of the binary QR code encoding which means fewer amount of chars can be encoded with the same version of QR code.
2. The model name key has not a fixed length. Some model names are longer than others. E.g. a part QR code with the shortest possible id requires 11 chars, while a stock location QR code with the same id would already require 20 chars, which already requires QR code version 2 and quickly version 3.
!!! info "QR code versions"
There are 40 different qr code versions from 1-40. They all can encode more data than the previous version, but require more "squares". E.g. a V1 QR codes has 21x21 "squares" while a V2 already has 25x25. For more information see [QR code comparison](https://www.qrcode.com/en/about/version.html).
For a more detailed size analysis of the JSON-based QR codes refer to [this issue](https://github.com/inventree/InvenTree/issues/6612).
### 2. Short alphanumeric QR Codes
While JSON-based QR Codes encode all necessary information, they come with the described downsides. This new, short, alphanumeric only format is build to improve those downsides. The basic format uses an alphanumeric string: `INV-??x`
- `INV-` is a constant prefix. This is configurable in the InvenTree Barcode plugins settings per instance to support environments that use multiple instances.
- `??` is a two character alphanumeric (`0-9A-Z $%*+-./:` (45 chars)) code, individual to each model.
- `x` the actual pk of the model.
Now with an overhead of 6 chars for every model, this format supports the following amount of model instances using the described QR code modes:
| QR code mode | Alphanumeric mode | Mixed mode |
| --- | --- | --- |
| v1 M ECL (15%) | `10**14` items (~3.170 items per sec for 1000 years) | `10**20` items (~3.170.979.198 items per sec for 1000 years) |
| v1 Q ECL (25%) | `10**10` items (~0.317 items per sec for 1000 years) | `10**13` items (~317 items per sec for 1000 years) |
| v1 H ECL (30%) | `10**4` items (~100 items per day for 100 days) | `10**3` items (~100 items per day for 10 days (*even worse*)) |
!!! info "QR code mixed mode"
Normally the QR code data is encoded only in one format (binary, alphanumeric, numeric). But the data can also be split into multiple chunks using different formats. This is especially useful with long model ids, because the first 6 chars can be encoded using the alphanumeric mode and the id using the more efficient numeric mode. Mixed mode is used by default, because the `qrcode` template tag uses a default value for optimize of 1.
Some simple examples of this format are shown below:
| Model Type | Example Barcode |
| --- | --- |
| Part | `INV-PA10` |
| Stock Item | `INV-SI123` |
| Stock Location | `INV-SL1` |
| Supplier Part | `INV-SP99` |
## Report Integration
This barcode format can be used to generate 1D or 2D barcodes (e.g. for [labels and reports](../report/barcodes.md))

View File

@ -26,14 +26,6 @@ To navigate to the Build Order display, select *Build* from the main navigation
{% include "img.html" %}
{% endwith %}
#### Tree View
*Tree View* also provides a tabulated view of Build Orders. Orders are displayed in a hierarchical manner, showing any parent / child relationships between different build orders.
{% with id="build_tree", url="build/build_tree.png", description="Build Tree" %}
{% include "img.html" %}
{% endwith %}
#### Calendar View
*Calendar View* shows a calendar display with upcoming build orders, based on the various dates specified for each build.
@ -74,10 +66,11 @@ Each *Build Order* has an associated *Status* flag, which indicates the state of
| Status | Description |
| ----------- | ----------- |
| `Pending` | Build has been created and build is ready for subpart allocation |
| `Production` | One or more build outputs have been created for this build |
| `Cancelled` | Build has been cancelled |
| `Completed` | Build has been completed |
| `Pending` | Build order has been created, but is not yet in production |
| `Production` | Build order is currently in production |
| `On Hold` | Build order has been placed on hold, but is still active |
| `Cancelled` | Build order has been cancelled |
| `Completed` | Build order has been completed |
**Source Code**
@ -111,41 +104,29 @@ For further information, refer to the [stock allocation documentation](./allocat
## Build Order Display
The detail view for a single build order provides multiple display tabs, as follows:
The detail view for a single build order provides multiple display panels, as follows:
### Build Details
The *Build Details* tab provides an overview of the Build Order:
The *Build Details* panel provides an overview of the Build Order:
{% with id="build_details", url="build/build_details.png", description="Details tab" %}
{% with id="build_details", url="build/build_panel_details.png", description="Build details panel" %}
{% include "img.html" %}
{% endwith %}
### Allocate Stock
### Line Items
The *Allocate Stock* tab provides an interface to allocate required stock (as specified by the BOM) to the build:
The *Line Items* panel displays all the line items (as defined by the [bill of materials](./bom.md)) required to complete the build order.
{% with id="build_allocate", url="build/build_allocate.png", description="Allocation tab" %}
{% with id="build_allocate", url="build/build_panel_line_items.png", description="Build line items panel" %}
{% include "img.html" %}
{% endwith %}
The allocation table (as shown above) shows the stock allocation progress for this build. In the example above, there are two BOM lines, which have been partially allocated.
The allocation table (as shown above) provides an interface to allocate required stock, and also shows the stock allocation progress for each line item in the build.
!!! info "Completed Builds"
The *Allocate Stock* tab is not available if the build has been completed!
### Incomplete Outputs
### Consumed Stock
The *Consumed Stock* tab displays all stock items which have been *consumed* by this build order. These stock items remain in the database after the build order has been completed, but are no longer available for use.
- [Tracked stock items](./allocate.md#tracked-stock) are consumed by specific build outputs
- [Untracked stock items](./allocate.md#untracked-stock) are consumed by the build order
### Build Outputs
The *Build Outputs* tab shows the [build outputs](./output.md) (created stock items) associated with this build.
As shown below, there are separate panels for *incomplete* and *completed* build outputs.
The *Incomplete Outputs* panel shows the list of in-progress [build outputs](./output.md) (created stock items) associated with this build.
{% with id="build_outputs", url="build/build_outputs.png", description="Outputs tab" %}
{% include "img.html" %}
@ -158,11 +139,48 @@ As shown below, there are separate panels for *incomplete* and *completed* build
- Outputs which are "in progress" can be completed or cancelled
- Completed outputs (which are simply *stock items*) can be viewed in the stock table at the bottom of the screen
### Completed Outputs
This panel displays all the completed build outputs (stock items) which have been created by this build order:
### Allocated Stock
The *Allocated Stock* tab displays all stock items which have been *allocated* to this build order. These stock items are reserved for this build, and will be consumed when the build is completed:
{% with id="allocated_stock_table", url="build/allocated_stock_table.png", description="Allocated Stock Table" %}
{% include "img.html" %}
{% endwith %}
### Consumed Stock
The *Consumed Stock* tab displays all stock items which have been *consumed* by this build order. These stock items remain in the database after the build order has been completed, but are no longer available for use.
- [Tracked stock items](./allocate.md#tracked-stock) are consumed by specific build outputs
- [Untracked stock items](./allocate.md#untracked-stock) are consumed by the build order
### Child Builds
If there exist any build orders which are *children* of the selected build order, they are displayed in the *Child Builds* tab:
{% with id="build_childs", url="build/build_childs.png", description="Child builds tab" %}
{% with id="build_childs", url="build/build_childs.png", description="Child builds panel" %}
{% include "img.html" %}
{% endwith %}
### Test Results
For *trackable* parts, test results can be recorded against each build output. These results are displayed in the *Test Results* panel:
{% with id="build_test_results", url="build/build_panel_test_results.png", description="Test Results panel" %}
{% include "img.html" %}
{% endwith %}
This table provides a summary of the test results for each build output, and allows test results to be quickly added for each build output.
### Test Statistics
For *trackable* parts, this panel displays a summary of the test results for all build outputs:
{% with id="build_test_stats", url="build/build_panel_test_statistics.png", description="Test Statistics panel" %}
{% include "img.html" %}
{% endwith %}
@ -246,3 +264,17 @@ Build orders may (optionally) have a target complete date specified. If this dat
- Builds can be filtered by overdue status in the build list
- Overdue builds will be displayed on the home page
## Build Order Settings
The following [global settings](../settings/global.md) are available for adjusting the behavior of build orders:
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("BUILDORDER_REFERENCE_PATTERN") }}
{{ globalsetting("BUILDORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("BUILDORDER_REQUIRE_ACTIVE_PART") }}
{{ globalsetting("BUILDORDER_REQUIRE_LOCKED_PART") }}
{{ globalsetting("BUILDORDER_REQUIRE_VALID_BOM") }}
{{ globalsetting("BUILDORDER_REQUIRE_CLOSED_CHILDS") }}
{{ globalsetting("PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS") }}

View File

@ -4,4 +4,22 @@ title: InvenTree Demo
## InvenTree Demo
This page has moved to [https://inventree.org/demo.html](https://inventree.org/demo.html)
If you are interested in trying out InvenTree, you can access the InvenTree demo instance at [https://demo.inventree.org](https://demo.inventree.org).
This page is populated with a sample dataset, which is reset every 24 hours.
You can read more about the InvenTree demo here: [https://inventree.org/demo.html](https://inventree.org/demo.html)
### User Accounts
The demo instance has a number of user accounts which you can use to explore the system:
| Username | Password | Staff Access | Enabled | Description |
| -------- | -------- | ------------ | ------- | ----------- |
| allaccess | nolimits | No | Yes | View / create / edit all pages and items |
| reader | readonly | No | Yes | Can view all pages but cannot create, edit or delete database records |
| engineer | partsonly | No | Yes | Can manage parts, view stock, but no access to purchase orders or sales orders |
| steven | wizardstaff | Yes | Yes | Staff account, can access some admin sections |
| ian | inactive | No | No | Inactive account, cannot log in |
| susan | inactive | No | No | Inactive account, cannot log in |
| admin | inventree | Yes | Yes | Superuser account, can access all parts of the system |

View File

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

View File

@ -67,7 +67,7 @@ If you need to process your queue with background workers, run the `worker` task
You can either only run InvenTree or use the integrated debugger for debugging. Goto the `Run and debug` side panel make sure `InvenTree Server` is selected. Click on the play button on the left.
!!! tip "Debug with 3rd party"
Sometimes you need to debug also some 3rd party packages. Just select `InvenTree Servre - 3rd party`
Sometimes you need to debug also some 3rd party packages. Just select `InvenTree Server - 3rd party`
You can now set breakpoints and vscode will automatically pause execution if that point is hit. You can see all variables available in that context and evaluate some code with the debugger console at the bottom. Use the play or step buttons to continue execution.

View File

@ -6,6 +6,9 @@ title: Machines
InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
!!! info "Requires Redis"
If the machines features is used in production setup using workers, a shared [redis cache](../../start/docker.md#redis-cache) is required to function properly.
### Registry
The machine registry is the main component which gets initialized on server start and manages all configured machines.
@ -21,6 +24,13 @@ The machine registry initialization process can be divided into three stages:
2. The driver.init_driver function is called for each used driver
3. The machine.initialize function is called for each machine, which calls the driver.init_machine function for each machine, then the machine.initialized state is set to true
#### Production setup (with a worker)
If a worker is connected, there exist multiple instances of the machine registry (one in each worker thread and one in the main thread) due to the nature of how python handles state in different processes. Therefore the machine instances and drivers are instantiated multiple times (The `__init__` method is called multiple times). But the init functions and update hooks (e.g. `init_machine`) are only called once from the main process.
The registry, driver and machine state (e.g. machine status codes, errors, ...) is stored in the cache. Therefore a shared redis cache is needed. (The local in-memory cache which is used by default is not capable to cache across multiple processes)
### Machine types
Each machine type can provide a different type of connection functionality between inventree and a physical machine. These machine types are already built into InvenTree.
@ -86,6 +96,7 @@ The machine type class gets instantiated for each machine on server startup and
- update
- restart
- handle_error
- clear_errors
- get_setting
- set_setting
- check_setting

View File

@ -4,7 +4,7 @@ title: Barcode Mixin
## Barcode Plugins
InvenTree supports decoding of arbitrary barcode data via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
InvenTree supports decoding of arbitrary barcode data and generation of internal barcode formats via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
InvenTree can generate native QR codes to represent database objects (e.g. a single StockItem). This barcode can then be used to perform quick lookup of a stock item or location in the database. A client application (for example the InvenTree mobile app) scans a barcode, and sends the barcode data to the InvenTree server. The server then uses the **InvenTreeBarcodePlugin** (found at `src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py`) to decode the supplied barcode data.
@ -26,7 +26,7 @@ POST {
### Builtin Plugin
The InvenTree server includes a builtin barcode plugin which can decode QR codes generated by the server. This plugin is enabled by default.
The InvenTree server includes a builtin barcode plugin which can generate and decode the QR codes. This plugin is enabled by default.
::: plugin.builtin.barcodes.inventree_barcode.InvenTreeInternalBarcodePlugin
options:
@ -39,14 +39,12 @@ The InvenTree server includes a builtin barcode plugin which can decode QR codes
### Example Plugin
Please find below a very simple example that is executed each time a barcode is scanned.
Please find below a very simple example that is used to return a part if the barcode starts with `PART-`
```python
from django.utils.translation import gettext_lazy as _
from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin
from part.models import Part
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
@ -56,16 +54,39 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
VERSION = "0.0.1"
AUTHOR = "Michael"
status = 0
def scan(self, barcode_data):
if barcode_data.startswith("PART-"):
try:
pk = int(barcode_data.split("PART-")[1])
instance = Part.objects.get(pk=pk)
label = Part.barcode_model_type()
self.status = self.status+1
print('Started barcode plugin', self.status)
print(barcode_data)
response = {}
return response
return {label: instance.format_matched_response()}
except Part.DoesNotExist:
pass
```
To try it just copy the file to src/InvenTree/plugins and restart the server. Open the scan barcode window and start to scan codes or type in text manually. Each time the timeout is hit the plugin will execute and printout the result. The timeout can be changed in `Settings->Barcode Support->Barcode Input Delay`.
### Custom Internal Format
To implement a custom internal barcode format, the `generate(...)` method from the Barcode Mixin needs to be overridden. Then the plugin can be selected at `System Settings > Barcodes > Barcode Generation Plugin`.
```python
from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
NAME = "MyInternalBarcode"
TITLE = "My Internal Barcodes"
DESCRIPTION = "support for custom internal barcodes"
VERSION = "0.0.1"
AUTHOR = "InvenTree contributors"
def generate(self, model_instance: InvenTreeBarcodeMixin):
return f'{model_instance.barcode_model_type()}: {model_instance.pk}'
```
!!! info "Scanning implementation required"
The parsing of the custom format needs to be implemented too, so that the scanning of the generated QR codes resolves to the correct part.

View File

@ -0,0 +1,19 @@
---
title: Icon Pack Mixin
---
## IconPackMixin
The IconPackMixin class provides basic functionality for letting plugins expose custom icon packs that are available in the InvenTree UI. This is especially useful to provide a custom crafted icon pack with icons for different location types, e.g. different sizes and styles of drawers, bags, ESD bags, ... which are not available in the standard tabler icons library.
### Sample Plugin
The following example demonstrates how to use the `IconPackMixin` class to add a custom icon pack:
::: plugin.samples.icons.icon_sample.SampleIconPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

View File

@ -20,6 +20,7 @@ Each Purchase Order has a specific status code which indicates the current state
| --- | --- |
| Pending | The purchase order has been created, but has not been submitted to the supplier |
| In Progress | The purchase order has been issued to the supplier, and is in progress |
| On Hold | The purchase order has been placed on hold, but is still active |
| Complete | The purchase order has been completed, and is now closed |
| Cancelled | The purchase order was cancelled, and is now closed |
| Lost | The purchase order was lost, and is now closed |
@ -138,3 +139,14 @@ This view can be accessed externally as an ICS calendar using a URL like the fol
`http://inventree.example.org/api/order/calendar/purchase-order/calendar.ics`
by default, completed orders are not exported. These can be included by appending `?include_completed=True` to the URL.
## Purchase Order Settings
The following [global settings](../settings/global.md) are available for purchase orders:
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PURCHASEORDER_REFERENCE_PATTERN") }}
{{ globalsetting("PURCHASEORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }}
{{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }}

View File

@ -45,6 +45,7 @@ Each Return Order has a specific status code, as follows:
| --- | --- |
| Pending | The return order has been created, but not sent to the customer |
| In Progress | The return order has been issued to the customer |
| On Hold | The return order has been placed on hold, but is still active |
| Complete | The return order was marked as complete, and is now closed |
| Cancelled | The return order was cancelled, and is now closed |
@ -120,3 +121,14 @@ This view can be accessed externally as an ICS calendar using a URL like the fol
`http://inventree.example.org/api/order/calendar/return-order/calendar.ics`
by default, completed orders are not exported. These can be included by appending `?include_completed=True` to the URL.
## Return Order Settings
The following [global settings](../settings/global.md) are available for return orders:
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("RETURNORDER_ENABLED") }}
{{ globalsetting("RETURNORDER_REFERENCE_PATTERN") }}
{{ globalsetting("RETURNORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("RETURNORDER_EDIT_COMPLETED_ORDERS") }}

View File

@ -20,6 +20,7 @@ Each Sales Order has a specific status code, which represents the state of the o
| --- | --- |
| Pending | The sales order has been created, but has not been finalized or submitted |
| In Progress | The sales order has been issued, and is in progress |
| On Hold | The sales order has been placed on hold, but is still active |
| Shipped | The sales order has been shipped, but is not yet complete |
| Complete | The sales order is fully completed, and is now closed |
| Cancelled | The sales order was cancelled, and is now closed |
@ -182,3 +183,15 @@ All these fields can be edited by the user:
{% with id="edit-shipment", url="order/edit_shipment.png", description="Edit shipment" %}
{% include "img.html" %}
{% endwith %}
## Sales Order Settings
The following [global settings](../settings/global.md) are available for sales orders:
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("SALESORDER_REFERENCE_PATTERN") }}
{{ globalsetting("SALESORDER_REQUIRE_RESPONSIBLE") }}
{{ globalsetting("SALESORDER_DEFAULT_SHIPMENT") }}
{{ globalsetting("SALESORDER_EDIT_COMPLETED_ORDERS") }}
{{ globalsetting("SALESORDER_SHIP_COMPLETE") }}

View File

@ -73,7 +73,15 @@ A [Purchase Order](../order/purchase_order.md) allows parts to be ordered from a
If a part is designated as *Salable* it can be sold to external customers. Setting this flag allows parts to be added to sales orders.
### Active
## Locked Parts
Parts can be locked to prevent them from being modified. This is useful for parts which are in production and should not be changed. The following restrictions apply to parts which are locked:
- Locked parts cannot be deleted
- BOM items cannot be created, edited, or deleted when they are part of a locked assembly
- Part parameters linked to a locked part cannot be created, edited or deleted
## Active Parts
By default, all parts are *Active*. Marking a part as inactive means it is not available for many actions, but the part remains in the database. If a part becomes obsolete, it is recommended that it is marked as inactive, rather than deleting it from the database.

View File

@ -0,0 +1,78 @@
---
title: Part Revisions
---
## Part Revisions
When creating a complex part (such as an assembly comprised of other parts), it is often necessary to track changes to the part over time. For example, throughout the lifetime of an assembly, it may be necessary to adjust the bill of materials, or update the design of the part.
Rather than overwrite the existing part data, InvenTree allows you to create a new *revision* of the part. This allows you to track changes to the part over time, and maintain a history of the part design.
Crucially, creating a new *revision* ensures that any related data entries which refer to the original part (such as stock items, build orders, purchase orders, etc) are not affected by the change.
### Revisions are Parts
A *revision* of a part is itself a part. This means that each revision of a part has its own part number, stock items, parameters, bill of materials, etc. The only thing that differentiates a *revision* from any other part is that the *revision* is linked to the original part.
### Revision Fields
Each part has two fields which are used to track the revision of the part:
* **Revision**: The revision number of the part. This is a user-defined field, and can be any string value.
* **Revision Of**: A reference to the part of which *this* part is a revision. This field is used to keep track of the available revisions for any particular part.
### Revision Restrictions
When creating a new revision of a part, there are some restrictions which must be adhered to:
* **Circular References**: A part cannot be a revision of itself. This would create a circular reference which is not allowed.
* **Unique Revisions**: A part cannot have two revisions with the same revision number. Each revision (of a given part) must have a unique revision code.
* **Revisions of Revisions**: A single part can have multiple revisions, but a revision cannot have its own revision. This restriction is in place to prevent overly complex part relationships.
* **Template Revisions**: A part which is a [template part](./template.md) cannot have revisions. This is because the template part is used to create variants, and allowing revisions of templates would create disallowed relationship states in the database. However, variant parts are allowed to have revisions.
* **Template References**: A part which is a revision of a variant part must point to the same template as the original part. This is to ensure that the revision is correctly linked to the original part.
## Revision Settings
The following options are available to control the behavior of part revisions.
Note that these options can be changed in the InvenTree settings:
{% with id="part_revision_settings", url="part/part_revision_settings.png", description="Part revision settings" %}
{% include 'img.html' %}
{% endwith %}
* **Enable Revisions**: If this setting is enabled, parts can have revisions. If this setting is disabled, parts cannot have revisions.
* **Assembly Revisions Only**: If this setting is enabled, only assembly parts can have revisions. This is useful if you only want to track revisions of assemblies, and not individual parts.
## Create a Revision
To create a new revision for a given part, navigate to the part detail page, and click on the "Revisions" tab.
Select the "Duplicate Part" action, to create a new copy of the selected part. This will open the "Duplicate Part" form:
{% with id="part_create_revision", url="part/part_create_revision.png", description="Create part revision" %}
{% include 'img.html' %}
{% endwith %}
In this form, make the following updates:
1. Set the *Revision Of* field to the original part (the one that you are duplicating)
2. Set the *Revision* field to a unique revision number for the new part revision
Once these changes (and any other required changes) are made, press *Submit* to create the new part.
Once the form is submitted (without any errors), you will be redirected to the new part revision. Here you can see that it is linked to the original part:
{% with id="part_revision_b", url="part/part_revision_b.png", description="Revision B" %}
{% include 'img.html' %}
{% endwith %}
## Revision Navigation
When multiple revisions exist for a particular part, you can navigate between revisions using the *Select Part Revision* drop-down which renders at the top of the part page:
{% with id="part_revision_select", url="part/part_revision_select.png", description="Select part revision" %}
{% include 'img.html' %}
{% endwith %}
Note that this revision selector is only visible when multiple revisions exist for the part.

View File

@ -5,7 +5,7 @@ title: Part Scheduling
## Part Scheduling
The *Scheduling* tab provides an overview of the *predicted* future availabile quantity of a particular part.
The *Scheduling* tab provides an overview of the *predicted* future available quantity of a particular part.
The *Scheduling* tab displays a chart of estimated future part stock levels. It begins at the current date, with the current stock level. It then projects into the "future", taking information from:

View File

@ -28,7 +28,6 @@ Details provides information about the particular part. Parts details can be dis
{% with id="part_overview", url="part/part_overview.png", description="Part details" %}
{% include 'img.html' %}
{% endwith %}
<p></p>
A Part is defined in the system by the following parameters:
@ -38,7 +37,7 @@ A Part is defined in the system by the following parameters:
**Description** - Longer form text field describing the Part
**Revision** - An optional revision code denoting the particular version for the part. Used when there are multiple revisions of the same master part object.
**Revision** - An optional revision code denoting the particular version for the part. Used when there are multiple revisions of the same master part object. Read [more about part revisions here](./revision.md).
**Keywords** - Optional few words to describe the part and make the part search more efficient.
@ -62,7 +61,7 @@ Parts can have multiple defined parameters.
If a part is a *Template Part* then the *Variants* tab will be visible.
[Read about Part templates](./template.md)
[Read about Part templates and variants](./template.md)
### Stock

View File

@ -61,7 +61,7 @@ Also, dedicated settings sections were added for:
For Category section, read [Category Parameter Templates](#category-parameter-templates)
Other section allows to set the prefix of build, puchase and sales orders.
Other section allows to set the prefix of build, purchase and sales orders.
### Category Parameter Templates

View File

@ -29,5 +29,5 @@ title: Release 0.4.0
| Pull Request | Description |
| --- | --- |
| [#1823](https://github.com/inventree/InvenTree/pull/1823) | Fixes link formatting issues for ManufacturerParts |
| [#1824](https://github.com/inventree/InvenTree/pull/1824) | Selects correct curreny code when creating a new PurchaseOrderLineItem |
| [#1824](https://github.com/inventree/InvenTree/pull/1824) | Selects correct currency code when creating a new PurchaseOrderLineItem |
| [#1867](https://github.com/inventree/InvenTree/pull/1867) | Fixes long-running bug when deleting sequential items via the API |

View File

@ -259,6 +259,31 @@ A shortcut function is provided for rendering an image associated with a Company
*Preview* and *thumbnail* image variations can be rendered for the `company_image` tag, in a similar manner to [part image variations](#image-variations)
## Icons
Some models (e.g. part categories and locations) allow to specify a custom icon. To render these icons in a report, there is a `{% raw %}{% icon location.icon %}{% endraw %}` template tag from the report template library available.
This tag renders the required html for the icon.
!!! info "Loading fonts"
Additionally the icon fonts need to be loaded into the template. This can be done using the `{% raw %}{% include_icon_fonts %}{% endraw %}` template tag inside of a style block
!!! tip "Custom classes for styling the icon further"
The icon template tag accepts an optional `class` argument which can be used to apply a custom class to the rendered icon used to style the icon further e.g. positioning it, changing it's size, ... `{% raw %}{% icon location.icon class="my-class" %}{% endraw %}`.
```html
{% raw %}
{% load report %}
{% block style %}
{% include_icon_fonts %}
{% endblock style %}
{% icon location.icon %}
{% endraw %}
```
## InvenTree Logo
A template tag is provided to load the InvenTree logo image into a report. You can render the logo using the `{% raw %}{% logo_image %}{% endraw %}` tag:

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.
@ -132,6 +132,31 @@ In the [settings screen](./global.md), navigate to the *Login Settings* panel. H
Note that [email settings](./email.md) must be correctly configured before SSO will be activated. Ensure that your email setup is correctly configured and operational.
## SSO Group Sync Configuration
InvenTree has the ability to synchronize groups assigned to each user directly from the IdP. To enable this feature, navigate to the *Login Settings* panel in the [settings screen](./global.md) first. Here, the following options are available:
| Setting | Description |
| --- | --- |
| Enable SSO group sync | Enable synchronizing InvenTree groups with groups provided by the IdP |
| SSO group key | The name of the claim containing all groups, e.g. `groups` or `roles` |
| SSO group map | A mapping from SSO groups to InvenTree groups as JSON, e.g. `{"/inventree/admins": "admin"}`. If the mapped group does not exist once a user signs up, a new group without assigned permissions will be created. |
| Remove groups outside of SSO | Whether groups should be removed from the user if they are not present in the IdP data |
!!! warning "Remove groups outside of SSO"
Disabling this feature might cause security issues as groups that are removed in the IdP will stay assigned in InvenTree
### Keycloak OIDC example configuration
!!! tip "Configuration for different IdPs"
The main challenge in enabling the SSO group sync feature is for the SSO admin to configure the IdP such that the groups are correctly represented in in the Django allauth `extra_data` attribute. The SSO group sync feature has been developed and tested using integrated Keycloak users/groups and OIDC. If you are utilizing this feature using another IdP, kindly consider documenting your configuration steps as well.
Keycloak groups are not sent to the OIDC client by default. To enable such functionality, create a new client scope named `groups` in the Keycloak admin console. For this scope, add a new mapper ('By Configuration') and select 'Group Membership'. Give it a descriptive name and set the token claim name to `groups`.
For each OIDC client that relies on those group, explicitly add the `groups` scope to client scopes. The groups will now be sent to client upon request.
**Note:** A group named `foo` will be displayed as `/foo`. For this reason, the example above recommends using group names like `appname/rolename` which will be sent to the client as `/appname/rolename`.
## Security Considerations
You should use SSL for your website if you want to use this feature. Also set your callback-endpoints to `https://` addresses to reduce the risk of leaking user's tokens.

View File

@ -24,12 +24,7 @@ If a different currency exchange backend is needed, or a custom implementation i
### Currency Settings
In the [settings screen](./global.md), under the *Pricing* section, the following currency settings are available:
| Setting | Description | Default Value |
| --- | --- |
| Default Currency | The selected *default* currency for the system. | USD |
| Supported Currencies | The list of supported currencies for the system. | AUD, CAD, CNY, EUR, GBP, JPY, NZD, USD |
Refer to the [global settings](./global.md#pricing-and-currency) documentation for more information on available currency settings.
#### Supported Currencies

View File

@ -17,116 +17,139 @@ Global settings are arranged in the following categories:
### Server Settings
Configuration of basic server settings.
Configuration of basic server settings:
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("INVENTREE_BASE_URL") }}
{{ globalsetting("INVENTREE_COMPANY_NAME") }}
{{ globalsetting("INVENTREE_INSTANCE") }}
{{ globalsetting("INVENTREE_INSTANCE_TITLE") }}
{{ globalsetting("INVENTREE_RESTRICT_ABOUT") }}
{{ globalsetting("DISPLAY_FULL_NAMES") }}
{{ globalsetting("INVENTREE_UPDATE_CHECK_INTERVAL") }}
{{ globalsetting("INVENTREE_DOWNLOAD_FROM_URL") }}
{{ globalsetting("INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE") }}
{{ globalsetting("INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT") }}
{{ globalsetting("INVENTREE_REQUIRE_CONFIRM") }}
{{ globalsetting("INVENTREE_STRICT_URLS") }}
{{ globalsetting("INVENTREE_TREE_DEPTH") }}
{{ globalsetting("INVENTREE_BACKUP_ENABLE") }}
{{ globalsetting("INVENTREE_BACKUP_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }}
{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }}
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| InvenTree Instance Name | String | String descriptor for the InvenTree server instance | InvenTree Server |
| Use Instance Name | Boolean | Use instance name in title bars | False |
| Restrict showing `about` | Boolean | Show the `about` modal only to superusers | False |
| Base URL | String | Base URL for server instance | *blank* |
| Company Name | String | Company name | My company name |
| Download from URL | Boolean | Allow downloading of images from remote URLs | False |
### Login Settings
Change how logins, password-forgot, signups are handled.
Change how logins, password-forgot, signups are handled:
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Enable registration | Boolean | Enable self-registration for users on the login-pages | False |
| Enable SSO | Boolean | Enable SSO on the login-pages | False |
| Enable SSO registration | Boolean | Enable self-registration for users via SSO on the login-pages | False |
| Enable password forgot | Boolean | Enable password forgot function on the login-pages.<br><br>This will let users reset their passwords on their own. For this feature to work you need to configure E-mail | True |
| E-Mail required | Boolean | Require user to supply e-mail on signup.<br><br>Without a way (e-mail) to contact the user notifications and security features might not work! | False |
| Enforce MFA | Boolean | Users must use multifactor security.<br><br>This forces each user to setup MFA and use it on each authentication | False |
| Mail twice | Boolean | On signup ask users twice for their mail | False |
| Password twice | Boolean | On signup ask users twice for their password | True |
| Auto-fill SSO users | Boolean | Automatically fill out user-details from SSO account-data.<br><br>If this feature is enabled the user is only asked for their username, first- and surname if those values can not be gathered from their SSO profile. This might lead to unwanted usernames bleeding over. | True |
| Allowed domains | String | Restrict signup to certain domains (comma-separated, starting with @) | |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("LOGIN_ENABLE_PWD_FORGOT") }}
{{ globalsetting("LOGIN_MAIL_REQUIRED") }}
{{ globalsetting("LOGIN_ENFORCE_MFA") }}
{{ globalsetting("LOGIN_ENABLE_REG") }}
{{ globalsetting("LOGIN_SIGNUP_MAIL_TWICE") }}
{{ globalsetting("LOGIN_SIGNUP_PWD_TWICE") }}
{{ globalsetting("SIGNUP_GROUP") }}
{{ globalsetting("LOGIN_SIGNUP_MAIL_RESTRICTION") }}
{{ globalsetting("LOGIN_ENABLE_SSO") }}
{{ globalsetting("LOGIN_ENABLE_SSO_REG") }}
{{ globalsetting("LOGIN_SIGNUP_SSO_AUTO") }}
{{ globalsetting("LOGIN_ENABLE_SSO_GROUP_SYNC") }}
{{ globalsetting("SSO_GROUP_MAP") }}
{{ globalsetting("SSO_GROUP_KEY") }}
{{ globalsetting("SSO_REMOVE_GROUPS") }}
#### Require User Email
If this setting is enabled, users must provide an email address when signing up. Note that some notification and security features require a valid email address.
#### Forgot Password
If this setting is enabled, users can reset their password via email. This requires a valid email address to be associated with the user account.
#### Enforce Multi-Factor Authentication
If this setting is enabled, users must have multi-factor authentication enabled to log in.
#### Auto Fil SSO Users
Automatically fill out user-details from SSO account-data. If this feature is enabled the user is only asked for their username, first- and surname if those values can not be gathered from their SSO profile. This might lead to unwanted usernames bleeding over.
### Barcodes
Configuration of barcode functionality
Configuration of barcode functionality:
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Barcode Support | Boolean | Enable barcode functionality in web interface | True |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("BARCODE_ENABLE") }}
{{ globalsetting("BARCODE_INPUT_DELAY") }}
{{ globalsetting("BARCODE_WEBCAM_SUPPORT") }}
{{ globalsetting("BARCODE_SHOW_TEXT") }}
{{ globalsetting("BARCODE_GENERATION_PLUGIN") }}
### Currencies
### Pricing and Currency
Configuration of currency support
Configuration of pricing data and currency support:
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Default Currency | Currency | Default currency | USD |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("INVENTREE_DEFAULT_CURRENCY") }}
{{ globalsetting("CURRENCY_CODES") }}
{{ globalsetting("PART_INTERNAL_PRICE") }}
{{ globalsetting("PART_BOM_USE_INTERNAL_PRICE") }}
{{ globalsetting("PRICING_DECIMAL_PLACES_MIN") }}
{{ globalsetting("PRICING_DECIMAL_PLACES") }}
{{ globalsetting("PRICING_UPDATE_DAYS") }}
{{ globalsetting("PRICING_USE_SUPPLIER_PRICING") }}
{{ globalsetting("PRICING_PURCHASE_HISTORY_OVERRIDES_SUPPLIER") }}
{{ globalsetting("PRICING_USE_STOCK_PRICING") }}
{{ globalsetting("PRICING_STOCK_ITEM_AGE_DAYS") }}
{{ globalsetting("PRICING_USE_VARIANT_PRICING") }}
{{ globalsetting("PRICING_ACTIVE_VARIANTS") }}
### Reporting
Configuration of report generation
Configuration of report generation:
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Enable Reports | Boolean | Enable report generation | False |
| Page Size | String | Default page size | A4 |
| Debug Mode | Boolean | Generate reports in debug mode (HTML output) | False |
| Test Reports | Boolean | Enable generation of test reports | False |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("REPORT_ENABLE") }}
{{ globalsetting("REPORT_DEFAULT_PAGE_SIZE") }}
{{ globalsetting("REPORT_DEBUG_MODE") }}
{{ globalsetting("REPORT_LOG_ERRORS") }}
{{ globalsetting("REPORT_ENABLE_TEST_REPORT") }}
{{ globalsetting("REPORT_ATTACH_TEST_REPORT") }}
### Parts
#### Main Settings
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| IPN Regex | String | Regular expression pattern for matching Part IPN | *blank* |
| Allow Duplicate IPN | Boolean | Allow multiple parts to share the same IPN | True |
| Allow Editing IPN | Boolean | Allow changing the IPN value while editing a part | True |
| Part Name Display Format | String | Format to display the part name | {% raw %}`{{ part.id if part.id }}{{ ' | ' if part.id }}{{ part.name }}{{ ' | ' if part.revision }}{{ part.revision if part.revision }}`{% endraw %} |
| Show Price History | Boolean | Display historical pricing for Part | False |
| Show Price in Forms | Boolean | Display part price in some forms | True |
| Show Price in BOM | Boolean | Include pricing information in BOM tables | True |
| Show related parts | Boolean | Display related parts for a part | True |
| Create initial stock | Boolean | Create initial stock on part creation | True |
#### Creation Settings
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Template | Boolean | Parts are templates by default | False |
| Assembly | Boolean | Parts can be assembled from other components by default | False |
| Component | Boolean | Parts can be used as sub-components by default | True |
| Trackable | Boolean | Parts are trackable by default | False |
| Purchaseable | Boolean | Parts are purchaseable by default | True |
| Salable | Boolean | Parts are salable by default | False |
| Virtual | Boolean | Parts are virtual by default | False |
#### Copy Settings
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Copy Part BOM Data | Boolean | Copy BOM data by default when duplicating a part | True |
| Copy Part Parameter Data | Boolean | Copy parameter data by default when duplicating a part | True |
| Copy Part Test Data | Boolean | Copy test data by default when duplicating a part | True |
| Copy Category Parameter Templates | Boolean | Copy category parameter templates when creating a part | True |
#### Internal Price Settings
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Internal Prices | Boolean | Enable internal prices for parts | False |
| Internal Price as BOM-Price | Boolean | Use the internal price (if set) in BOM-price calculations | False |
#### Part Import Setting
This section of the part settings allows staff users to:
- import parts to InvenTree clicking the <span class="badge inventree add"><span class='fas fa-plus-circle'></span> Import Part</span> button
- enable the ["Import Parts" tab in the part category view](../part/part.md#part-import).
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Show Import in Views | Boolean | Display the import wizard in some part views | True |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PART_IPN_REGEX") }}
{{ globalsetting("PART_ALLOW_DUPLICATE_IPN") }}
{{ globalsetting("PART_ALLOW_EDIT_IPN") }}
{{ globalsetting("PART_ALLOW_DELETE_FROM_ASSEMBLY") }}
{{ globalsetting("PART_ENABLE_REVISION") }}
{{ globalsetting("PART_REVISION_ASSEMBLY_ONLY") }}
{{ globalsetting("PART_NAME_FORMAT") }}
{{ globalsetting("PART_SHOW_RELATED") }}
{{ globalsetting("PART_CREATE_INITIAL") }}
{{ globalsetting("PART_CREATE_SUPPLIER") }}
{{ globalsetting("PART_TEMPLATE") }}
{{ globalsetting("PART_ASSEMBLY") }}
{{ globalsetting("PART_COMPONENT") }}
{{ globalsetting("PART_TRACKABLE") }}
{{ globalsetting("PART_PURCHASEABLE") }}
{{ globalsetting("PART_SALABLE") }}
{{ globalsetting("PART_VIRTUAL") }}
{{ globalsetting("PART_COPY_BOM") }}
{{ globalsetting("PART_COPY_PARAMETERS") }}
{{ globalsetting("PART_COPY_TESTS") }}
{{ globalsetting("PART_CATEGORY_PARAMETERS") }}
{{ globalsetting("PART_CATEGORY_DEFAULT_ICON") }}
#### Part Parameter Templates
@ -149,45 +172,48 @@ After a list of parameters is added to a part category and upon creation of a ne
Configuration of stock item options
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Stock Expiry | Boolean | Enable stock expiry functionality | False |
| Stock Stale Time | Days | Number of days stock items are considered stale before expiring | 90 |
| Sell Expired Stock | Boolean | Allow sale of expired stock | False |
| Build Expired Stock | Boolean | Allow building with expired stock | False |
| Stock Ownership Control | Boolean | Enable ownership control functionality | False |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("SERIAL_NUMBER_GLOBALLY_UNIQUE") }}
{{ globalsetting("SERIAL_NUMBER_AUTOFILL") }}
{{ globalsetting("STOCK_DELETE_DEPLETED_DEFAULT") }}
{{ globalsetting("STOCK_BATCH_CODE_TEMPLATE") }}
{{ globalsetting("STOCK_ENABLE_EXPIRY") }}
{{ globalsetting("STOCK_STALE_DAYS") }}
{{ globalsetting("STOCK_ALLOW_EXPIRED_SALE") }}
{{ globalsetting("STOCK_ALLOW_EXPIRED_BUILD") }}
{{ globalsetting("STOCK_OWNERSHIP_CONTROL") }}
{{ globalsetting("STOCK_LOCATION_DEFAULT_ICON") }}
{{ globalsetting("STOCK_SHOW_INSTALLED_ITEMS") }}
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
{{ globalsetting("TEST_STATION_DATA") }}
### Build Orders
Options for build orders
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Reference Pattern | String | Pattern for defining Build Order reference values | {% raw %}BO-{ref:04d}{% endraw %} |
Refer to the [build order settings](../build/build.md#build-order-settings).
### Purchase Orders
Options for purchase orders
Refer to the [purchase order settings](../order/purchase_order.md#purchase-order-settings).
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Reference Pattern | String | Pattern for defining Purchase Order reference values | {% raw %}PO-{ref:04d}{% endraw %} |
### Sales Orders
### Sales orders
Refer to the [sales order settings](../order/sales_order.md#sales-order-settings).
Options for sales orders
### Return Orders
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Reference Pattern | String | Pattern for defining Sales Order reference values | {% raw %}SO-{ref:04d}{% endraw %} |
Refer to the [return order settings](../order/return_order.md#return-order-settings).
### Plugin Settings
Change into what parts plugins can integrate into.
| Setting | Type | Description | Default |
| --- | --- | --- | --- |
| Enable URL integration | Boolean | Enable plugins to add URL routes | False |
| Enable navigation integration | Boolean | Enable plugins to integrate into navigation | False |
| Enable setting integration | Boolean | Enable plugins to integrate into inventree settings | False |
| Enable app integration | Boolean | Enable plugins to add apps | False |
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PLUGIN_ON_STARTUP") }}
{{ globalsetting("PLUGIN_UPDATE_CHECK") }}
{{ globalsetting("ENABLE_PLUGINS_URL") }}
{{ globalsetting("ENABLE_PLUGINS_NAVIGATION") }}
{{ globalsetting("ENABLE_PLUGINS_APP") }}
{{ globalsetting("ENABLE_PLUGINS_SCHEDULE") }}
{{ globalsetting("ENABLE_PLUGINS_EVENTS") }}

View File

@ -32,24 +32,44 @@ This screen allows the user to customize display of items on the InvenTree home
### Search Settings
Customize settings for search results
Customize settings for search results:
{% with id="user-search", url="settings/user_search.png", description="User Search Settings" %}
{% include 'img.html' %}
{% endwith %}
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ usersetting("SEARCH_WHOLE") }}
{{ usersetting("SEARCH_REGEX") }}
{{ usersetting("SEARCH_PREVIEW_RESULTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }}
{{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_CATEGORIES") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_STOCK") }}
{{ usersetting("SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_LOCATIONS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_COMPANIES") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_BUILD_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_SALES_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_EXCLUDE_INACTIVE_SALES_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_RETURN_ORDERS") }}
{{ usersetting("SEARCH_PREVIEW_EXCLUDE_INACTIVE_RETURN_ORDERS") }}
### Notifications
Settings related to notification messages
Settings related to notification messages:
{% with id="user-notification", url="settings/user_notifications.png", description="User Notification Settings" %}
{% include 'img.html' %}
{% endwith %}
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ usersetting("NOTIFICATION_ERROR_REPORT") }}
### Reporting
Settings for label printing and report generation
Settings for label printing and report generation:
{% with id="user-reporting", url="settings/user_reporting.png", description="User Reporting Settings" %}
{% include 'img.html' %}
{% endwith %}
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ usersetting("REPORT_INLINE") }}
{{ usersetting("LABEL_INLINE") }}
{{ usersetting("LABEL_DEFAULT_PRINTER") }}

View File

@ -54,6 +54,8 @@ The following basic options are available:
| --- | --- | --- | --- |
| INVENTREE_SITE_URL | site_url | Specify a fixed site URL | *Not specified* |
| INVENTREE_DEBUG | debug | Enable [debug mode](./intro.md#debug-mode) | True |
| INVENTREE_DEBUG_QUERYCOUNT | debug_querycount | Enable [query count logging](https://github.com/bradmontgomery/django-querycount) in the terminal | False |
| INVENTREE_DEBUG_SHELL | debug_shell | Enable [administrator shell](https://github.com/djk2/django-admin-shell) (only in debug mode) | False |
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
| INVENTREE_TIMEZONE | timezone | Server timezone | UTC |
@ -297,6 +299,10 @@ Alternatively this location can be specified with the `INVENTREE_BACKUP_DIR` env
InvenTree provides allowance for additional sign-in options. The following options are not enabled by default, and care must be taken by the system administrator when configuring these settings.
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_MFA_ENABLED | mfa_enabled | Enable or disable multi-factor authentication support for the InvenTree server | True |
### Single Sign On
Single Sign On (SSO) allows users to sign in to InvenTree using a third-party authentication provider. This functionality is provided by the [django-allauth](https://docs.allauth.org/en/latest/) package.

View File

@ -199,7 +199,7 @@ Any persistent files generated by the Caddy container (such as certificates, etc
### Demo Dataset
To quickly get started with a demo dataset, you can run the following command:
To quickly get started with a [demo dataset](../demo.md), you can run the following command:
```
docker compose run --rm inventree-server invoke setup-test -i
@ -212,3 +212,86 @@ To start afresh (and completely remove the existing database), run the following
```
docker compose run --rm inventree-server invoke delete-data
```
## Install custom packages
To install custom packages to your docker image, a custom docker image can be built and used automatically each time when updating. The following changes need to be applied to the docker compose file:
<details><summary>docker-compose.yml changes</summary>
```diff
diff --git a/docker-compose.yml b/docker-compose.yml
index 8adee63..dc3993c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -69,7 +69,14 @@ services:
# Uses gunicorn as the web server
inventree-server:
# If you wish to specify a particular InvenTree version, do so here
- image: inventree/inventree:${INVENTREE_TAG:-stable}
+ image: inventree/inventree:${INVENTREE_TAG:-stable}-custom
+ pull_policy: never
+ build:
+ context: .
+ dockerfile: Dockerfile
+ target: production
+ args:
+ INVENTREE_TAG: ${INVENTREE_TAG:-stable}
# Only change this port if you understand the stack.
# If you change this you have to change:
# - the proxy settings (on two lines)
@@ -88,7 +95,8 @@ services:
# Background worker process handles long-running or periodic tasks
inventree-worker:
# If you wish to specify a particular InvenTree version, do so here
- image: inventree/inventree:${INVENTREE_TAG:-stable}
+ image: inventree/inventree:${INVENTREE_TAG:-stable}-custom
+ pull_policy: never
command: invoke worker
depends_on:
- inventree-server
```
</details>
And the following `Dockerfile` needs to be created:
<details><summary>Dockerfile</summary>
```dockerfile
ARG INVENTREE_TAG
FROM inventree/inventree:${INVENTREE_TAG} as production
# Install whatever dependency is needed here (e.g. git)
RUN apk add --no-cache git
```
</details>
And if additional, development packages are needed e.g. just for building a wheel for a pip package, a multi stage build can be used with the following `Dockerfile`:
<details><summary>Dockerfile</summary>
```dockerfile
ARG INVENTREE_TAG
# prebuild stage - needs a lot of build dependencies
# make sure, the alpine and python version matches the version used in the inventree base image
FROM python:3.11-alpine3.18 as prebuild
# Install whatever development dependency is needed (e.g. cups-dev, gcc, the musl-dev build tools and the pip pycups package)
RUN apk add --no-cache cups-dev gcc musl-dev && \
pip install --user --no-cache-dir pycups
# production image - only install the cups shared library
FROM inventree/inventree:${INVENTREE_TAG} as production
# Install e.g. shared library later available in the final image
RUN apk add --no-cache cups-libs
# Copy the pip wheels from the build stage in the production stage
COPY --from=prebuild /root/.local /root/.local
```
</details>

View File

@ -239,6 +239,7 @@ Run the following command to initialize the database with the required tables.
cd /home/inventree/src
invoke update
```
NOTE: If you are on Debian, and get "No module named 'django', it might be that `/usr/bin/invoke` are used. Make sure that the python environment (`/home/inventree/env/bin`) is ahead in the PATH variable.
### Create Admin Account

View File

@ -12,8 +12,25 @@ The stock ownership feature is disabled by default, and must be enabled via the
{% include 'img.html' %}
{% endwith %}
### Background
The stock item ownership function does not change or the influence the access rights that have been set
in admin panel. It just hides buttons for edit, add and delete actions. This means that the user who
should have access by ownership needs to have stock item write access set in the admin panel. By
this he also gets write access to all other items, except the item has a different owner.
Example
* Stock item 1111 has an owner called Daniel
* Stock item 2222 has an owner called Peter
* Stock item 3333 has no owner
Set stock item access for Daniel and Peter to V,A,E,D in the admin panel. Now Daniel can edit
1111 but not 2222. Peter can edit 2222 but not 1111. Both can edit 3333.
!!! warning "Existing Stock Locations and Items"
Enabling the ownership feature will automatically remove the edit permissions to all users for stock locations and items which **do not have** any owner set. Only a user with admin permissions will be able to set the owner for those locations and items.
Items with no user set can be edited by everyone who has the access rights. An owner has
to be added to every stock item in order to use the ownership control.
### Owner: Group vs User
@ -46,6 +63,12 @@ Setting the owner of stock location will automatically:
* Set the owner of all children locations to the same owner.
* Set the owner of all stock items at this location to the same owner.
The inherited ownership changes with stock transfer. If a stock item is transferred from a location
owned by Daniel to a location owned by Peter the owner of the stock item and the access right will
also change from Daniel to Peter. This is only valid for the inherited ownership. If a stock item
is directly owned by Peter the ownership and the access right will stay with Peter even when the item
is moved to a location owned by Daniel.
!!! info
If the owner of a children location or a stock item is a subset of the specified owner (eg. a user linked to the specified group), the owner won't be updated.

View File

@ -6,6 +6,18 @@ title: Stock
A stock location represents a physical real-world location where *Stock Items* are stored. Locations are arranged in a cascading manner and each location may contain multiple sub-locations, or stock, or both.
### Icons
Stock locations can be assigned custom icons (either directly or through [Stock Location Types](#stock-location-type)). When using PUI there is a custom icon picker component available that can help to select the right icon. However in CUI the icon needs to be entered manually.
By default, the tabler icons package (with prefix: `ti`) is available. To manually select an item, search on the [tabler icons](https://tabler.io/icons) page for an icon and copy its name e.g. `bookmark`. Some icons have a filled and an outline version (if no variants are specified, it's an outline variant). Now these values can be put into the format: `<package-prefix>:<icon-name>:<variant>`. E.g. `ti:bookmark:outline` or `ti:bookmark:filled`.
If there are some icons missing in the tabler icons package, users can even install their own custom icon packs through a plugin. See [`IconPackMixin`](../extend/plugins/icon.md).
## Stock Location Type
A stock location type represents a specific type of location (e.g. one specific size of drawer, shelf, ... or box) which can be assigned to multiple stock locations. In the first place, it is used to specify an icon and having the icon in sync for all locations that use this location type, but it also serves as a data field to quickly see what type of location this is. It is planned to add e.g. drawer dimension information to the location type to add a "find a matching, empty stock location" tool.
## Stock Item
A *Stock Item* is an actual instance of a [*Part*](../part/part.md) item. It represents a physical quantity of the *Part* in a specific location.
@ -20,7 +32,7 @@ The *Stock Item* detail view shows information regarding the particular stock it
**Quantity** - How many items are in stock?
**Supplier** - If this part was purcahsed from a *Supplier*, which *Supplier* did it come from?
**Supplier** - If this part was purchased from a *Supplier*, which *Supplier* did it come from?
**Supplier Part** - Link to the particular *Supplier Part*, if appropriate.

View File

@ -1,5 +1,6 @@
"""Main entry point for the documentation build process."""
import json
import os
import subprocess
import textwrap
@ -7,6 +8,20 @@ import textwrap
import requests
import yaml
# Cached settings dict values
global GLOBAL_SETTINGS
global USER_SETTINGS
# Read in the InvenTree settings file
here = os.path.dirname(__file__)
settings_file = os.path.join(here, 'inventree_settings.json')
with open(settings_file, 'r') as sf:
settings = json.load(sf)
GLOBAL_SETTINGS = settings['global']
USER_SETTINGS = settings['user']
def get_repo_url(raw=False):
"""Return the repository URL for the current project."""
@ -54,11 +69,23 @@ def check_link(url) -> bool:
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='master'):
def sourcedir(dirname, branch=None):
"""Return a link to a directory within the source code repository.
Arguments:
@ -70,6 +97,9 @@ def define_env(env):
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:]
@ -94,7 +124,7 @@ def define_env(env):
return url
@env.macro
def sourcefile(filename, branch='master', raw=False):
def sourcefile(filename, branch=None, raw=False):
"""Return a link to a file within the source code repository.
Arguments:
@ -106,6 +136,9 @@ def define_env(env):
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:]
@ -201,3 +234,37 @@ def define_env(env):
)
return includefile(fn, f'Template: {base}', format='html')
@env.macro
def rendersetting(setting: dict):
"""Render a provided setting object into a table row."""
name = setting['name']
description = setting['description']
default = setting.get('default', None)
units = setting.get('units', None)
return f'| {name} | {description} | {default if default is not None else ""} | {units if units is not None else ""} |'
@env.macro
def globalsetting(key: str):
"""Extract information on a particular global setting.
Arguments:
- key: The name of the global setting to extract information for.
"""
global GLOBAL_SETTINGS
setting = GLOBAL_SETTINGS[key]
return rendersetting(setting)
@env.macro
def usersetting(key: str):
"""Extract information on a particular user setting.
Arguments:
- key: The name of the user setting to extract information for.
"""
global USER_SETTINGS
setting = USER_SETTINGS[key]
return rendersetting(setting)

View File

@ -106,6 +106,7 @@ nav:
- Part Views: part/views.md
- Tracking: part/trackable.md
- Parameters: part/parameter.md
- Revisions: part/revision.md
- Templates: part/template.md
- Tests: part/test.md
- Pricing: part/pricing.md
@ -202,6 +203,7 @@ nav:
- Barcode Mixin: extend/plugins/barcode.md
- Currency Mixin: extend/plugins/currency.md
- Event Mixin: extend/plugins/event.md
- Icon Pack Mixin: extend/plugins/icon.md
- Label Printing Mixin: extend/plugins/label.md
- Locate Mixin: extend/plugins/locate.md
- Navigation Mixin: extend/plugins/navigation.md

View File

@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile docs/requirements.in -o docs/requirements.txt --python-version=3.9 --no-strip-extras --generate-hashes
# uv pip compile docs/requirements.in -o docs/requirements.txt --no-strip-extras --generate-hashes
anyio==4.3.0 \
--hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \
--hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6
@ -14,9 +14,9 @@ bracex==2.4 \
--hash=sha256:a27eaf1df42cf561fed58b7a8f3fdf129d1ea16a81e1fadd1d17989bc6384beb \
--hash=sha256:efdc71eff95eaff5e0f8cfebe7d01adf2c8637c8c92edaf63ef348c241a82418
# via wcmatch
certifi==2024.2.2 \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1
certifi==2024.7.4 \
--hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
--hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
# via
# httpcore
# httpx
@ -134,9 +134,9 @@ essentials-openapi==1.0.9 \
--hash=sha256:1431e98ef0a442f1919fd9833385bf44d832c355fd05919dc06d43d4da0f8ef4 \
--hash=sha256:ebc46aac41c0b917a658f77caaa0ca93a6e4a4519de8a272f82c1538ccd5619f
# via neoteroi-mkdocs
exceptiongroup==1.2.1 \
--hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \
--hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16
exceptiongroup==1.2.2 \
--hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \
--hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc
# via anyio
ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
@ -173,9 +173,9 @@ idna==3.7 \
# anyio
# httpx
# requests
importlib-metadata==7.1.0 \
--hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \
--hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2
importlib-metadata==8.2.0 \
--hash=sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369 \
--hash=sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d
# via
# markdown
# mkdocs
@ -284,6 +284,7 @@ mkdocs==1.6.0 \
--hash=sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7 \
--hash=sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512
# via
# -r docs/requirements.in
# mkdocs-autorefs
# mkdocs-git-revision-date-localized-plugin
# mkdocs-include-markdown-plugin
@ -300,18 +301,22 @@ mkdocs-get-deps==0.2.0 \
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
# via mkdocs
mkdocs-git-revision-date-localized-plugin==1.2.5 \
--hash=sha256:0c439816d9d0dba48e027d9d074b2b9f1d7cd179f74ba46b51e4da7bb3dc4b9b \
--hash=sha256:d796a18b07cfcdb154c133e3ec099d2bb5f38389e4fd54d3eb516a8a736815b8
mkdocs-include-markdown-plugin==6.0.6 \
--hash=sha256:7c80258b2928563c75cc057a7b9a0014701c40804b1b6aa290f3b4032518b43c \
--hash=sha256:7ccafbaa412c1e5d3510c4aff46d1fe64c7a810c01dace4c636253d1aa5bc193
mkdocs-git-revision-date-localized-plugin==1.2.6 \
--hash=sha256:e432942ce4ee8aa9b9f4493e993dee9d2cc08b3ea2b40a3d6b03ca0f2a4bcaa2 \
--hash=sha256:f015cb0f3894a39b33447b18e270ae391c4e25275cac5a626e80b243784e2692
# via -r docs/requirements.in
mkdocs-include-markdown-plugin==6.2.1 \
--hash=sha256:46fc372886d48eec541d36138d1fe1db42afd08b976ef7c8d8d4ea6ee4d5d1e8 \
--hash=sha256:8dfc3aee9435679b094cbdff023239e91d86cf357c40b0e99c28036449661830
# via -r docs/requirements.in
mkdocs-macros-plugin==1.0.5 \
--hash=sha256:f60e26f711f5a830ddf1e7980865bf5c0f1180db56109803cdd280073c1a050a \
--hash=sha256:fe348d75f01c911f362b6d998c57b3d85b505876dde69db924f2c512c395c328
mkdocs-material==9.5.24 \
--hash=sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34 \
--hash=sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6
# via -r docs/requirements.in
mkdocs-material==9.5.31 \
--hash=sha256:1b1f49066fdb3824c1e96d6bacd2d4375de4ac74580b47e79ff44c4d835c5fcb \
--hash=sha256:31833ec664772669f5856f4f276bf3fdf0e642a445e64491eda459249c3a1ca8
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
--hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
@ -319,10 +324,13 @@ mkdocs-material-extensions==1.3.1 \
mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
mkdocstrings[python]==0.25.1 \
--hash=sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf \
--hash=sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51
# via mkdocstrings-python
# via -r docs/requirements.in
mkdocstrings[python]==0.25.2 \
--hash=sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc \
--hash=sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc
# via
# -r docs/requirements.in
# mkdocstrings-python
mkdocstrings-python==1.10.2 \
--hash=sha256:38a4fd41953defb458a107033440c229c7e9f98f35a24e84d888789c97da5a63 \
--hash=sha256:e8e596b37f45c09b67bec253e035fe18988af5bbbbf44e0ccd711742eed750e5
@ -330,6 +338,7 @@ mkdocstrings-python==1.10.2 \
neoteroi-mkdocs==1.0.5 \
--hash=sha256:1f3b372dee79269157361733c0f45b3a89189077078e0e3224d829a144ef3579 \
--hash=sha256:29875ef444b08aec5619a384142e16f1b4e851465cab4e380fb2b8ae730fe046
# via -r docs/requirements.in
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
@ -432,90 +441,90 @@ pyyaml-env-tag==0.1 \
--hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \
--hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069
# via mkdocs
regex==2024.5.15 \
--hash=sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649 \
--hash=sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35 \
--hash=sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb \
--hash=sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68 \
--hash=sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5 \
--hash=sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133 \
--hash=sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0 \
--hash=sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d \
--hash=sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da \
--hash=sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f \
--hash=sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d \
--hash=sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53 \
--hash=sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa \
--hash=sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a \
--hash=sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890 \
--hash=sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67 \
--hash=sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c \
--hash=sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2 \
--hash=sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced \
--hash=sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741 \
--hash=sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f \
--hash=sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa \
--hash=sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf \
--hash=sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4 \
--hash=sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5 \
--hash=sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2 \
--hash=sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384 \
--hash=sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7 \
--hash=sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014 \
--hash=sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704 \
--hash=sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5 \
--hash=sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2 \
--hash=sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49 \
--hash=sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1 \
--hash=sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694 \
--hash=sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629 \
--hash=sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6 \
--hash=sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435 \
--hash=sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c \
--hash=sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835 \
--hash=sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e \
--hash=sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201 \
--hash=sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62 \
--hash=sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5 \
--hash=sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16 \
--hash=sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f \
--hash=sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1 \
--hash=sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f \
--hash=sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f \
--hash=sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145 \
--hash=sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3 \
--hash=sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed \
--hash=sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143 \
--hash=sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca \
--hash=sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9 \
--hash=sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa \
--hash=sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850 \
--hash=sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80 \
--hash=sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe \
--hash=sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656 \
--hash=sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388 \
--hash=sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1 \
--hash=sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294 \
--hash=sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3 \
--hash=sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d \
--hash=sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b \
--hash=sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40 \
--hash=sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600 \
--hash=sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c \
--hash=sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569 \
--hash=sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456 \
--hash=sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9 \
--hash=sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb \
--hash=sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e \
--hash=sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f \
--hash=sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d \
--hash=sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a \
--hash=sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a \
--hash=sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796
regex==2024.7.24 \
--hash=sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c \
--hash=sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535 \
--hash=sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24 \
--hash=sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce \
--hash=sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc \
--hash=sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5 \
--hash=sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce \
--hash=sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53 \
--hash=sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d \
--hash=sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c \
--hash=sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908 \
--hash=sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8 \
--hash=sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024 \
--hash=sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281 \
--hash=sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a \
--hash=sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169 \
--hash=sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364 \
--hash=sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa \
--hash=sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be \
--hash=sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53 \
--hash=sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759 \
--hash=sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e \
--hash=sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b \
--hash=sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52 \
--hash=sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610 \
--hash=sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05 \
--hash=sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2 \
--hash=sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca \
--hash=sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0 \
--hash=sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293 \
--hash=sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289 \
--hash=sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e \
--hash=sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f \
--hash=sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c \
--hash=sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94 \
--hash=sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad \
--hash=sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46 \
--hash=sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9 \
--hash=sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9 \
--hash=sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee \
--hash=sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9 \
--hash=sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1 \
--hash=sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9 \
--hash=sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799 \
--hash=sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1 \
--hash=sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b \
--hash=sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf \
--hash=sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5 \
--hash=sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2 \
--hash=sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e \
--hash=sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51 \
--hash=sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506 \
--hash=sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73 \
--hash=sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7 \
--hash=sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5 \
--hash=sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57 \
--hash=sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4 \
--hash=sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd \
--hash=sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b \
--hash=sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41 \
--hash=sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe \
--hash=sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59 \
--hash=sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8 \
--hash=sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f \
--hash=sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e \
--hash=sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750 \
--hash=sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1 \
--hash=sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96 \
--hash=sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc \
--hash=sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440 \
--hash=sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe \
--hash=sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38 \
--hash=sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950 \
--hash=sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2 \
--hash=sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd \
--hash=sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce \
--hash=sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66 \
--hash=sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3 \
--hash=sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86
# via mkdocs-material
requests==2.32.2 \
--hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \
--hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via mkdocs-material
rich==13.7.1 \
--hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \
@ -539,15 +548,15 @@ termcolor==2.4.0 \
--hash=sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63 \
--hash=sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a
# via mkdocs-macros-plugin
typing-extensions==4.11.0 \
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# anyio
# mkdocstrings
urllib3==2.2.1 \
--hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \
--hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via requests
watchdog==4.0.0 \
--hash=sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257 \
@ -584,7 +593,7 @@ wcmatch==8.5.2 \
--hash=sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478 \
--hash=sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2
# via mkdocs-include-markdown-plugin
zipp==3.18.2 \
--hash=sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059 \
--hash=sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e
zipp==3.19.2 \
--hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \
--hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c
# via importlib-metadata

View File

@ -16,11 +16,11 @@ exclude = [
src = ["src/backend/InvenTree"]
# line-length = 120
[tool.ruff.extend-per-file-ignores]
[tool.ruff.lint.extend-per-file-ignores]
"__init__.py" = ["D104"]
[tool.ruff.lint]
select = ["A", "B", "C4", "D", "I", "N"]
select = ["A", "B", "C4", "D", "I", "N", "F"]
# Things that should be enabled in the future:
# - LOG
# - DJ # for Django stuff
@ -69,6 +69,11 @@ indent-style = "space"
skip-magic-trailing-comma = true
line-ending = "auto"
[tool.uv.pip]
python-version = "3.9"
no-strip-extras=true
generate-hashes=true
[tool.coverage.run]
source = ["src/backend/InvenTree", "InvenTree"]
@ -80,3 +85,6 @@ src_paths=["src/backend/InvenTree", ]
skip_glob ="*/migrations/*.py"
known_django="django"
sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
[tool.codespell]
ignore-words-list = ["assertIn","SME","intoto"]

View File

@ -17,5 +17,6 @@ build:
- echo "Generating API schema file"
- pip install -U invoke
- invoke migrate
- invoke export-settings-definitions --filename docs/inventree_settings.json --overwrite
- invoke schema --filename docs/schema.yml --ignore-warnings
- python docs/extract_schema.py docs/schema.yml

View File

@ -8,7 +8,6 @@ from pathlib import Path
from django.conf import settings
from django.db import transaction
from django.http import JsonResponse
from django.urls import include, path
from django.utils.translation import gettext_lazy as _
from django_q.models import OrmQ
@ -21,9 +20,7 @@ from rest_framework.views import APIView
import InvenTree.version
import users.models
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI
from InvenTree.permissions import RolePermission
from InvenTree.templatetags.inventree_extras import plugins_info
from part.models import Part
from plugin.serializers import MetadataSerializer
@ -311,8 +308,26 @@ class BulkDeleteMixin:
- Speed (single API call and DB query)
"""
def validate_delete(self, queryset, request) -> None:
"""Perform validation right before deletion.
Arguments:
queryset: The queryset to be deleted
request: The request object
Returns:
None
Raises:
ValidationError: If the deletion should not proceed
"""
pass
def filter_delete_queryset(self, queryset, request):
"""Provide custom filtering for the queryset *before* it is deleted."""
"""Provide custom filtering for the queryset *before* it is deleted.
The default implementation does nothing, just returns the queryset.
"""
return queryset
def delete(self, request, *args, **kwargs):
@ -371,6 +386,9 @@ class BulkDeleteMixin:
if filters:
queryset = queryset.filter(**filters)
# Run a final validation step (should raise an error if the deletion should not proceed)
self.validate_delete(queryset, request)
n_deleted = queryset.count()
queryset.delete()
@ -383,58 +401,6 @@ class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
...
class APIDownloadMixin:
"""Mixin for enabling a LIST endpoint to be downloaded a file.
To download the data, add the ?export=<fmt> to the query string.
The implementing class must provided a download_queryset method,
e.g.
def download_queryset(self, queryset, export_format):
dataset = StockItemResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
date=datetime.now().strftime("%d-%b-%Y"),
fmt=export_format
)
return DownloadFile(filedata, filename)
"""
def get(self, request, *args, **kwargs):
"""Generic handler for a download request."""
export_format = request.query_params.get('export', None)
if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']:
queryset = self.filter_queryset(self.get_queryset())
return self.download_queryset(queryset, export_format)
# Default to the parent class implementation
return super().get(request, *args, **kwargs)
def download_queryset(self, queryset, export_format):
"""This function must be implemented to provide a downloadFile request."""
raise NotImplementedError('download_queryset method not implemented!')
class AttachmentMixin:
"""Mixin for creating attachment objects, and ensuring the user information is saved correctly."""
permission_classes = [permissions.IsAuthenticated, RolePermission]
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['attachment', 'comment', 'link']
def perform_create(self, serializer):
"""Save the user information when a file is uploaded."""
attachment = serializer.save()
attachment.user = self.request.user
attachment.save()
class APISearchViewSerializer(serializers.Serializer):
"""Serializer for the APISearchView."""

View File

@ -1,11 +1,115 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 206
INVENTREE_API_VERSION = 236
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
- Adds "supplier_name" to the PurchaseOrder API serializer
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
- Adds "on_order" quantity to SalesOrderLineItem serializer
- Adds "building" quantity to SalesOrderLineItem serializer
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
- Fixes bug in the plugin metadata endpoint
v233 - 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
- Adds new endpoints for managing state of build orders
- Adds new endpoints for managing state of purchase orders
- Adds new endpoints for managing state of sales orders
- Adds new endpoints for managing state of return orders
v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
- Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date'
v231 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794
- Optimize BuildItem and BuildLine serializers to improve API efficiency
v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164
- Adds test statistics endpoint
v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775
- Add extra exportable fields to the BomItem serializer
v228 - 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
- Adds "icon" field to the PartCategory.path and StockLocation.path API
- Adds icon packages API endpoint
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
- Adds endpoints to list and revoke the tokens issued to the current user
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
- Adds barcode generation API endpoint
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
- Adds "filters" field to DataImportSession API
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
- Add notes field to ManufacturerPart and SupplierPart API endpoints
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
- Allow adjustment of "packaging" field when receiving items against a purchase order
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
- Adds missing fields from StockItemBriefSerializer
- Adds missing fields from PartBriefSerializer
- Adds extra exportable fields to BuildItemSerializer
v220 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
- Adds "revision_of" field to Part serializer
- Adds new API filters for "revision" status
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
- Adds new fields to the BuildItem API endpoints
- Adds new ordering / filtering options to the BuildItem API endpoints
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
- Adds "can_build" field to the BomItem API
v217 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599
- Fixes bug in "project_code" field for order API endpoints
v216 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595
- Moves API endpoint for contenttype lookup by model name
v215 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591
- Adds additional fields to the BuildLine serializer
v214 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587
- Adds "default_location_detail" field to the Part API
v213 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527
- Adds 'locked' field to Part API
v212 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562
- Makes API generation more robust (no functional changes)
v211 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911
- Adds API endpoints for managing data import and export
v210 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518
- Adds translateable text to User API fields
v209 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514
- Add "top_level" filter to PartCategory API endpoint
- Add "top_level" filter to StockLocation API endpoint
v208 - 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479
- Adds documentation for the user roles API endpoint (no functional changes)
v207 - 2024-06-09 : https://github.com/inventree/InvenTree/pull/7420
- Moves all "Attachment" models into a single table
- All "Attachment" operations are now performed at /api/attachment/
- Add permissions information to /api/user/roles/ endpoint
v206 - 2024-06-08 : https://github.com/inventree/InvenTree/pull/7417
- Adds "choices" field to the PartTestTemplate model
@ -111,7 +215,7 @@ v178 - 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
- Adds "external_stock" field to the Part API endpoint
- Adds "external_stock" field to the BomItem API endpoint
- Adds "external_stock" field to the BuildLine API endpoint
- Stock quantites represented in the BuildLine API endpoint are now filtered by Build.source_location
- Stock quantities represented in the BuildLine API endpoint are now filtered by Build.source_location
v177 - 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581
- Adds "subcategoies" count to PartCategoryTree serializer

View File

@ -11,6 +11,8 @@ from django.core.exceptions import AppRegistryNotReady
from django.db import transaction
from django.db.utils import IntegrityError, OperationalError
from allauth.socialaccount.signals import social_account_updated
import InvenTree.conversion
import InvenTree.ready
import InvenTree.tasks
@ -70,6 +72,12 @@ class InvenTreeConfig(AppConfig):
self.add_user_on_startup()
self.add_user_from_file()
# register event receiver and connect signal for SSO group sync. The connected signal is
# used for account updates whereas the receiver is used for the initial account creation.
from InvenTree import sso
social_account_updated.connect(sso.ensure_sso_groups)
def remove_obsolete_tasks(self):
"""Delete any obsolete scheduled tasks in the database."""
obsolete = [

View File

@ -153,7 +153,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
if unit:
try:
valid = unit in ureg
except Exception as exc:
except Exception:
valid = False
if not valid:
@ -196,7 +196,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
try:
value = convert_value(attempt, unit)
break
except Exception as exc:
except Exception:
value = None
if value is None:

View File

@ -9,7 +9,6 @@ import traceback
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db.utils import IntegrityError, OperationalError
from django.utils.translation import gettext_lazy as _
import rest_framework.views as drfviews

View File

@ -8,6 +8,7 @@ from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from common.currency import currency_code_default, currency_codes
from common.settings import get_global_setting
logger = logging.getLogger('inventree')
@ -22,14 +23,13 @@ class InvenTreeExchange(SimpleExchangeBackend):
def get_rates(self, **kwargs) -> dict:
"""Set the requested currency codes and get rates."""
from common.models import InvenTreeSetting
from plugin import registry
base_currency = kwargs.get('base_currency', currency_code_default())
symbols = kwargs.get('symbols', currency_codes())
# Find the selected exchange rate plugin
slug = InvenTreeSetting.get_setting('CURRENCY_UPDATE_PLUGIN', '', create=False)
slug = get_global_setting('CURRENCY_UPDATE_PLUGIN', create=False)
if slug:
plugin = registry.get_plugin(slug)

View File

@ -33,7 +33,7 @@ class InvenTreeRestURLField(RestURLField):
def run_validation(self, data=empty):
"""Override default validation behaviour for this field type."""
strict_urls = get_global_setting('INVENTREE_STRICT_URLS', True, cache=False)
strict_urls = get_global_setting('INVENTREE_STRICT_URLS', cache=False)
if not strict_urls and data is not empty and '://' not in data:
# Validate as if there were a schema provided

View File

@ -15,6 +15,7 @@ from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth_2fa.adapter import OTPAdapter
from allauth_2fa.forms import TOTPDeviceForm
from allauth_2fa.utils import user_has_valid_totp_device
from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText
from crispy_forms.helper import FormHelper
@ -190,7 +191,7 @@ class CustomSignupForm(SignupForm):
# check for two password fields
if not get_global_setting('LOGIN_SIGNUP_PWD_TWICE'):
self.fields.pop('password2')
self.fields.pop('password2', None)
# reorder fields
set_form_field_order(
@ -211,6 +212,16 @@ class CustomSignupForm(SignupForm):
return cleaned_data
class CustomTOTPDeviceForm(TOTPDeviceForm):
"""Ensure that db registration is enabled."""
def __init__(self, user, metadata=None, **kwargs):
"""Override to check if registration is open."""
if not settings.MFA_ENABLED:
raise forms.ValidationError(_('MFA Registration is disabled.'))
super().__init__(user, metadata, **kwargs)
def registration_enabled():
"""Determine whether user registration is enabled."""
if get_global_setting('LOGIN_ENABLE_REG') or InvenTree.sso.registration_enabled():
@ -269,7 +280,9 @@ class RegistratonMixin:
# Check if a default group is set in settings
start_group = get_global_setting('SIGNUP_GROUP')
if start_group:
if (
start_group and user.groups.count() == 0
): # check that no group has been added through SSO group sync
try:
group = Group.objects.get(id=start_group)
user.groups.add(group)

View File

@ -3,7 +3,6 @@
import datetime
import hashlib
import io
import json
import logging
import os
import os.path
@ -27,7 +26,6 @@ from bleach import clean
from djmoney.money import Money
from PIL import Image
import InvenTree.version
from common.currency import currency_code_default
from .settings import MEDIA_URL, STATIC_URL
@ -396,41 +394,9 @@ def WrapWithQuotes(text, quote='"'):
return text
def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
"""Generate a string for a barcode. Adds some global InvenTree parameters.
Args:
cls_name: string describing the object type e.g. 'StockItem'
object_pk (int): ID (Primary Key) of the object in the database
object_data: Python dict object containing extra data which will be rendered to string (must only contain stringable values)
Returns:
json string of the supplied data plus some other data
"""
if object_data is None:
object_data = {}
brief = kwargs.get('brief', True)
data = {}
if brief:
data[cls_name] = object_pk
else:
data['tool'] = 'InvenTree'
data['version'] = InvenTree.version.inventreeVersion()
data['instance'] = InvenTree.version.inventreeInstanceName()
# Ensure PK is included
object_data['id'] = object_pk
data[cls_name] = object_data
return str(json.dumps(data, sort_keys=True))
def GetExportFormats():
"""Return a list of allowable file formats for exporting data."""
return ['csv', 'tsv', 'xls', 'xlsx', 'json', 'yaml']
"""Return a list of allowable file formats for importing or exporting tabular data."""
return ['csv', 'xlsx', 'tsv', 'json']
def DownloadFile(

View File

@ -2,8 +2,10 @@
import inspect
from pathlib import Path
from typing import Any, Callable
from django.conf import settings
from django.core.cache import cache
from plugin import registry as plg_registry
@ -104,3 +106,37 @@ class ClassProviderMixin:
except ValueError:
# Path(...).relative_to throws an ValueError if its not relative to the InvenTree source base dir
return False
def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
"""Get a mixin class that provides shared state for classes across the main application and worker.
Arguments:
get_state_key: A function that returns the key for the shared state when given a class instance.
"""
class SharedClassStateMixinClass:
"""Mixin to provide shared state for classes across the main application and worker."""
def set_shared_state(self, key: str, value: Any):
"""Set a shared state value for this machine.
Arguments:
key: The key for the shared state
value: The value to set
"""
cache.set(self._get_key(key), value, timeout=None)
def get_shared_state(self, key: str, default=None):
"""Get a shared state value for this machine.
Arguments:
key: The key for the shared state
"""
return cache.get(self._get_key(key)) or default
def _get_key(self, key: str):
"""Get the key for this class instance."""
return f'{get_state_key(self)}:{key}'
return SharedClassStateMixinClass

View File

@ -15,9 +15,6 @@ from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from PIL import Image
import InvenTree
import InvenTree.helpers_model
import InvenTree.version
from common.notifications import (
InvenTreeNotificationBodies,
NotificationBody,
@ -39,8 +36,6 @@ def get_base_url(request=None):
3. If settings.SITE_URL is set (e.g. in the Django settings), use that
4. If the InvenTree setting INVENTREE_BASE_URL is set, use that
"""
import common.models
# Check if a request is provided
if request:
return request.build_absolute_uri('/')
@ -107,8 +102,6 @@ def download_image_from_url(remote_url, timeout=2.5):
ValueError: Server responded with invalid 'Content-Length' value
TypeError: Response is not a valid image
"""
import common.models
# Check that the provided URL at least looks valid
validator = URLValidator()
validator(remote_url)
@ -206,8 +199,6 @@ def render_currency(
max_decimal_places: The maximum number of decimal places to render to. If unspecified, uses the PRICING_DECIMAL_PLACES setting.
include_symbol: If True, include the currency symbol in the output
"""
import common.models
if money in [None, '']:
return '-'
@ -252,7 +243,7 @@ def render_currency(
def getModelsWithMixin(mixin_class) -> list:
"""Return a list of models that inherit from the given mixin class.
"""Return a list of database models that inherit from the given mixin class.
Args:
mixin_class: The mixin class to search for
@ -331,9 +322,7 @@ def notify_users(
'instance': instance,
'name': content.name.format(**content_context),
'message': content.message.format(**content_context),
'link': InvenTree.helpers_model.construct_absolute_url(
instance.get_absolute_url()
),
'link': construct_absolute_url(instance.get_absolute_url()),
'template': {'subject': content.name.format(**content_context)},
}

View File

@ -15,6 +15,7 @@ Additionally, update the following files with the new locale code:
from django.utils.translation import gettext_lazy as _
LOCALES = [
('ar', _('Arabic')),
('bg', _('Bulgarian')),
('cs', _('Czech')),
('da', _('Danish')),
@ -23,6 +24,7 @@ LOCALES = [
('en', _('English')),
('es', _('Spanish')),
('es-mx', _('Spanish (Mexican)')),
('et', _('Estonian')),
('fa', _('Farsi / Persian')),
('fi', _('Finnish')),
('fr', _('French')),

View File

@ -0,0 +1,53 @@
"""Custom management command to export settings definitions.
This is used to generate a JSON file which contains all of the settings,
so that they can be introspected by the InvenTree documentation system.
This in turn allows settings to be documented in the InvenTree documentation,
without having to manually duplicate the information in multiple places.
"""
import json
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Extract settings information, and export to a JSON file."""
def add_arguments(self, parser):
"""Add custom arguments for this command."""
parser.add_argument(
'filename', type=str, help='Output filename for settings definitions'
)
def handle(self, *args, **kwargs):
"""Export settings information to a JSON file."""
from common.models import InvenTreeSetting, InvenTreeUserSetting
settings = {'global': {}, 'user': {}}
# Global settings
for key, setting in InvenTreeSetting.SETTINGS.items():
settings['global'][key] = {
'name': str(setting['name']),
'description': str(setting['description']),
'default': str(InvenTreeSetting.get_setting_default(key)),
'units': str(setting.get('units', '')),
}
# User settings
for key, setting in InvenTreeUserSetting.SETTINGS.items():
settings['user'][key] = {
'name': str(setting['name']),
'description': str(setting['description']),
'default': str(InvenTreeUserSetting.get_setting_default(key)),
'units': str(setting.get('units', '')),
}
filename = kwargs.get('filename', 'inventree_settings.json')
with open(filename, 'w') as f:
json.dump(settings, f, indent=4)
print(f"Exported InvenTree settings definitions to '{filename}'")

View File

@ -0,0 +1,192 @@
"""Custom management command to migrate the old FontAwesome icons."""
import json
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError
from django.db import models
from common.icons import validate_icon
from part.models import PartCategory
from stock.models import StockLocation, StockLocationType
class Command(BaseCommand):
"""Generate an icon map from the FontAwesome library to the new icon library."""
help = """Helper command to migrate the old FontAwesome icons to the new icon library."""
def add_arguments(self, parser):
"""Add the arguments."""
parser.add_argument(
'--output-file',
type=str,
help='Path to file to write generated icon map to',
)
parser.add_argument(
'--input-file', type=str, help='Path to file to read icon map from'
)
parser.add_argument(
'--include-items',
default=False,
action='store_true',
help='Include referenced inventree items in the output icon map (optional)',
)
parser.add_argument(
'--import-now',
default=False,
action='store_true',
help='CAUTION: If this flag is set, the icon map will be imported and the database will be touched',
)
def handle(self, *args, **kwargs):
"""Generate an icon map from the FontAwesome library to the new icon library."""
# Check for invalid combinations of arguments
if kwargs['output_file'] and kwargs['input_file']:
raise CommandError('Cannot specify both --input-file and --output-file')
if not kwargs['output_file'] and not kwargs['input_file']:
raise CommandError('Must specify either --input-file or --output-file')
if kwargs['include_items'] and not kwargs['output_file']:
raise CommandError(
'--include-items can only be used with an --output-file specified'
)
if kwargs['output_file'] and kwargs['import_now']:
raise CommandError(
'--import-now can only be used with an --input-file specified'
)
ICON_MODELS = [
(StockLocation, 'custom_icon'),
(StockLocationType, 'icon'),
(PartCategory, '_icon'),
]
def get_model_items_with_icons(model: models.Model, icon_field: str):
"""Return a list of models with icon fields."""
return model.objects.exclude(**{f'{icon_field}__isnull': True}).exclude(**{
f'{icon_field}__exact': ''
})
# Generate output icon map file
if kwargs['output_file']:
icons = {}
for model, icon_name in ICON_MODELS:
self.stdout.write(
f'Processing model {model.__name__} with icon field {icon_name}'
)
items = get_model_items_with_icons(model, icon_name)
for item in items:
icon = getattr(item, icon_name)
try:
validate_icon(icon)
continue # Skip if the icon is already valid
except ValidationError:
pass
if icon not in icons:
icons[icon] = {
**({'items': []} if kwargs['include_items'] else {}),
'new_icon': '',
}
if kwargs['include_items']:
icons[icon]['items'].append({
'model': model.__name__.lower(),
'id': item.id, # type: ignore
})
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')
with open(kwargs['output_file'], 'w') as f:
json.dump(icons, f, indent=2)
self.stdout.write(f'Icon map written to {kwargs["output_file"]}')
# Import icon map file
if kwargs['input_file']:
with open(kwargs['input_file'], 'r') as f:
icons = json.load(f)
self.stdout.write(f'Loaded icon map for {len(icons.keys())} icons')
self.stdout.write('Verifying icon map')
has_errors = False
# Verify that all new icons are valid icons
for old_icon, data in icons.items():
try:
validate_icon(data.get('new_icon', ''))
except ValidationError:
self.stdout.write(
f'[ERR] Invalid icon: "{old_icon}" -> "{data.get("new_icon", "")}'
)
has_errors = True
# Verify that all required items are provided in the icon map
for model, icon_name in ICON_MODELS:
self.stdout.write(
f'Processing model {model.__name__} with icon field {icon_name}'
)
items = get_model_items_with_icons(model, icon_name)
for item in items:
icon = getattr(item, icon_name)
try:
validate_icon(icon)
continue # Skip if the icon is already valid
except ValidationError:
pass
if icon not in icons:
self.stdout.write(
f' [ERR] Icon "{icon}" not found in icon map'
)
has_errors = True
# If there are errors, stop here
if has_errors:
self.stdout.write(
'[ERR] Icon map has errors, please fix them before continuing with importing'
)
return
# Import the icon map into the database if the flag is set
if kwargs['import_now']:
self.stdout.write('Start importing icons and updating database...')
cnt = 0
for model, icon_name in ICON_MODELS:
self.stdout.write(
f'Processing model {model.__name__} with icon field {icon_name}'
)
items = get_model_items_with_icons(model, icon_name)
for item in items:
icon = getattr(item, icon_name)
try:
validate_icon(icon)
continue # Skip if the icon is already valid
except ValidationError:
pass
setattr(item, icon_name, icons[icon]['new_icon'])
cnt += 1
item.save()
self.stdout.write(
f'Icon map successfully imported - changed {cnt} items'
)
self.stdout.write('Icons are now migrated')
else:
self.stdout.write('Icon map is valid and ready to be imported')
self.stdout.write(
'Run the command with --import-now to import the icon map and update the database'
)

View File

@ -115,9 +115,14 @@ class InvenTreeMetadata(SimpleMetadata):
return metadata
def override_value(self, field_name, field_value, model_value):
def override_value(self, field_name: str, field_key: str, field_value, model_value):
"""Override a value on the serializer with a matching value for the model.
Often, the serializer field will point to an underlying model field,
which contains extra information (which is translated already).
Rather than duplicating this information in the serializer, we can extract it from the model.
This is used to override the serializer values with model values,
if (and *only* if) the model value should take precedence.
@ -125,17 +130,28 @@ class InvenTreeMetadata(SimpleMetadata):
- field_value is None
- model_value is callable, and field_value is not (this indicates that the model value is translated)
- model_value is not a string, and field_value is a string (this indicates that the model value is translated)
Arguments:
- field_name: The name of the field
- field_key: The property key to override
- field_value: The value of the field (if available)
- model_value: The equivalent value of the model (if available)
"""
if model_value and not field_value:
if field_value is None and model_value is not None:
return model_value
if field_value and not model_value:
if model_value is None and field_value is not None:
return field_value
# Callable values will be evaluated later
if callable(model_value) and not callable(field_value):
return model_value
if type(model_value) is not str and type(field_value) is str:
if callable(field_value) and not callable(model_value):
return field_value
# Prioritize translated text over raw string values
if type(field_value) is str and type(model_value) is not str:
return model_value
return field_value
@ -144,6 +160,8 @@ class InvenTreeMetadata(SimpleMetadata):
"""Override get_serializer_info so that we can add 'default' values to any fields whose Meta.model specifies a default value."""
self.serializer = serializer
request = getattr(self, 'request', None)
serializer_info = super().get_serializer_info(serializer)
# Look for any dynamic fields which were not available when the serializer was instantiated
@ -153,12 +171,19 @@ class InvenTreeMetadata(SimpleMetadata):
# Already know about this one
continue
if hasattr(serializer, field_name):
field = getattr(serializer, field_name)
if field := getattr(serializer, field_name, None):
serializer_info[field_name] = self.get_field_info(field)
model_class = None
# Extract read_only_fields and write_only_fields from the Meta class (if available)
if meta := getattr(serializer, 'Meta', None):
read_only_fields = getattr(meta, 'read_only_fields', [])
write_only_fields = getattr(meta, 'write_only_fields', [])
else:
read_only_fields = []
write_only_fields = []
# Attributes to copy extra attributes from the model to the field (if they don't exist)
# Note that the attributes may be named differently on the underlying model!
extra_attributes = {
@ -172,16 +197,20 @@ class InvenTreeMetadata(SimpleMetadata):
model_fields = model_meta.get_field_info(model_class)
model_default_func = getattr(model_class, 'api_defaults', None)
if model_default_func:
model_default_values = model_class.api_defaults(self.request)
if model_default_func := getattr(model_class, 'api_defaults', None):
model_default_values = model_default_func(request=request) or {}
else:
model_default_values = {}
# Iterate through simple fields
for name, field in model_fields.fields.items():
if name in serializer_info.keys():
if name in read_only_fields:
serializer_info[name]['read_only'] = True
if name in write_only_fields:
serializer_info[name]['write_only'] = True
if field.has_default():
default = field.default
@ -197,10 +226,12 @@ class InvenTreeMetadata(SimpleMetadata):
serializer_info[name]['default'] = model_default_values[name]
for field_key, model_key in extra_attributes.items():
field_value = serializer_info[name].get(field_key, None)
field_value = getattr(serializer.fields[name], field_key, None)
model_value = getattr(field, model_key, None)
if value := self.override_value(name, field_value, model_value):
if value := self.override_value(
name, field_key, field_value, model_value
):
serializer_info[name][field_key] = value
# Iterate through relations
@ -213,6 +244,12 @@ class InvenTreeMetadata(SimpleMetadata):
# Ignore reverse relations
continue
if name in read_only_fields:
serializer_info[name]['read_only'] = True
if name in write_only_fields:
serializer_info[name]['write_only'] = True
# Extract and provide the "limit_choices_to" filters
# This is used to automatically filter AJAX requests
serializer_info[name]['filters'] = (
@ -220,10 +257,12 @@ class InvenTreeMetadata(SimpleMetadata):
)
for field_key, model_key in extra_attributes.items():
field_value = serializer_info[name].get(field_key, None)
field_value = getattr(serializer.fields[name], field_key, None)
model_value = getattr(relation.model_field, model_key, None)
if value := self.override_value(name, field_value, model_value):
if value := self.override_value(
name, field_key, field_value, model_value
):
serializer_info[name][field_key] = value
if name in model_default_values:
@ -241,7 +280,8 @@ class InvenTreeMetadata(SimpleMetadata):
if instance is None and model_class is not None:
# Attempt to find the instance based on kwargs lookup
kwargs = getattr(self.view, 'kwargs', None)
view = getattr(self, 'view', None)
kwargs = getattr(view, 'kwargs', None) if view else None
if kwargs:
pk = None
@ -298,8 +338,10 @@ class InvenTreeMetadata(SimpleMetadata):
# Force non-nullable fields to read as "required"
# (even if there is a default value!)
if not field.allow_null and not (
hasattr(field, 'allow_blank') and field.allow_blank
if (
'required' not in field_info
and not field.allow_null
and not (hasattr(field, 'allow_blank') and field.allow_blank)
):
field_info['required'] = True
@ -326,8 +368,11 @@ class InvenTreeMetadata(SimpleMetadata):
field_info['api_url'] = '/api/user/'
elif field_info['model'] == 'contenttype':
field_info['api_url'] = '/api/contenttype/'
else:
elif hasattr(model, 'get_api_url'):
field_info['api_url'] = model.get_api_url()
else:
logger.warning("'get_api_url' method not defined for %s", model)
field_info['api_url'] = getattr(model, 'api_url', None)
# Handle custom 'primary key' field
field_info['pk_field'] = getattr(field, 'pk_field', 'pk') or 'pk'

View File

@ -12,6 +12,7 @@ from django.urls import Resolver404, include, path, resolve, reverse_lazy
from allauth_2fa.middleware import AllauthTwoFactorMiddleware, BaseRequire2FAMiddleware
from error_report.middleware import ExceptionProcessor
from common.settings import get_global_setting
from InvenTree.urls import frontendpatterns
from users.models import ApiToken
@ -153,11 +154,9 @@ class Check2FAMiddleware(BaseRequire2FAMiddleware):
def require_2fa(self, request):
"""Use setting to check if MFA should be enforced for frontend page."""
from common.models import InvenTreeSetting
try:
if url_matcher.resolve(request.path[1:]):
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
return get_global_setting('LOGIN_ENFORCE_MFA')
except Resolver404:
pass
return False

View File

@ -1,13 +1,9 @@
"""Generic models which provide extra functionality over base Django model types."""
import logging
import os
from datetime import datetime
from io import BytesIO
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
@ -20,11 +16,11 @@ from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
import common.settings
import InvenTree.fields
import InvenTree.format
import InvenTree.helpers
import InvenTree.helpers_model
from InvenTree.sanitizer import sanitize_svg
logger = logging.getLogger('inventree')
@ -97,7 +93,7 @@ class PluginValidationMixin(DiffMixin):
return
except ValidationError as exc:
raise exc
except Exception as exc:
except Exception:
# Log the exception to the database
import InvenTree.exceptions
@ -218,12 +214,15 @@ class MetadataMixin(models.Model):
self.save()
class DataImportMixin(object):
class DataImportMixin:
"""Model mixin class which provides support for 'data import' functionality.
Models which implement this mixin should provide information on the fields available for import
"""
# TODO: This mixin should be removed after https://github.com/inventree/InvenTree/pull/6911 is implemented
# TODO: This approach to data import functionality is *outdated*
# Define a map of fields available for import
IMPORT_FIELDS = {}
@ -304,10 +303,7 @@ class ReferenceIndexingMixin(models.Model):
if cls.REFERENCE_PATTERN_SETTING is None:
return ''
# import at function level to prevent cyclic imports
from common.models import InvenTreeSetting
return InvenTreeSetting.get_setting(
return common.settings.get_global_setting(
cls.REFERENCE_PATTERN_SETTING, create=False
).strip()
@ -503,207 +499,71 @@ class InvenTreeMetadataModel(MetadataMixin, InvenTreeModel):
abstract = True
def rename_attachment(instance, filename):
"""Function for renaming an attachment file. The subdirectory for the uploaded file is determined by the implementing class.
Args:
instance: Instance of a PartAttachment object
filename: name of uploaded file
Returns:
path to store file, format: '<subdir>/<id>/filename'
"""
# Construct a path to store a file attachment for a given model type
return os.path.join(instance.getSubdir(), filename)
class InvenTreeAttachment(InvenTreeModel):
class InvenTreeAttachmentMixin:
"""Provides an abstracted class for managing file attachments.
An attachment can be either an uploaded file, or an external URL
Links the implementing model to the common.models.Attachment table,
and provides the following methods:
Attributes:
attachment: Upload file
link: External URL
comment: String descriptor for the attachment
user: User associated with file upload
upload_date: Date the file was uploaded
- attachments: Return a queryset containing all attachments for this model
"""
class Meta:
"""Metaclass options. Abstract ensures no database table is created."""
def delete(self):
"""Handle the deletion of a model instance.
abstract = True
def getSubdir(self):
"""Return the subdirectory under which attachments should be stored.
Note: Re-implement this for each subclass of InvenTreeAttachment
Before deleting the model instance, delete any associated attachments.
"""
return 'attachments'
def save(self, *args, **kwargs):
"""Provide better validation error."""
# Either 'attachment' or 'link' must be specified!
if not self.attachment and not self.link:
raise ValidationError({
'attachment': _('Missing file'),
'link': _('Missing external link'),
})
if self.attachment and self.attachment.name.lower().endswith('.svg'):
self.attachment.file.file = self.clean_svg(self.attachment)
super().save(*args, **kwargs)
def clean_svg(self, field):
"""Sanitize SVG file before saving."""
cleaned = sanitize_svg(field.file.read())
return BytesIO(bytes(cleaned, 'utf8'))
def __str__(self):
"""Human name for attachment."""
if self.attachment is not None:
return os.path.basename(self.attachment.name)
return str(self.link)
attachment = models.FileField(
upload_to=rename_attachment,
verbose_name=_('Attachment'),
help_text=_('Select file to attach'),
blank=True,
null=True,
)
link = InvenTree.fields.InvenTreeURLField(
blank=True,
null=True,
verbose_name=_('Link'),
help_text=_('Link to external URL'),
)
comment = models.CharField(
blank=True,
max_length=100,
verbose_name=_('Comment'),
help_text=_('File comment'),
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_('User'),
help_text=_('User'),
)
upload_date = models.DateField(
auto_now_add=True, null=True, blank=True, verbose_name=_('upload date')
)
self.attachments.all().delete()
super().delete()
@property
def basename(self):
"""Base name/path for attachment."""
if self.attachment:
return os.path.basename(self.attachment.name)
return None
def attachments(self):
"""Return a queryset containing all attachments for this model."""
return self.attachments_for_model().filter(model_id=self.pk)
@basename.setter
def basename(self, fn):
"""Function to rename the attachment file.
@classmethod
def check_attachment_permission(cls, permission, user) -> bool:
"""Check if the user has permission to perform the specified action on the attachment.
- Filename cannot be empty
- Filename cannot contain illegal characters
- Filename must specify an extension
- Filename cannot match an existing file
The default implementation runs a permission check against *this* model class,
but this can be overridden in the implementing class if required.
Arguments:
permission: The permission to check (add / change / view / delete)
user: The user to check against
Returns:
bool: True if the user has permission, False otherwise
"""
fn = fn.strip()
perm = f'{cls._meta.app_label}.{permission}_{cls._meta.model_name}'
return user.has_perm(perm)
if len(fn) == 0:
raise ValidationError(_('Filename must not be empty'))
def attachments_for_model(self):
"""Return all attachments for this model class."""
from common.models import Attachment
attachment_dir = settings.MEDIA_ROOT.joinpath(self.getSubdir())
old_file = settings.MEDIA_ROOT.joinpath(self.attachment.name)
new_file = settings.MEDIA_ROOT.joinpath(self.getSubdir(), fn).resolve()
model_type = self.__class__.__name__.lower()
# Check that there are no directory tricks going on...
if new_file.parent != attachment_dir:
logger.error(
"Attempted to rename attachment outside valid directory: '%s'", new_file
)
raise ValidationError(_('Invalid attachment directory'))
return Attachment.objects.filter(model_type=model_type)
# Ignore further checks if the filename is not actually being renamed
if new_file == old_file:
return
def create_attachment(self, attachment=None, link=None, comment='', **kwargs):
"""Create an attachment / link for this model."""
from common.models import Attachment
forbidden = [
"'",
'"',
'#',
'@',
'!',
'&',
'^',
'<',
'>',
':',
';',
'/',
'\\',
'|',
'?',
'*',
'%',
'~',
'`',
]
kwargs['attachment'] = attachment
kwargs['link'] = link
kwargs['comment'] = comment
kwargs['model_type'] = self.__class__.__name__.lower()
kwargs['model_id'] = self.pk
for c in forbidden:
if c in fn:
raise ValidationError(_(f"Filename contains illegal character '{c}'"))
if len(fn.split('.')) < 2:
raise ValidationError(_('Filename missing extension'))
if not old_file.exists():
logger.error(
"Trying to rename attachment '%s' which does not exist", old_file
)
return
if new_file.exists():
raise ValidationError(_('Attachment with this filename already exists'))
try:
os.rename(old_file, new_file)
self.attachment.name = os.path.join(self.getSubdir(), fn)
self.save()
except Exception:
raise ValidationError(_('Error renaming file'))
def fully_qualified_url(self):
"""Return a 'fully qualified' URL for this attachment.
- If the attachment is a link to an external resource, return the link
- If the attachment is an uploaded file, return the fully qualified media URL
"""
if self.link:
return self.link
if self.attachment:
media_url = InvenTree.helpers.getMediaUrl(self.attachment.url)
return InvenTree.helpers_model.construct_absolute_url(media_url)
return ''
Attachment.objects.create(**kwargs)
class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
"""Provides an abstracted self-referencing tree model for data categories.
- Each Category has one parent Category, which can be blank (for a top-level Category).
- Each Category can have zero-or-more child Categor(y/ies)
- Each Category can have zero-or-more child Category(y/ies)
Attributes:
name: brief name
@ -715,6 +575,9 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
# e.g. for StockLocation, this value is 'location'
ITEM_PARENT_KEY = None
# Extra fields to include in the get_path result. E.g. icon
EXTRA_PATH_FIELDS = []
class Meta:
"""Metaclass defines extra model properties."""
@ -920,7 +783,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
on_delete=models.DO_NOTHING,
blank=True,
null=True,
verbose_name=_('parent'),
verbose_name='parent',
related_name='children',
)
@ -1008,7 +871,14 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
name: <name>,
}
"""
return [{'pk': item.pk, 'name': item.name} for item in self.path]
return [
{
'pk': item.pk,
'name': item.name,
**{k: getattr(item, k, None) for k in self.EXTRA_PATH_FIELDS},
}
for item in self.path
]
def __str__(self):
"""String representation of a category is the full path to that category."""
@ -1072,6 +942,8 @@ class InvenTreeBarcodeMixin(models.Model):
- barcode_data : Raw data associated with an assigned barcode
- barcode_hash : A 'hash' of the assigned barcode data used to improve matching
The barcode_model_type_code() classmethod must be implemented in the model class.
"""
class Meta:
@ -1102,11 +974,25 @@ class InvenTreeBarcodeMixin(models.Model):
# By default, use the name of the class
return cls.__name__.lower()
@classmethod
def barcode_model_type_code(cls):
r"""Return a 'short' code for the model type.
This is used to generate a efficient QR code for the model type.
It is expected to match this pattern: [0-9A-Z $%*+-.\/:]{2}
Note: Due to the shape constrains (45**2=2025 different allowed codes)
this needs to be explicitly implemented in the model class to avoid collisions.
"""
raise NotImplementedError(
'barcode_model_type_code() must be implemented in the model class'
)
def format_barcode(self, **kwargs):
"""Return a JSON string for formatting a QR code for this model instance."""
return InvenTree.helpers.MakeBarcode(
self.__class__.barcode_model_type(), self.pk, **kwargs
)
from plugin.base.barcodes.helper import generate_barcode
return generate_barcode(self)
def format_matched_response(self):
"""Format a standard response for a matched barcode."""
@ -1124,7 +1010,7 @@ class InvenTreeBarcodeMixin(models.Model):
@property
def barcode(self):
"""Format a minimal barcode string (e.g. for label printing)."""
return self.format_barcode(brief=True)
return self.format_barcode()
@classmethod
def lookup_barcode(cls, barcode_hash):

View File

@ -115,6 +115,7 @@ def canAppAccessDatabase(
'makemessages',
'compilemessages',
'spectactular',
'collectstatic',
]
if not allow_shell:
@ -125,7 +126,7 @@ def canAppAccessDatabase(
excluded_commands.append('test')
if not allow_plugins:
excluded_commands.extend(['collectstatic', 'collectplugins'])
excluded_commands.extend(['collectplugins'])
for cmd in excluded_commands:
if cmd in sys.argv:

View File

@ -404,6 +404,17 @@ class UserSerializer(InvenTreeModelSerializer):
read_only_fields = ['username']
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
first_name = serializers.CharField(
label=_('First Name'), help_text=_('First name of the user')
)
last_name = serializers.CharField(
label=_('Last Name'), help_text=_('Last name of the user')
)
email = serializers.EmailField(
label=_('Email'), help_text=_('Email address of the user')
)
class ExendedUserSerializer(UserSerializer):
"""Serializer for a User with a bit more info."""
@ -424,6 +435,16 @@ class ExendedUserSerializer(UserSerializer):
read_only_fields = UserSerializer.Meta.read_only_fields + ['groups']
is_staff = serializers.BooleanField(
label=_('Staff'), help_text=_('Does this user have staff permissions')
)
is_superuser = serializers.BooleanField(
label=_('Superuser'), help_text=_('Is this user a superuser')
)
is_active = serializers.BooleanField(
label=_('Active'), help_text=_('Is this user account active')
)
def validate(self, attrs):
"""Expanded validation for changing user role."""
# Check if is_staff or is_superuser is in attrs
@ -509,43 +530,6 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
return os.path.join(str(settings.MEDIA_URL), str(value))
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
"""Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
The only real addition here is that we support "renaming" of the attachment file.
"""
@staticmethod
def attachment_fields(extra_fields=None):
"""Default set of fields for an attachment serializer."""
fields = [
'pk',
'attachment',
'filename',
'link',
'comment',
'upload_date',
'user',
'user_detail',
]
if extra_fields:
fields += extra_fields
return fields
user_detail = UserSerializer(source='user', read_only=True, many=False)
attachment = InvenTreeAttachmentSerializerField(required=False, allow_null=False)
# The 'filename' field must be present in the serializer
filename = serializers.CharField(
label=_('Filename'), required=False, source='basename', allow_blank=False
)
upload_date = serializers.DateField(read_only=True)
class InvenTreeImageSerializerField(serializers.ImageField):
"""Custom image serializer.
@ -872,7 +856,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
remote_image = serializers.URLField(
required=False,
allow_blank=False,
allow_blank=True,
write_only=True,
label=_('Remote Image'),
help_text=_('URL of remote image file'),

View File

@ -18,7 +18,6 @@ import django.conf.locale
import django.core.exceptions
from django.core.validators import URLValidator
from django.http import Http404
from django.utils.translation import gettext_lazy as _
import pytz
from dotenv import load_dotenv
@ -198,6 +197,7 @@ INSTALLED_APPS = [
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'machine.apps.MachineConfig',
'importer.apps.ImporterConfig',
'web',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
@ -296,10 +296,11 @@ ADMIN_SHELL_IMPORT_MODELS = False
if (
DEBUG
and INVENTREE_ADMIN_ENABLED
and not TESTING
and get_boolean_setting('INVENTREE_DEBUG_SHELL', 'debug_shell', False)
): # noqa
try:
import django_admin_shell
import django_admin_shell # noqa: F401
INSTALLED_APPS.append('django_admin_shell')
ADMIN_SHELL_ENABLE = True
@ -1208,6 +1209,9 @@ ACCOUNT_FORMS = {
'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
}
ALLAUTH_2FA_FORMS = {'setup': 'InvenTree.forms.CustomTOTPDeviceForm'}
# Determine if multi-factor authentication is enabled for this server (default = True)
MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True)
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
@ -1272,7 +1276,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

@ -3,6 +3,7 @@
import logging
from importlib import import_module
from django.conf import settings
from django.urls import NoReverseMatch, include, path, reverse
from allauth.account.models import EmailAddress
@ -177,7 +178,9 @@ class SocialProviderListView(ListAPI):
data = {
'sso_enabled': InvenTree.sso.login_enabled(),
'sso_registration': InvenTree.sso.registration_enabled(),
'mfa_required': get_global_setting('LOGIN_ENFORCE_MFA'),
'mfa_required': settings.MFA_ENABLED
and get_global_setting('LOGIN_ENFORCE_MFA'),
'mfa_enabled': settings.MFA_ENABLED,
'providers': provider_list,
'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'),
'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'),

View File

@ -1,7 +1,14 @@
"""Helper functions for Single Sign On functionality."""
import json
import logging
from django.contrib.auth.models import Group
from django.db.models.signals import post_save
from django.dispatch import receiver
from allauth.socialaccount.models import SocialAccount, SocialLogin
from common.settings import get_global_setting
from InvenTree.helpers import str2bool
@ -75,3 +82,55 @@ def registration_enabled() -> bool:
def auto_registration_enabled() -> bool:
"""Return True if SSO auto-registration is enabled."""
return str2bool(get_global_setting('LOGIN_SIGNUP_SSO_AUTO'))
def ensure_sso_groups(sender, sociallogin: SocialLogin, **kwargs):
"""Sync groups from IdP each time a SSO user logs on.
This event listener is registered in the apps ready method.
"""
if not get_global_setting('LOGIN_ENABLE_SSO_GROUP_SYNC'):
return
group_key = get_global_setting('SSO_GROUP_KEY')
group_map = json.loads(get_global_setting('SSO_GROUP_MAP'))
# map SSO groups to InvenTree groups
group_names = []
for sso_group in sociallogin.account.extra_data.get(group_key, []):
if mapped_name := group_map.get(sso_group):
group_names.append(mapped_name)
# ensure user has groups
user = sociallogin.account.user
for group_name in group_names:
try:
user.groups.get(name=group_name)
except Group.DoesNotExist:
# user not in group yet
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
logger.info(f'Creating group {group_name} as it did not exist')
group = Group(name=group_name)
group.save()
logger.info(f'Adding group {group_name} to user {user}')
user.groups.add(group)
# remove groups not listed by SSO if not disabled
if get_global_setting('SSO_REMOVE_GROUPS'):
for group in user.groups.all():
if not group.name in group_names:
logger.info(f'Removing group {group.name} from {user}')
user.groups.remove(group)
@receiver(post_save, sender=SocialAccount)
def on_social_account_created(sender, instance: SocialAccount, created: bool, **kwargs):
"""Sync SSO groups when new SocialAccount is added.
Since the allauth `social_account_added` signal is not sent for some reason, this
signal is simulated using post_save signals. The issue has been reported as
https://github.com/pennersr/django-allauth/issues/3834
"""
if created:
ensure_sso_groups(None, SocialLogin(account=instance))

View File

@ -1101,3 +1101,19 @@ a {
.large-treeview-icon {
font-size: 1em;
}
.api-icon {
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better font rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.test-statistics-table-total-row {
font-weight: bold;
border-top-style: double;
}

View File

@ -60,10 +60,6 @@ function exportFormatOptions() {
value: 'tsv',
display_name: 'TSV',
},
{
value: 'xls',
display_name: 'XLS',
},
{
value: 'xlsx',
display_name: 'XLSX',

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2024 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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